2

I am trying to make a cooking recipe calculator, which basically grabs all numbers and divides them (so far). On top of dividing all numbers, it will also turn decimals into fractions. This is important. Now, when I run my loop to look for numbers to replace, I have a problem when numbers on lines are greater than 1. If I enter 1 egg and 2 cups of milk and tell it to divide by 2, it will do this:

first iteration:
find: 1 and replace 1 with 1/2
result: 1/2 egg and 2 cups of milk

second iteration:
find: 2 and replace 2 with 1
result: 1/1 egg and 2 cups of milk

If you understood that correctly, I would now have 1/1 2. Why? Because on the second iteration, it will find 2 and replace it with 1. I basically need to say:

if(number != fraction)
    replace number;

How am I able to do that from my code?

JSFiddle: http://jsfiddle.net/fgzt82yk/8/
My loop:

for(i = 0; i < content.length; i++) {
    //Finds numbers on the line
    var number = content[i].match(regex);

    //number == null if the line is empty
    if(number != null) {

        //There can be more than 1 number on each line, create a new loop
        for(j = 0; j < number.length; j++) {

            //Turns fractions into decimals
            var evalNumber = eval(number[j]);

            //Divides the number
            var divided = parseFloat(evalNumber / selection);

            //We need some kind of precision, so we won't get 0.1666667 and so on
            var precision = selection;
            if(precision == 2) {
                precision = 0;
            }

            //Turns our decimal into a fraction (or a whole-number)
            var newNum = Math.fraction(divided.toString(), precision);

            //Replaces the original number from the content[i] (whole line) with the new number/fraction
            content[i] = content[i].replace(number[j], newNum);
        }
    }
}

Added comments to make it more clear what each line means. It's the last line that bugs me.. I'm sure.

I am basically looking for a better way to apply the math to all numbers (decimals, fractions, mixed numbers, et cetera). So far it kinda works, but I am sure someone has a better way of doing it! Thanks for looking!

MortenMoulder
  • 6,138
  • 11
  • 60
  • 116
  • You're also going to presumably be in trouble if the recipe calls for "1/2 cup", as you'll end up with "1/2/1 cup". Also, there is a character for the half symbol, you may need to account for that too. – Peter Oct 28 '15 at 21:12
  • 1
    @PeterBailey Already accounted for that with my regex, which matches fractions and numbers! :) Check: http://i.imgur.com/cBG3o5p.png – MortenMoulder Oct 28 '15 at 21:13
  • The half character ½ is "Unicode Character 'VULGAR FRACTION ONE HALF' (U+00BD)" for reference - . http://www.fileformat.info/info/unicode/char/00bd/index.htm . You've also got ¼ ¾ and so on as well :) – Peter Oct 28 '15 at 21:18
  • @PeterBailey Won't work with my fraction function unfortunately. And trust me, it's a pain in the rear to convert decimals into fractions. – MortenMoulder Oct 28 '15 at 21:19

2 Answers2

7

For your use case, I think the solution is not to find out how to not-divide numbers already divided, but to replace all the numbers at once.

For example, if you were to have a recipe that called for:

  • 12 eggs
  • 6 cups of water
  • ...

and you wanted to halve things, you could end up replacing 12 with 6 and then later replacing 6 again on that pass. (Yes, you could probably do it largest to smallest number, but then you need a pass to order all the numbers as well. Unnecessary complexity).

If you're going to use regexes anyway, I suggest you do something like

function fractionize(n, d) {
  var gcd = (function gcd(a, b) {
      if ( ! b) {
          return a;
      }
      return gcd(b, a % b);
  })(n ,d);
  return d == gcd ? n / d : (n > d ? Math.floor(n / d) + ' ': '') + (n % d / gcd) + '/' + (d / gcd);
}

function decimalize(decimal, precision) {
  var power = Math.pow(10, precision);
  return Math.round(decimal * power) / power;
}

function scaleRecipe(recipe, multiply, divide) {
  return recipe.replace(/((\d*\.\d+)|((\d+)\s+)?(\d+)(\s*\/\s*(\d+))?)/g, function(_, _, decimal, _, wn, n, _, d) {
    d = d || 1;
    return decimal ? decimalize(decimal * multiply / divide, decimal.match(/\d/g).length) : fractionize(((wn || 0) * d + parseInt(n)) * multiply, divide * d);
  });
}

For example:

scaleRecipe('12 eggs, 6 cups of water, 13 chickens, 4 lb rice, 17 tsp vanilla, 60g sugar, 3/4 cups chocolate chips, 2/3 lb beef, 1/5 oz secret sauce, 3 1 / 2 teaspoons salt', 1, 6)

would give you

"2 eggs, 1 cups of water, 2 1/6 chickens, 2/3 lb rice, 2 5/6 tsp vanilla, 10g sugar, 1/8 cups chocolate chips, 1/9 lb beef, 1/30 oz secret sauce, 7/12 teaspoons salt, 0.1 lb ham, 0.027 oz honey, 0.35 pint blueberries"

and that same recipe /8 would give you

