10

I am writing a recipe manager for a software engineering class I am taking, and I would like to implement classes (in Java, if it matters) to allow quantities and units.

Now, taking recipes into account, we have the typical 'scientific' units like kg, g, lbs, ml, l (dividing between SI, SI-derived, avoirdupois, US customary, imperial, etc); then there are the non-standard units like tbsp, tsp, cups, and finally, we have the countable units like 'cloves', 'slices', 'sticks'.

The good thing is that all of these units have officially been defined in terms of SI units, and can hence be easily converted amongst one another. This is ideally another feature I'd like to implement.

Right now I have an extremely long enum of these units and their values in the corresponding SI units. I feel I'm doing something wrong.

How best should I implement something like this? I have already explored the javax.measure API and its reference implementation, Indriya, but documentation is rather sparse and I'm not sure how to proceed.

The examples above make use of Java generics, which is something I might use.

9
  • 1
    Martin Fowler gave quantities a chapter in his book "Analysis patterns", it may of interest for you. I found an online excerpt here
    – Doc Brown
    Apr 10, 2020 at 10:54
  • 3
    Note for recipes, I foresee only two kinds of calculations for quanties which make sense: multiplication by a scalar (for the use case "recalculation to a different number of portions"), and unit conversion between metric and imperial system (for the use case "display the recipe in the preferred system of an international user"). But you should check if you really have to support the latter - clarify your use cases first! Do not implement something you don't have clear requirements for.
    – Doc Brown
    Apr 10, 2020 at 10:57
  • 1
    There is an official definition in SI units for cloves, slices, and sticks? Really? Apr 10, 2020 at 20:57
  • 1
    On the subject of unit conversion, be careful. One occasionally sees recipes like "add 113.4 grams of butter", which looks ridiculous. What are we to do, get out the milligram scales we all have lying about the house for weighing dust motes in order to make sure there is not 113.5 or 113.3 grams of butter in that banana bread? Of course what has happened is some silly person has translated "a quarter pound of butter" to 113.4 grams without noticing that they've gone from one significant digit to four. Apr 11, 2020 at 4:44
  • 1
    What I'm really saying here is think about the end users' needs and their usage cases, and let that inform your approach, rather than going the other way; don't start with a possibly-inappropriate technological solution and then take a hammer to it to make it conform to user requirements. Apr 11, 2020 at 4:47

3 Answers 3

3

You will find it exceedingly advantageous to avoid the enum and "wrap" the unit into a class of its own. You can only do that after you have noticed that these units you are referencing are not (and cannot be, in fact) entirely equivalent in terms of Physics.

So you are not looking at Units, you are looking at Measures, indeed (think not kg, g, mg, lbs... but Mass, not L, mL, fl. oz, but Volume).

Then you can do:

public class Mass
{

    private double _gramsToKgFactor = 0.001;
    private double _gramsToPoundsFactor = 0.00220462262;

    private double _grams; //Use a common internal representation.

    //Properties that convert to the desired unit.
    public double Grams => _grams;
    public double Kilograms => _grams * _gramsToKgFactor;
    public double Pounds => _grams * _gramsToPoundsFactor;
    //....

    //Constructor
    private Mass(double grams)
    {
        _grams = grams;
    }


    //Couple of Factory Methods
    public static Mass FromGrams(double grams)
    {
        return new Mass(grams);
    }

    public static Mass FromPounds(double pounds)
    {
        //Divide here, so that you get the other measure of the conversion.
        return new Mass(pounds / _gramsToPoundsFactor);
    }

    //etc. SIMILARLY for Volumes and other units too.
}

That's pretty much the first step you can use to improve. As your codebase grows considerably, you will find out that you need to AVOID having to think about units that much. Units are only relevant at the "boundaries", i.e. where you input data, and where you output data. They are (almost) not important for your operations. You are not "juggling" units anyway, you are juggling physical quantities, Mass, Volume, etc.

NOTE: You will want to create some operators (add, subtract, multiply, etc.) for your Mass, Volume etc. classes too, typically by operating on the common underlying value, e.g. addition will simply do return Mass.FromGrams(val1.Grams + val2.Grams);. You may need Equality methods as well.

NOTE 2: As Hans-Martin Mosner brilliantly suggests, you can keep the conversion factors in some other organized location, if you don't want them lying inside your classes. They are simply unavoidable.

13

I'd be highly surprised if there's an official definition of 'clove', 'slice', or 'stick' in terms of SI units, do you have a pointer? :-)

