Allow printing as specific unit

Issue #2 resolved
Former user created an issue

Currently, when trying to print a Volume, Mass, etc. the only option is to use toString, which spits out the base value (such as 10 g for Mass), however, I think it'd be beneficial to allow something like:

Mass x = 1.grams;

print(x.toStringAs(milli.grams))

1000 mg

I believe something like this can be accomplished by adding another value to the MeasurementPrefix constructors namely a string representation of the prefix (m for milli, n for nano, etc.) and then concatenating this with the correct unit after this (such as g for grams).

In my use case I have to display the units in various occasions and have to be able to convert units and display those values with the unit, which is quite a hazard at the moment.

Thank you in advance!

Comments (8)

  1. Bruce Santier repo owner

    Thank you for the request!

    In general, supporting things like the toString() method seems fraught with danger to me. No matter what I choose to put there, someone will complain that the format isn't quite right. For example, all of these are valid outputs for the same measurement (see here for an in-depth discussion of the problem):

    1000.0 mg

    1000.0 milligrams

    1,000.0 mg

    1.000,0 mg

    1 000,0 mg

    The toString() method must choose a format, but is not given any parameters to work with. Further, there are many differing standards worldwide for how quantities should be displayed. We could create a bunch of different toString()-esque methods for various formats, but that smacks of serious method proliferation. For this reason, I would recommend (for the time being) avoiding the use of toString() for anything but example code and instead building the output you want yourself (perhaps also throwing in something like NumberFormat to get the number format you want):

    Mass x = 1.grams;
    print("I have ${x.as(milli.grams)} mg!");
    

    I have 1000.0 mg!

    A helper method or class could reduce duplication. Of course, this requires that you manage the various strings for each unit type yourself (e.g. the “mg”), and the syntax overall isn’t all that nice. For these reasons, I will propose a slightly different solution, though this may take some time to implement correctly...

    Suppose we had a formatter that you could configure with a few settings, such as whether to use the short form of unit names or which decimal separator to use. You would likely only need one instance of this thing for your application, tailored to your needs (this is all hypothetical code):

    var myFormatter = UnitFormatter(unitNames: UnitNames.short, decimalSeparator: '.', thousandsSeparator: ',', grouping);
    

    To simplify things a bit, we could provide regional defaults, so you could choose one of the standard formatting options without having to name all of the parameters yourself:

    var myFormatter = Formatters.NIST;
    // or
    var myFormatter = Formatters.BIPM;
    

    Then, a single toString()-esque method would work, as long as it accepted a UnitFormatter and a :

    print("I have ${x.toStringAs(milli.grams, myFormatter)}");
    

    To go a little further, we could set up a single place where users could set a UnitFormatter of their choice and make the second argument to toStringAs() optional. At the risk of becoming a little cultural-centric, we could also choose some default settings in case users don’t explicitly define a formatter themselves:

    // supposing the default formatter is something like the NIST formatter...
    print("I have ${x.toStringAs(milli.grams)!}"); // 1,000.0 mg
    
    fling.defaultFormatter = UnitFormatter.BIPM;
    print("I have ${x.toStringAs(milli.grams)!}"); // 1 000,0 mg
    print("I have ${x.toStringAs(milli.grams, UnitFormatter.NIST)!}"); // 1,000.0 mg
    

    Since we already have the concept of precision in the library, we could take advantage of that to format the numbers a little more correctly as well. Currently, even if your precision is 1, printing the value of x results in 1000.0 since it’s a double. Ideally, we don’t want to imply that we have more precision by including the decimal point. So, our formatter could examine the measurement being printed and be smarter about displaying significant digits.

    As I mentioned, this will likely take a little time to get things right. In the meantime, we could still define the interface and provide a basic formatter akin to the one you describe. In other words, there would not be a way to define the fling.defaultFormatter and toStringAs() would accept only the first parameter (the unit). It would be understood that this method will format things exactly one way, but it would set up the interface for the long term. This way, the recommendation for displaying stringified measurements can be to use toStringAs(), and once we support different schemes, a single change in your code base to set the default formatter would “fix” all the places that need it.

    // short term solution, offers no customization
    print("I have ${x.toStringAs(milli.grams)!}"); // 1000 mg
    

    I think we could get something like this done much more quickly while still meeting your needs.

    Now that I look back at all of this, I am a little ashamed to have said quite so much when, in the end, I proposed exactly the same interface and functionality you did. I’ll start on a solution, but would still be interested in your thoughts on my proposed extension if you have any.

  2. Luca Cras

    Everything you’ve said seems reasonable to me. I agree with you about the difficulty to allow for all the specific formatting needs.

    Your proposed solution definitely works, and you might also consider leaving the number formatting to the user and opt for a way to get a specific unit string from the unit itself, for example:

    final x = 1.grams;
    
    print(I have ${x.as(milli.grams) ${milli.grams.toString(format: UnitFormat.short)}") // 1000 mg
    

    This might be a little easier to implement and still makes it easier to print the correct unit.

    Either way, I support both!

  3. Bruce Santier repo owner

    Thanks, I was actually a little apprehensive about getting into the number formatting business! Another thing I have realized is that the full unit names will vary by locale, so I would have a localization problem trying to support all of those (e.g. “meters” vs. “metres” vs. “metros” etc.). Thus, I propose the simplest solution would be to support toString() on the unit interpreters to produce the short form of the unit (I don’t think there are any localization issues there…). Your example could just be written as:

    final x = 1.grams;
    print(I have ${x.as(milli.grams)} ${milli.grams}") // 1000.0 mg
    

    I’ll save localizing the full names of units for another day.

  4. Bruce Santier repo owner

    Version 2.2.0 (available now) should have what you want.

    In short, you’ll have a bit more control over the interpreters each measurement uses. By default, a measurement will use the same interpreter that was used to create it. For example:

    var myDistance = 100.miles;
    print(myDistance); // "100 mi"
    

    You can create a measurement with a different interpreter with the new withDefaultUnit() method:

    var myDistance = 100.miles.withDefaultUnit(kilo.meters);
    print(myDistance); // "160.934 km"
    

    The interpreters themselves are toString()-able now, so you can get the short form of the unit name easily that way:

    print(miles); // "mi"
    print(kilo.meters); // "km"
    

    Which means you should be able to print measurements with whatever formatting you like:

    var myDistance = 100.miles;
    print("I have travelled ${myDistance.as(kilo.meters)} ${kilo.meters} today!");
    

    Please let me know if you have any issues or further recommendations!

  5. Log in to comment