2

I'm writing a recipe app that will need to perform addition/subtraction on ingredients to make a master shopping list. What I can't figure out is how to represent a fraction like 1/3 (which doesn't convert to a clean decimal) in a way that won't result in .9999999.... instead of 1. I can't even use ounces as a base unit because a third of a cup is 2.6666666 oz.

In short, the idea is that if the user is making recipes that use sugar, they may have one calling for a cup, another for half a cup, and another with a third of a cup. This would then tell them they need to get 1 5/6 cups of sugar on their next shopping trip.

Has anyone got a fix for this, or should I just ban all recipes with ugly fractions (which is far from ideal)?

The closest thing I've come up with so far is to store every ingredient as multiples of a third of an ounce, but that makes the backend logic a bit complicated!

Bonus points if the fix will allow me to easily store the value in a Postgres database!

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
sbrevolution5
  • 183
  • 2
  • 11
  • Why not multiply on, say, `60`? You are going to have `20` instead of `1 / 3`, `110` instead of `1 5 /6` etc – Dmitry Bychenko Jul 18 '21 at 19:21
  • 2
    https://stackoverflow.com/questions/1662538/how-deal-with-the-fact-that-most-decimal-fractions-cannot-be-accurately-represen has nothing to do with this question. This is not a duplicate of that. – abdusco Jul 18 '21 at 19:21
  • You can easily create a struct representing a fraction and use it however you like. In Postgresql looks like you could make use of [user defined data types](https://www.postgresql.org/docs/9.5/xtypes.html). – Cleptus Jul 18 '21 at 19:24
  • 1
    Store the absolute amount in grams (or pounds, if that's your thing) and let the user pick the units. Then when formatting the amount to cups, you round the amount to the nearest conventional fraction (5/6, not 11/13 and so on.) – abdusco Jul 18 '21 at 19:25
  • I think you'll find that C# itself handles this just fine. `(1.0/3)*3==1.0`. The issue is almost certainly in how you're storing that data in your Postgres database. I recommend reframing your question around that issue, since it's not an issue with C# specifically. – Jeremy Caney Jul 18 '21 at 19:27
  • @abdusco: of course it's a duplicate of that. Whether the problem is that the fraction 1/10 can't be represented as a binary floating point number, or 1/3 can't be, it's the exact same issue. It is absurd to think that the Stack Overflow community needs to revisit this with multiple new answers every time someone runs into the problem. – Peter Duniho Jul 18 '21 at 20:29

4 Answers4

3

I think you'd be better off combining an existing implementation of a fraction, like the Fraction type in the Fractions NuGet package with a unit, which would be an enumeration. Something like

struct Amount
{
    public Amount(Fraction quantity, Unit unit)
    {
        Quantity = quantity;
        Unit = unit;
    }

    public Fraction Quantity { get; }
    public Unit Unit { get; }

    ...
}

public enum Unit { Ounce, Cup, Gram, ... };

This leaves you with conversions between unlike units like cups/ounces, but I'd add some rules about rounding in there so you can still work with reasonable fractions. Because you're talking about recipes, this seems like a reasonable constraint because if you're off by a small amount, it won't make much difference to a recipe (hopefully)! Season to taste!

The conversions would be hidden behind operators; you'd overload +, =, etc.

For storage, you'd need 3 columns: numerator, denominator and unit. This makes for a 1:1 mapping from DB to C#.

You could even make a user defined data type as @Cleptus suggested in the comments, which would maintain a 1:1 mapping. There is a drawback to this approach as @Jeremy stated in the comments: lack of support for aggregates such as an average. If you don't need to worry about that (e.g. average sugar use for related recipes) then this is not a concern.

Yes, you could do all the math yourself, and even encapsulate it, but I think the natural representation would be more maintainable and would map well from client, to server, to storage.

Finally, because rounding is involved, it should be done at the last possible moment to reduce error accumulation, especially if you do aggregate functions like summing, averaging, and so on.

Kit
  • 20,354
  • 4
  • 60
  • 103
  • "_For storage, you'd need 3 columns: numerator, denominator and unit_" Or a user defined data type with those three properties – Cleptus Jul 18 '21 at 22:30
  • That would prevent you from using aggregate functions in sql, though you could add mass / volume SI base units as well. – Jeremy Lakeman Jul 19 '21 at 00:37
3

The coin has two sides. First, storing exact values is not a bug, it's a feature. Even though dividing with some numbers yield ugly results. Don't worry about them when you store them. What you do need to solve however, is the other side of the coin, namely the display. You can use Math.round with a few decimals if needed. When we speak about recipes, some level of preciseness is needed, but your soup will not be destroyed if a molecule of NaCl more makes its way into it than the absolute precise value, so for this purpose, rounding seems to be a reasonable approach.

Lajos Arpad
  • 64,414
  • 37
  • 100
  • 175
1

Assuming that you are working with "simple" vulgar fraction (you can face, say 1/3, 1 5/6, 3/4, but not 103/2043) you can try to multiply by some good common factor. 60, 600, 420 are typical choices:

   1/3 * 60   ==  20
   1 5/6 * 60 == 110
   3/4 * 60   ==  45

So you have good old integer values which are easy to store and manipulate. When we want to represent the result as vulgar fraction, all we should compute Greatest Common Divisor gcd(result, factor). For instance:

   1 5/6 - 1/4 => // let factor be 60
   110 - 15 == 95

So we store 95. When we want to represent it, since gcd(95, 60) = 5

   95 / 60 == 19 / 12 == 1 7/12

In general case, when any vulgar fraction is technically possible, we have to use BigRational or alike structures, e.g. https://www.nuget.org/packages?q=BigRational and store both numerator and denominator

Dmitry Bychenko
  • 180,369
  • 20
  • 160
  • 215
0

There is solution from MS website. Gives you instance of struct that stores 2 values nominator and denominator and overloads operators to get new instances of fracture with arithmetic.

https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/operator-overloading

public readonly struct Fraction
{
    private readonly int num;
    private readonly int den;

    public Fraction(int numerator, int denominator)
    {
        if (denominator == 0)
        {
            throw new ArgumentException("Denominator cannot be zero.", nameof(denominator));
        }
        num = numerator;
        den = denominator;
    }

    public static Fraction operator +(Fraction a) => a;
    public static Fraction operator -(Fraction a) => new Fraction(-a.num, a.den);

    public static Fraction operator +(Fraction a, Fraction b)
        => new Fraction(a.num * b.den + b.num * a.den, a.den * b.den);

    public static Fraction operator -(Fraction a, Fraction b)
        => a + (-b);

    public static Fraction operator *(Fraction a, Fraction b)
        => new Fraction(a.num * b.num, a.den * b.den);

    public static Fraction operator /(Fraction a, Fraction b)
    {
        if (b.num == 0)
        {
            throw new DivideByZeroException();
        }
        return new Fraction(a.num * b.den, a.den * b.num);
    }

    public override string ToString() => $"{num} / {den}";
}

public static class OperatorOverloading
{
    public static void Main()
    {
        var a = new Fraction(5, 4);
        var b = new Fraction(1, 2);
        Console.WriteLine(-a);   // output: -5 / 4
        Console.WriteLine(a + b);  // output: 14 / 8
        Console.WriteLine(a - b);  // output: 6 / 8
        Console.WriteLine(a * b);  // output: 5 / 8
        Console.WriteLine(a / b);  // output: 10 / 4
    }
}