As a general rule (regardless of whether it's a class exercise or a real world problem) don't overcomplicate this.

There are often many ways of doing it right and many more ways of doing it wrong, but no single best way.

Look at the actual use cases that your code has to support, and choose a representation that supports these use cases well. It should lead to well-readable and maintainable code (i.e. no complicated sequences to construct a quantity.)

If the main purpose of your program is to store and present recipes using the units originally given, a simple (amount, unit) tuple suffices probably. It still allows you to scale the recipe to a different number of servings while avoiding issues with ill-defined units such as tsp or slice.

If you need actual conversion between unit systems (for example to convert recipes from metric to imperial units) a better internal representation might be (amount_in_SI_unit, presentation_unit) since that defers the conversion step until actual numbers have to be shown, which avoids any loss of precision due to intermediate rounding.

Conversion tables listing unit names and conversion factors are tedious but unavoidable if you need to perform conversions. Choose a table format that keeps information in one place, again to make the code readable and maintainable.

Finally, the point of a class exercise is probably not to create perfect code but to show that you are able to make informed choices when designing a solution to a problem. Write down the reasoning for doing it the way you do (and "some guy on stack exchange recommended it" is not a valid reason.) This enables the teacher to better judge your solution, and in real world software development it enables the next person working with your code to understand and modify it the way it was meant to be.

5
  • 4
    "do you have a pointer?" - here you go. Apr 10, 2020 at 21:00
  • 1
    One US Customary stick of butter is 118.3 mL.
    – Mark
    Apr 10, 2020 at 21:55
  • interesting, giving that butter is sold by weight
    – njzk2
    Apr 10, 2020 at 22:05
  • @njzk2 turns out you can sell it by volume as well. Apr 11, 2020 at 1:36
  • That was a typo: I meant 'most of the above units'. Thanks for a clear, straightforward response!
    – SRSR333
    Apr 11, 2020 at 3:00
1

What should really drive your design ?

It is indeed tempting to use a very general library to handle units to the perfection.

It is appealing to know that it can convert between any units using the SI system. It is reinsuring to know that javax.measure provides a function to return the right square-meter unit when raising meters unit to the power of 2. But will it help you to achieve your objectives and fulfil the requirements?

But what's a slice? a thick slice? a thin slice? a medium slice? Let's look at a slice of ham. The SI doesn't define the conversion of a slice to a gram! In fact, I doubt that it s a question of units: a slice of ham will certainly not weithg the same than a slice of cheese or a slice of tomato.

Your design should not be driven by perfection, but by requirements.

Is there a simple, pragmatic approach ?

The main requirement for a recipe is to know what quantity of each ingredient is needed, so:

double quantity;           // 3 
unit_id unit;              // thin slices
material_id ingredient;    // of ham

There are two kind of units: the known SI units, and the non-SI cooking units. The SI units in the kitchen are units of weight or unit volume. The non-SI units are pieces of something.

So you can have two classes for this: * SI units, which depending on the dimension (volume or weight) can convert automatically to the relevant base unit (e.g. a kg is 1000 g) * Non SI units, which depending on the ingredient can convert to other non SI or to SI units (e.g. 1 thick slice of ham is 2 thin slices of ham, which are each an average 40g of ham). * SI units from different dimensions, mainly volume to weight and vis-versa (e.g. 1 ml of olive oil, is how many grams?). Here it is also a conversion that is ingredient dependent, but unlike exotic units, it can be simply calculated based on the density of the ingredient

So in the end you just need a conversion between different SI units (ingredient independent) and a conversion from exotic units to SI units (material dependent, and based on either averages or ranges).

The advantage of such a simple model, is that it's very easy to implement. Major ERP systems use such a simple model to routinely encode industrial BOMs and routings from everything from aircrafts to luxus sandwiches. It's a proven approach. It can work for you.

How to do it ?

You can encode the metric prefix in the relevant class.

The unit could be encoded in their own class. For every unit, you'd have an inidciation whether it's SI (and which dimension) or if it's exotic.

Finaly you'd have a class for the conversion factors. It would be ingredient dependent and define the conversion factors from one source unit to a target unit.

Otherwise stated, you'd have very little hard-coded, and most of the information that is kept in ingredient-dependent entities.

Final remark

For teh sake of simplicity, I have mainly spoken about units for ingredients, and have ignored the activities. Activities are time dependent.

It's not in any SI definition, but when it comes to cooking there are two kind of times units:

  • The absolute time units: Boil the egg for 4 minutes. This is straight forward.
  • The relative units: Put the chicken in the oven until it's roasted. THis is more complex, since very often it's not absolute but it will depend on the ingredients (chicken doesn't get roasted as fast as salmon) and the weight ( either you hava a calculation formula based on teh energy absorbtion of the ingredient, or youdo as myself, you ask your butcher and determine a range table (half an hour for small chicken under 500g, one hour between 500 and 1kg. 2 hours above 1 kg). So here, it's a notion of scale.

I say this, because industrial practice is to store BOMs and routings separately, except for chemical industry (and cooking should belong to this), which merge the two notions into a single recipe that contains both ingredients and activities to process these ingredients.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.