I am TRWTF



  • Enjoy the ugliest thing I've ever created:

             private static ElementParser<ReadOnly.Order> _orderParser = new ElementParser<ReadOnly.Order>(
                new ElementAction<ReadOnly.Order>("orderid", (r, o) => { o.ID = r.ReadString(); }),
                new ElementAction<ReadOnly.Order>("ordtoken", (r, o) => { o.Token = r.ReadString(); }),
                new ElementAction<ReadOnly.Order>("ordcustid", (r, o) => { o.Customer = r.ReadString(); }),
                new ElementAction<ReadOnly.Order>("orddate", (r, o) => { o.OrderedOn = r.ReadString().FromUnix().ToLocalTime(); }),
                new ElementAction<ReadOnly.Order>("ordlastmodified", (r, o) => { o.ModifiedOn = r.ReadString().FromUnix().ToLocalTime(); }),
                new ElementAction<ReadOnly.Order>("ebay_order_id", (r, o) => { o.EbayID = r.ReadString(); }),
                new ElementAction<ReadOnly.Order>("subtotal_ex_tax", (r, o) => { o.SubtotalCosts.ExcludingTax = r.ReadString().ToDecimal(); }),
                new ElementAction<ReadOnly.Order>("subtotal_inc_tax", (r, o) => { o.SubtotalCosts.IncludingTax = r.ReadString().ToDecimal(); }),
                new ElementAction<ReadOnly.Order>("subtotal_tax", (r, o) => { o.SubtotalCosts.Tax = r.ReadString().ToDecimal(); }),
                new ElementAction<ReadOnly.Order>("total_tax", (r, o) => { o.TotalCosts.Tax = r.ReadString().ToDecimal(); }),
                new ElementAction<ReadOnly.Order>("base_shipping_cost", (r, o) => { o.ShippingCosts.BaseCost = r.ReadString().ToDecimal(); }),
                new ElementAction<ReadOnly.Order>("shipping_cost_ex_tax", (r, o) => { o.ShippingCosts.ExcludingTax = r.ReadString().ToDecimal(); }),
                new ElementAction<ReadOnly.Order>("shipping_cost_inc_tax", (r, o) => { o.ShippingCosts.IncludingTax = r.ReadString().ToDecimal(); }),
                new ElementAction<ReadOnly.Order>("shipping_cost_tax", (r, o) => { o.ShippingCosts.Tax = r.ReadString().ToDecimal(); }),
                new ElementAction<ReadOnly.Order>("shipping_cost_tax_class_id", (r, o) => { o.ShippingCosts.TaxClassID = r.ReadString(); }),
                new ElementAction<ReadOnly.Order>("base_handling_cost", (r, o) => { o.HandlingCosts.BaseCost = r.ReadString().ToDecimal(); }),
                new ElementAction<ReadOnly.Order>("handling_cost_ex_tax", (r, o) => { o.HandlingCosts.ExcludingTax = r.ReadString().ToDecimal(); }),
                new ElementAction<ReadOnly.Order>("handling_cost_inc_tax", (r, o) => { o.HandlingCosts.IncludingTax = r.ReadString().ToDecimal(); }),
                new ElementAction<ReadOnly.Order>("handling_cost_tax", (r, o) => { o.HandlingCosts.Tax = r.ReadString().ToDecimal(); }),
                new ElementAction<ReadOnly.Order>("handling_cost_tax_class_id", (r, o) => { o.HandlingCosts.TaxClassID = r.ReadString(); }),
                new ElementAction<ReadOnly.Order>("base_wrapping_cost", (r, o) => { o.WrappingCosts.BaseCost = r.ReadString().ToDecimal(); }),
                new ElementAction<ReadOnly.Order>("wrapping_cost_ex_tax", (r, o) => { o.WrappingCosts.ExcludingTax = r.ReadString().ToDecimal(); }),
                new ElementAction<ReadOnly.Order>("wrapping_cost_inc_tax", (r, o) => { o.WrappingCosts.IncludingTax = r.ReadString().ToDecimal(); }),
                new ElementAction<ReadOnly.Order>("wrapping_cost_tax", (r, o) => { o.WrappingCosts.Tax = r.ReadString().ToDecimal(); }),
                new ElementAction<ReadOnly.Order>("wrapping_cost_tax_class_id", (r, o) => { o.WrappingCosts.TaxClassID = r.ReadString(); }),
                new ElementAction<ReadOnly.Order>("total_ex_tax", (r, o) => { o.TotalCosts.ExcludingTax = r.ReadString().ToDecimal(); }),
                new ElementAction<ReadOnly.Order>("total_inc_tax", (r, o) => { o.TotalCosts.IncludingTax = r.ReadString().ToDecimal(); }),
                new ElementAction<ReadOnly.Order>("ordstatus", (r, o) => { o.Status = (OrderStatus)r.ReadString().ToInt(); }),
                new ElementAction<ReadOnly.Order>("ordtotalqty", (r, o) => { o.PiecesOrdered = r.ReadString().ToInt(); }),
                new ElementAction<ReadOnly.Order>("ordtotalshipped", (r, o) => { o.PiecesShipped = r.ReadString().ToInt(); }),
                new ElementAction<ReadOnly.Order>("orderpaymentmethod", (r, o) => { o.PaymentMethod = r.ReadString(); }),
                new ElementAction<ReadOnly.Order>("orderpaymentmodule", (r, o) => { o.PaymentModule = r.ReadString(); }),
                new ElementAction<ReadOnly.Order>("ordpayproviderid", (r, o) => { o.PaymentProviderID = r.ReadString(); }),
                new ElementAction<ReadOnly.Order>("ordpaymentstatus", (r, o) => { o.PaymentStatus = r.ReadString(); }),
                new ElementAction<ReadOnly.Order>("ordrefundedamount", (r, o) => { o.RefundAmount = r.ReadString().ToDecimal(); }),
                new ElementAction<ReadOnly.Order>("ordbillfirstname", (r, o) => { o.BillToAddress.FirstName = r.ReadString(); }),
                new ElementAction<ReadOnly.Order>("ordbilllastname", (r, o) => { o.BillToAddress.LastName = r.ReadString(); }),
                new ElementAction<ReadOnly.Order>("ordbillcompany", (r, o) => { o.BillToAddress.CompanyName = r.ReadString(); }),
                new ElementAction<ReadOnly.Order>("ordbillstreet1", (r, o) => { o.BillToAddress.Address1 = r.ReadString(); }),
                new ElementAction<ReadOnly.Order>("ordbillstreet2", (r, o) => { o.BillToAddress.Address2 = r.ReadString(); }),
                new ElementAction<ReadOnly.Order>("ordbillsuburb", (r, o) => { o.BillToAddress.City = r.ReadString(); }),
                new ElementAction<ReadOnly.Order>("ordbillstate", (r, o) => { o.BillToAddress.State = r.ReadString(); }),
                new ElementAction<ReadOnly.Order>("ordbillzip", (r, o) => { o.BillToAddress.Zip = r.ReadString(); }),
                new ElementAction<ReadOnly.Order>("ordbillcountry", (r, o) => { o.BillToAddress.Country = r.ReadString(); }),
                new ElementAction<ReadOnly.Order>("ordbillcountrycode", (r, o) => { o.BillToAddress.CountryCode = r.ReadString(); }),
                new ElementAction<ReadOnly.Order>("ordbillcountryid", (r, o) => { o.BillToAddress.CountryID = r.ReadString(); }),
                new ElementAction<ReadOnly.Order>("ordbillstateid", (r, o) => { o.BillToAddress.StateID = r.ReadString(); }),
                new ElementAction<ReadOnly.Order>("ordbillphone", (r, o) => { o.BillToAddress.Phone = r.ReadString(); }),
                new ElementAction<ReadOnly.Order>("ordbillemail", (r, o) => { o.BillToAddress.Email = r.ReadString(); }),
                new ElementAction<ReadOnly.Order>("ordisdigital", (r, o) => { o.IsDigital = r.ReadString().ToBool(); }),
                new ElementAction<ReadOnly.Order>("orddateshipped", (r, o) => { o.ShippedOn = r.ReadString().FromUnix().ToLocalTime(); }),
                new ElementAction<ReadOnly.Order>("ordstorecreditamount", (r, o) => { o.CreditAmount = r.ReadString().ToDecimal(); }),
                new ElementAction<ReadOnly.Order>("ordgiftcertificateamount", (r, o) => { o.GiftCertificateAmount = r.ReadString().ToDecimal(); }),
                new ElementAction<ReadOnly.Order>("ordinventoryupdated", (r, o) => { o.WasInventoryUpdated = r.ReadString().ToBool(); }),
                new ElementAction<ReadOnly.Order>("ordonlygiftcerts", (r, o) => { o.HasOnlyGiftCertificates = r.ReadString().ToBool(); }),
                new ElementAction<ReadOnly.Order>("ordipaddress", (r, o) => { o.IP = r.ReadString(); }),
                new ElementAction<ReadOnly.Order>("ordgeoipcountry", (r, o) => { o.IPCountry = r.ReadString(); }),
                new ElementAction<ReadOnly.Order>("ordgeoipcountrycode", (r, o) => { o.IPCountryCode = r.ReadString(); }),
                new ElementAction<ReadOnly.Order>("ordcurrencyid", (r, o) => { o.Currency = r.ReadString(); }),
                new ElementAction<ReadOnly.Order>("orddefaultcurrencyid", (r, o) => { o.DefaultCurrency = r.ReadString(); }),
                new ElementAction<ReadOnly.Order>("ordcurrencyexchangerate", (r, o) => { o.ExchangeRate = r.ReadString().ToDecimal(); }),
                new ElementAction<ReadOnly.Order>("ordnotes", (r, o) => { o.Notes = r.ReadString(); }),
                new ElementAction<ReadOnly.Order>("ordcustmessage", (r, o) => { o.Message = r.ReadString(); }),
                new ElementAction<ReadOnly.Order>("ordvendorid", (r, o) => { o.VendorID = r.ReadString(); }),
                new ElementAction<ReadOnly.Order>("ordformsessionid", (r, o) => { o.SessionID = r.ReadString(); }),
                new ElementAction<ReadOnly.Order>("orddiscountamount", (r, o) => { o.DiscountAmount = r.ReadString().ToDecimal(); }),
                new ElementAction<ReadOnly.Order>("shipping_address_count", (r, o) => { o.ShipToCount = r.ReadString().ToInt(); }),
                new ElementAction<ReadOnly.Order>("coupon_discount", (r, o) => { o.CouponAmount = r.ReadString().ToDecimal(); }),
                new ElementAction<ReadOnly.Order>("deleted", (r, o) => { o.IsDeleted = r.ReadString().ToBool(); }),
                new ElementAction<ReadOnly.Order>("items", (r, o) => { o.Items = _itemsParser.Parse(r); }),
                new ElementAction<ReadOnly.Order>("order_addresses", (r, o) => { o.ShipToAddress = _shipToParser.Parse(r); }),
                new ElementAction<ReadOnly.Order>("taxes", (r, o) => { o.OrderTaxes = _taxesParser.Parse(r); })
                );



  • One time working on a project merging data between two ERP I had to create a 1.2MB query. It was not data, it was not a dump file, it was not a DDL script - it was actually 1.2MB of SQL code. Because of the requirements I had no way to do it differently.



  • W...T...F...

    Please don't tell me you're doing this for more than one object...  Please!

    Seriously though, I've been the author of some similar WTFey code, probably worse than this, for one-off things.  What's the context?



  • I've had to do things like this before, too, as I could see no other way to do it except for putting the same info into a config file or database ... I've not seen any other way to do it.  What *IS* the alternative?



  • @C-Octothorpe said:

    What's the context?
    I was hoping for guesses, so I could identify any fellow-travellers.  It used to be a giant switch, but I found that I was able to encapsulate the part of this type of activity that causes the most bugs if I used a more OO solution than just a switch

    Let's just say that ElementParser is part of a system designed to create and initialize T : new() objects from an XML stream.  It's part of a package that lets me asynchronously send an XML POST, get the XML response, and convert that response into objects live as each node of the XML data comes in off the NIC.



  • @hoodaticus said:

    @C-Octothorpe said:

    What's the context?
    I was hoping for guesses, so I could identify any fellow-travellers.
    On first glance, it looks like you're creating a mapping object between some key/value pair dictionary and your business objects...  I hope your doing some caching here.

    I'd also guess that you have some generic method where you pass this object in, the dictionary and returns a collection of your business objects.



  • @C-Octothorpe said:

    On first glance, it looks like you're creating a mapping object between some key/value pair dictionary and your business objects...  I hope your doing some caching here.

    I'd also guess that you have some generic method where you pass this object in, the dictionary and returns a collection of your business objects.

    Exactly.  And I am caching; the types I make that use this will have their parsers as static members so they don't have to be built every time.

    When you parse an XML stream, you have to switch on the element name at some point.  Usually, a lot.  When to call and when not to call the .Read() method tends towards bugginess, so I built two or three stupid classes like ElementParser<T> so that I won't have to touch the XmlReader beyond using ReadString.  I'm thinking of wrapping or inheriting XmlTextReader and eliminating the possibility of manually advancing the stream to complete the circle of safety

    But it's still ugly as sin.

     



  • Switch to Java

    Use jibx



  • It would be cool if that was somehow your whole program.



  • So... are those varargs or does ElementParser's constructor really take 72 arguments?



  • @PSWorx said:

    So... are those varargs or does ElementParser's constructor really take 72 arguments?

    This isn't the whole class, since I'm not at work right now, but it's a rough sketch of the members:

    public class ElementParser<T> where T : new()
    public ElementParser(params ElementAction<T>[]] actions) { /*store the actions in a map*/}
    public T Parse(XmlTextReader reader)
    {
         T t = new T(); 
         /* when an element with a matching dictionary entry is found */ action.Action(reader, t); 
         /* when the closing element for the element this method started on is located */ return t;
    }

    The idea is that you pass the Parse method an XmlTextReader positioned at the opening tag of the element that you want to turn into a T.  It then loops through all sub-element and, if the name matches an ElementAction, the action/lambda expression is executed on it.  When it reaches the end of the element it started on, it returns.  There's one ElementAction / constructor parameter for each element you have to handle.

    Since most of my graphs are nested anyway, this means inevitably nesting ElementParsers inside ElementActions inside ElementParsers...

    On the other hand, it does add some extra safety over my earlier approach, code named Switchthulhu.



  • @thistooshallpass said:

    One time working on a project merging data between two ERP I had to create a 1.2MB query. It was not data, it was not a dump file, it was not a DDL script - it was actually 1.2MB of SQL code. Because of the requirements I had no way to do it differently.
    I bet after you ran it and it all worked like a charm, you had an earth-shattering progasm and needed a smoke after.



  • @millimeep said:

    Switch to Java

    Use jibx

    Never heard of it, but if I were using Java, I probably would have gone straight to SAXON and never looked back.



  • It's C#, isn't it? It could be done with attributes and reflections. Create an attribute with appropriate name and optional parser, attach it to the properties and construct the actions with reflection. Somewhat like what the XmlSerializer does. It's a bit complicated by the fact that some of the properties are in member objects, but recursing to the right objects shouldn't be too hard.

    I guess creating it manually is OK if you have one or two objects, but if you have tens of them, the reflection way would be a lot easier to maintain.



  • @Bulb said:

    It's C#, isn't it? It could be done with attributes and reflections.
    An excellent suggestion, not that I hadn't thought of it.  But if I weren't doing this for performance reasons, I would have used LINQ on an XDocument and been done with it.

    I also had the bright idea to serialize a sample business object, then build an XSLT stylesheet to transform the source XML into a serialized graph, then simply de-serialize the transformed XML.  Also not feasible for performance reasons.



  • @hoodaticus said:

    performance

    But ... XML!



  • At the very least you could try to factor out the repeated stuff, e.g.

    using Str = StringAction<ReadOnly.Order>;
    using Dec = DecimalAction<ReadOnly.Order>;
    using Raw = ElementAction<ReadOnly.Order>;
    ...
    private static ElementParser<ReadOnly.Order> _orderParser = new ElementParser<ReadOnly.Order>(
    new Str("orderid", (r, o) => { o.ID = r; }),
    new Dec("base_shipping_cost", (r, o) => { o.ShippingCosts.BaseCost = r; }), new Raw("order_addresses", (r, o) => { o.ShipToAddress = _shipToParser.Parse(r); }),
    ...

    Now my eyes bleed 17.8% less.



  • You can't at least standardise the names a bit and then auto-generate this code?



  • I'm curious what FromUnix does


  • ♿ (Parody)

    @Sutherlands said:

    I'm curious what FromUnix does

    Looks like a unix timestamp.



  • It's not really comparable (in the mathematical sense of the word), but if we're talking "ugliest thing I ever created", this is mine.

    As for the reasons, this is for a BI report on performance measure achievement. Some performance measures are percentages, some are numbers, some are dollar amounts. Since they're all mixed together we couldn't use the report's built-in column formatting. Also, we needed the report to be able to do aggregation of totals, so the values passed to the report had to be numeric - that meant we couldn't do any preliminary stuff in the logical layer, such as converting to a string and locating the decimal point; it all had to be in the report formula. Further, the only number-to-string conversion function available was CAST(value AS CHARACTER(max length)) - no formatting options could be given. Experimentation showed that CAST AS CHARACTER sometimes returned two decimal places, sometimes one, and sometimes none.

    The report wound up as a union of three queries, one for each of the data types. Formatting the numbers and the percentages was pretty straightforward. This is what we wound up with for the dollar values. And no, we couldn't split it onto multiple lines for what little clarity might have been possible.

    CONCAT(CASE WHEN "Fact - Sales Actual Empower".Actual < 0 THEN '-' ELSE '' END, CONCAT('$', CASE WHEN LENGTH(CASE WHEN LOCATE('.', CAST(ABS("Fact - Sales Actual Empower".Actual) AS CHARACTER (50))) = 0 THEN CAST(ABS("Fact - Sales Actual Empower".Actual) AS CHARACTER (50)) ELSE SUBSTRING(CAST(ABS("Fact - Sales Actual Empower".Actual) AS CHARACTER (50)), 1, LOCATE('.', CAST(ABS("Fact - Sales Actual Empower".Actual) AS CHARACTER (50))) -1) END) > 9 THEN INSERT(INSERT(INSERT(CASE WHEN LOCATE('.', CAST(ABS("Fact - Sales Actual Empower".Actual) AS CHARACTER (50))) = 0 THEN CAST(ABS("Fact - Sales Actual Empower".Actual) AS CHARACTER (50)) ELSE SUBSTRING(CAST(ABS("Fact - Sales Actual Empower".Actual) AS CHARACTER (50)), 1, LOCATE('.', CAST(ABS("Fact - Sales Actual Empower".Actual) AS CHARACTER (50))) -1) END, LENGTH(CASE WHEN LOCATE('.', CAST(ABS("Fact - Sales Actual Empower".Actual) AS CHARACTER (50))) = 0 THEN CAST(ABS("Fact - Sales Actual Empower".Actual) AS CHARACTER (50)) ELSE SUBSTRING(CAST(ABS("Fact - Sales Actual Empower".Actual) AS CHARACTER (50)), 1, LOCATE('.', CAST(ABS("Fact - Sales Actual Empower".Actual) AS CHARACTER (50))) -1) END) - 2, 0, ','), LENGTH(CASE WHEN LOCATE('.', CAST(ABS("Fact - Sales Actual Empower".Actual) AS CHARACTER (50))) = 0 THEN CAST(ABS("Fact - Sales Actual Empower".Actual) AS CHARACTER (50)) ELSE SUBSTRING(CAST(ABS("Fact - Sales Actual Empower".Actual) AS CHARACTER (50)), 1, LOCATE('.', CAST(ABS("Fact - Sales Actual Empower".Actual) AS CHARACTER (50))) -1) END) - 5, 0, ','), LENGTH(CASE WHEN LOCATE('.', CAST(ABS("Fact - Sales Actual Empower".Actual) AS CHARACTER (50))) = 0 THEN CAST(ABS("Fact - Sales Actual Empower".Actual) AS CHARACTER (50)) ELSE SUBSTRING(CAST(ABS("Fact - Sales Actual Empower".Actual) AS CHARACTER (50)), 1, LOCATE('.', CAST(ABS("Fact - Sales Actual Empower".Actual) AS CHARACTER (50))) -1) END) - 8, 0, ',') ELSE CASE WHEN LENGTH(CASE WHEN LOCATE('.', CAST(ABS("Fact - Sales Actual Empower".Actual) AS CHARACTER (50))) = 0 THEN CAST(ABS("Fact - Sales Actual Empower".Actual) AS CHARACTER (50)) ELSE SUBSTRING(CAST(ABS("Fact - Sales Actual Empower".Actual) AS CHARACTER (50)), 1, LOCATE('.', CAST(ABS("Fact - Sales Actual Empower".Actual) AS CHARACTER (50))) -1) END) > 6 THEN INSERT(INSERT(CASE WHEN LOCATE('.', CAST(ABS("Fact - Sales Actual Empower".Actual) AS CHARACTER (50))) = 0 THEN CAST(ABS("Fact - Sales Actual Empower".Actual) AS CHARACTER (50)) ELSE SUBSTRING(CAST(ABS("Fact - Sales Actual Empower".Actual) AS CHARACTER (50)), 1, LOCATE('.', CAST(ABS("Fact - Sales Actual Empower".Actual) AS CHARACTER (50))) -1) END, LENGTH(CASE WHEN LOCATE('.', CAST(ABS("Fact - Sales Actual Empower".Actual) AS CHARACTER (50))) = 0 THEN CAST(ABS("Fact - Sales Actual Empower".Actual) AS CHARACTER (50)) ELSE SUBSTRING(CAST(ABS("Fact - Sales Actual Empower".Actual) AS CHARACTER (50)), 1, LOCATE('.', CAST(ABS("Fact - Sales Actual Empower".Actual) AS CHARACTER (50))) -1) END) - 2, 0, ','), LENGTH(CASE WHEN LOCATE('.', CAST(ABS("Fact - Sales Actual Empower".Actual) AS CHARACTER (50))) = 0 THEN CAST(ABS("Fact - Sales Actual Empower".Actual) AS CHARACTER (50)) ELSE SUBSTRING(CAST(ABS("Fact - Sales Actual Empower".Actual) AS CHARACTER (50)), 1, LOCATE('.', CAST(ABS("Fact - Sales Actual Empower".Actual) AS CHARACTER (50))) -1) END) - 5, 0, ',') ELSE CASE WHEN LENGTH(CASE WHEN LOCATE('.', CAST(ABS("Fact - Sales Actual Empower".Actual) AS CHARACTER (50))) = 0 THEN CAST(ABS("Fact - Sales Actual Empower".Actual) AS CHARACTER (50)) ELSE SUBSTRING(CAST(ABS("Fact - Sales Actual Empower".Actual) AS CHARACTER (50)), 1, LOCATE('.', CAST(ABS("Fact - Sales Actual Empower".Actual) AS CHARACTER (50))) -1) END) > 3 THEN INSERT(CASE WHEN LOCATE('.', CAST(ABS("Fact - Sales Actual Empower".Actual) AS CHARACTER (50))) = 0 THEN CAST(ABS("Fact - Sales Actual Empower".Actual) AS CHARACTER (50)) ELSE SUBSTRING(CAST(ABS("Fact - Sales Actual Empower".Actual) AS CHARACTER (50)), 1, LOCATE('.', CAST(ABS("Fact - Sales Actual Empower".Actual) AS CHARACTER (50))) -1) END, LENGTH(CASE WHEN LOCATE('.', CAST(ABS("Fact - Sales Actual Empower".Actual) AS CHARACTER (50))) = 0 THEN CAST(ABS("Fact - Sales Actual Empower".Actual) AS CHARACTER (50)) ELSE SUBSTRING(CAST(ABS("Fact - Sales Actual Empower".Actual) AS CHARACTER (50)), 1, LOCATE('.', CAST(ABS("Fact - Sales Actual Empower".Actual) AS CHARACTER (50))) -1) END) - 2, 0, ',') ELSE CASE WHEN LOCATE('.', CAST(ABS("Fact - Sales Actual Empower".Actual) AS CHARACTER (50))) = 0 THEN CAST(ABS("Fact - Sales Actual Empower".Actual) AS CHARACTER (50)) ELSE SUBSTRING(CAST(ABS("Fact - Sales Actual Empower".Actual) AS CHARACTER (50)), 1, LOCATE('.', CAST(ABS("Fact - Sales Actual Empower".Actual) AS CHARACTER (50))) -1) END END END END))

    There was a fair bit of cut and paste involved with creating this thing (once we got the basic component down that says "convert to character, check whether there's a decimal point, and if so strip off everything after it" we used that sucker everywhere; from memory I wound up cutting and pasting higher order components as well), and after creating it we resolved never, ever to attempt to maintain it. So far we haven't had to.

    Oh, how I wished for "to_char(num, '999,999,999,990')"... (was going to put that in the tags but the commas are pretty much essential, since inserting them what most of the statement is there for)


Log in to reply