"1 1/2 eggs, 3/4 cups of water, 1 5/8 chickens, 1/2 lb rice, 2 1/8 tsp vanilla, 7 1/2g sugar, 3/32 cups chocolate chips, 1/12 lb beef, 1/40 oz secret sauce, 7/16 teaspoons salt, 0.1 lb ham, 0.02 oz honey, 0.26 pint blueberries"

You could also have additional logic to reduce fractions (see: JS how to find the greatest common divisor) and to format as a mixed number (take integer and remainder parts separately).

Plurals might be another aspect you'd want to consider, but English is a tricky language with many edge cases. :)

Community
  • 1
  • 1
arcyqwerty
  • 10,325
  • 4
  • 47
  • 84
  • Hmm yeah I get what you mean, but then I will get "incorrect" fractions back. Example, if I divide your `13 chickens` with 8, I will get `13/8 chickens`. I need that to be `1 5/8` instead – MortenMoulder Oct 28 '15 at 21:24
  • I basically need any number you enter into my script back the same way. I don't see how that is possible, but that surely is a neat little function. – MortenMoulder Oct 28 '15 at 21:25
  • Yes, see what I said about mixed numbers. Essentially, you see any fraction > 1 and split out the integer part. – arcyqwerty Oct 28 '15 at 21:26
  • 1
    Updated for mixed numbers. Also, "incorrect" fractions are technically called "improper" fractions FYI :) – arcyqwerty Oct 28 '15 at 21:28
  • And if you like `/8`ths, `"1 4/8 eggs, 6/8 cups of water, 1 5/8 chickens, 4/8 lb rice, 2 1/8 tsp vanilla, 7 4/8g sugar"` as you requested – arcyqwerty Oct 28 '15 at 21:31
  • @arcyqwerty Okay, which actually works better than expected. ON NUMBERS. What do I do when it comes to fractions now? How do I apply `1/4` into your function? Mind you, some of these recipes come as `1 1/2 pounds of beef` et cetera. +1 for improper vs incorrect. – MortenMoulder Oct 28 '15 at 21:34
  • Your top heavy fractions could be tidied like so: https://jsfiddle.net/wnwkg0c6/ - edit nevermind, @arcyqwerty just added the mod'ing. – Peter Oct 28 '15 at 21:35
  • Simplified fractions, for completeness :) (credit to http://stackoverflow.com/q/17445231/1059070 as mentioned in answer) – arcyqwerty Oct 28 '15 at 21:38
  • @arcyqwerty How would you divide fractions then? http://jsfiddle.net/cp3chru2/ - `1/4` divided by 2 is `1/8` for instance. – MortenMoulder Oct 28 '15 at 21:40
  • Hmm, will update for that functionality. Likely parse and shove into the fractionize as well (now that that's there). – arcyqwerty Oct 28 '15 at 21:42
  • @arcyqwerty You're a great human being :) Making this fit all cases (people entering decimals, fractions, and whole numbers plays a role here) is the toughest part. – MortenMoulder Oct 28 '15 at 21:44
  • @Snorlax, thanks! I think this is an interesting question so I'm more than happy to give a good solution. Updated for fractional inputs. (will go back and support whitespace too, actually, because English is hard). – arcyqwerty Oct 28 '15 at 21:50
  • @arcyqwerty Exactly. `1 1/2` will turn into `1/42/2`. I think the regex is the kicker here, but I'm not sure to be honest. This can most likely be used in future projects for other people as well. An easy "apply math to all numbers, fractions, decimals, etc.". – MortenMoulder Oct 28 '15 at 21:53
  • @Snorlax: Ok, got a regex going. Look ok? – arcyqwerty Oct 28 '15 at 22:00
  • @arcyqwerty WORKS WONDERS! http://jsfiddle.net/fgzt82yk/11/ - It works just as intended. Even works with `5 5/5 = 6`. Last and only that that doesn't work... decimals. `0.5` divided by 2 is `0.2 1/2` :( – MortenMoulder Oct 28 '15 at 22:06
  • Hmm, so not sure if I would be able to make decimals into fractions... especially since doubles and precision and such. I could probably leave decimals as decimals though. – arcyqwerty Oct 28 '15 at 22:12
  • @arcyqwerty In my example there's a function that converts decimals into fractions. It's a lot more code, but it has worked for me as well. From your perspective, if you were to use this site, what do you think is best? – MortenMoulder Oct 28 '15 at 22:14
  • Added decimal support (with rounding!). I think that most fractions you'd see in a recipe could be converted reasonably to a fraction (You probably won't get anything more complex than say 1/16) – arcyqwerty Oct 28 '15 at 22:23
  • @arcyqwerty http://jsfiddle.net/fgzt82yk/13/ Doesn't get much better than that! Decimals are a pain in the rear to work with, so adding support, including rounding, is pretty damn neat. If you don't mind me asking... When you do your `function(_, _, decimal, _, wn, n, _, d) {`, what does the underscores do? – MortenMoulder Oct 28 '15 at 22:29
  • And sig figs (I think? Someone remind me how sig figs work...). This recipe is starting to look like it wouldn't taste too good :/ – arcyqwerty Oct 28 '15 at 22:29
  • @Snorlax: The `_` just consume an argument. Since the `()` grouping operators in the regex are used for `()?` optional selection, sometimes I don't need the items returned by the outer group. The `_` convention is used to denote an unused variable in some languages. I don't know if there is a standard for JS. – arcyqwerty Oct 28 '15 at 22:30
  • @arcyqwerty Haha yeah what a great recipe :-D Sounds good though. I've always wondered why some add that into some functions, but I guess I know the answer now. Google wasn't much help though... if I just knew the name of it haha. Thank you so much for the help! To make this more SEO friendly, I should probably change the title. Don't you think? – MortenMoulder Oct 28 '15 at 22:33
  • Sure, go ahead and edit your question/title to reflect the general scope of what you're asking. It could get more SEO points but also help other SO users find the question if they have similar needs. `_ variable` gets a couple results but yeah, unless you already kind of knew what you're looking for, you get some other answers first. – arcyqwerty Oct 28 '15 at 22:38
  • @arcyqwerty I see. I will go ahead change it right away! – MortenMoulder Oct 28 '15 at 22:39
  • @Snorlax: I might say "Scale all numbers, fractions, and mixed numbers in a string" or something like that since it indicates different numerical types (as opposed to straight computer-parsable decimal types) and that you're only allowed to divide. -- I was going to say downscale for division only, but it seems like this actually works for scaling UP values as well... who knew! – arcyqwerty Oct 28 '15 at 23:02
  • @arcyqwerty Haha, I got a new request. This should be farely easy. Is it possible to parse the scaleRecipe function a new parameter, which tells if it should divide or multiply? I'd hate to tear up that function by adding `if(param == "divide") { return .... decimal / scale ... } else { return .... decimal * scale }` if you know what I mean. I know you can divide by < 1 to get a positive number, but if I want to multiply by 3 by using division... It's going to be a struggle with `decimal / 0.3333` etc. – MortenMoulder Oct 28 '15 at 23:04
  • @Snorlax: From your end, I think I'd keep that logic in the calling code (or wrapper method: `function(scale, multiply) { return scaleRecipe(recipe, multiply ? 1 / ratio : ratio) }`). If you do want to modify `scaleRecipe` itself, I'd just add `scale = multiply ? 1 / scale : scale;` as the first line. I wouldn't worry about changing the function too much. I haven't even tidied up the code and it probably breaks a ton of linting standards :) – arcyqwerty Oct 28 '15 at 23:12
  • @arcyqwerty Works.. kinda, I assume. But same problem when I get to multiplying with 3: http://jsfiddle.net/fgzt82yk/14/ – MortenMoulder Oct 28 '15 at 23:19
  • 1
    Ah, yeah, I think that's probably due to double precision and gcd not liking fractional inputs. Updated code so you can just pass a `multiply` and `divide` argument (i.e. Triple is `multiply = 3` and `divide = 1`; half is `multiply = 1` and `divide = 2`; 3/8 is `multiply = 3` and `divide = 8`, etc. with numerator and denominator of the ratio as `multiply` and `divide` respectively) – arcyqwerty Oct 29 '15 at 00:55
  • @arcyqwerty Works wonders! Now it can do whatever I want :) `content[i] = multiply ? scaleRecipe(content[i], selection, 1) : scaleRecipe(content[i], 1, selection);` a ternary operator is all I had to add. Thank you so much once again! – MortenMoulder Oct 29 '15 at 01:01
  • @arcyqwerty Hey it's me again! Would it be possible to make it multiply by decimal numbers? Here is my latest code: http://jsfiddle.net/fgzt82yk/20/ - As you can see, it grabs the amount to multiply with in the data-value attribute, if the div has the data-multiply attribute set to 1. Right now if I try to multiply 1 by 2.1, I will get `2 225179981368525/2251799813685248`, which definitely is wrong. – MortenMoulder Oct 30 '15 at 12:06
  • *Easiest* way is probably add a `forceDecimal` argument to `scaleRecipe` (i.e. after `multiply` and `divide`) then modify `decimal ? decimalize(...) : ...` to `decimal || forceDecimal`. Note it has the benefit of being backward compatible as, if the argument is not specified, it will default to `undefined` which evaluates to `false` in the boolean expression. – arcyqwerty Oct 30 '15 at 15:23
  • Alternatively, detect that if `multiply` or `divide` is not an integer, decimalize. – arcyqwerty Oct 30 '15 at 15:24
0

It looks like a job for String.prototype.replace() - exactly the one you're using already.

Just pass a regular expression as a first and transforming function as a second parameter, so you'll change all the numbers at once - you won't need to worry about numbers being affected twice

Hipolith
  • 451
  • 3
  • 14
  • Hmm I don't think I follow. Care to elaborate with an example? – MortenMoulder Oct 28 '15 at 21:26
  • 1
    It looks like @arcyqwerty has already turned my thoughts into the code - that's what I've had in mind - grab all the numbers at once and transform them with a neat function – Hipolith Oct 28 '15 at 21:35