abrobbins90 / groceries

Menu Organizer
1 stars 0 forks source link

Quantities recognize units when adding together #9

Open MareoRaft opened 4 years ago

MareoRaft commented 4 years ago

If two recipes have the same ingredient "pasta" and their quantities are "2 lbs" and "2 lbs" respectively, then when both recipes are selected, the total quantity will appear as "4 lbs" as opposed to "2".

MareoRaft commented 4 years ago

branch

matt/issues/9/smartQuantities
MareoRaft commented 4 years ago

Since I am changing the data structure of "info" to move quantities into a "quantityDict" sub-key, then I must write mongo script to alter all the data in the DB to conform to the new data model.

-->

It looks like the updateMany operation is the right tool for updating many dictionaries in the DB.

-->

On my mongo version, there is only update.

abrobbins90 commented 4 years ago

Hold on there. I'm making some pretty significant changes on that front too. We should talk about that data structure and coordinate. I'm still just playing around, so no firm ideas or anything, but I think the changes made to info should be even more drastic!

MareoRaft commented 4 years ago

Okay we can definitely talk. The changes I'm making right now are on my local laptop DB, not on learnnation.org.

MareoRaft commented 4 years ago

VERSION 1:

Quantity Data Model:

{
    userInputtedStrings: ["2 heads of broccoli", "1 broccoli head", ...],
}

For example, the user may have typed "2 heads of broccoli". For example, "1/3 cup sugar".

For a single thing that a user has typed, the array will be a singleton. But when we combine together ingredients, the array will get longer.

Atomic Quantity Helper Functions:

These functions run a single user-inputted string, and are used by the quantity class.

getUserInputtedAmountString(user_inputted_string) Uses a regex to find and return the amount that the user typed. For example, "2" or even "1 /3" (note the space).

getAmount(user_inputted_string) Uses getUserInputtedAmountString and then processes it into a JavaScript decimal.

getUserInputtedUnit(user_inputted_string) Uses a regex to find and return the unit that the user typed. For example, "heads". For example, "head". For example, "cup".

convertUnitToPlural(unit) Given a unit, convert it to its plural form. At first, this will be merely adding an "s" if there is none. In the future, it can be smarter.

convertUnitToSingular(unit) Given a unit, convert it to its singular form. At first, this will be merely removing an "s" if there is one. In the future, it can be smarter.

getUnitString(user_inputted_string) Uses getUserInputtedUnit and then converts it to a "standard" plural form using convertUnitToPlural.

getUserInputtedIngredientNameString(user_inputted_string) For example, "broccoli"

getIngredientName(user_inputted_string) For example, "carrot". Uses getUserInputtedIngredientNameString and converts it to a "standard" singular form.

Quantity Class:

Contains the data model dictionary and also the following methods. Note that these methods must deal with COMBINING data from multiple user-inputted strings.

getAmount() returns a decimal. For example, 2. For example, 0.33. Adds up all quantities from atomic getAmount function.

getAmountString() returns a string. For example, "2". For example, "1/3". Take the output of this.getAmount() and stringifies it. PROBLEM: How can we respect exact fractions when adding quantities? SOLUTION: We could add an extra property that notes the format in which they types the number, and tries to simulate it. Maybe there exists a 3rd party library. This sounds like an after-the-fact addition.

getUnit() returns a string. For example, "heads". We use the atomic getUnit function on each string. If they don't all agree, we output an error. If the amount from getAmount is 1, we output the singular version of the unit; otherwise, the plural.

getIngredientName() Just like getUnit, but for the ingredient name instead.

getDisplayString() optional addition: If this.userInputtedStrings is a singleton, output the original user-inputted string (for the sake of not switching things up on the user when we don't have to). In the case of one unit, output "{getAmountString()} {getUnit()} {getIngredientName()}".

MareoRaft commented 4 years ago

VERSION 2:

Quantity Data Model:

{
    userInputtedStrings: ["2 heads of broccoli", "1 broccoli head", ...],
}

For example, the user may have typed "2 heads of broccoli". For example, "1/3 cup sugar".

For a single thing that a user has typed, the array will be a singleton. But when we combine together ingredients, the array will get longer.

Atomic Quantity Helper Functions:

These functions run a single user-inputted string, and are used by the quantity class.

getUserInputtedAmountString(user_inputted_string) Uses a regex to find and return the amount that the user typed. For example, "2" or even "1 /3" (note the space).

getAmount(user_inputted_string) Uses getUserInputtedAmountString and then processes it into a JavaScript decimal.

getUserInputtedUnit(user_inputted_string) Uses a regex to find and return the unit that the user typed. For example, "heads". For example, "head". For example, "cup".

convertUnitToPlural(unit) Given a unit, convert it to its plural form. At first, this will be merely adding an "s" if there is none. In the future, it can be smarter.

convertUnitToSingular(unit) Given a unit, convert it to its singular form. At first, this will be merely removing an "s" if there is one. In the future, it can be smarter.

getUnit(user_inputted_string) Uses getUserInputtedUnit and then converts it to a "standard" plural form using convertUnitToPlural.

getUserInputtedIngredientNameString(user_inputted_string) For example, "broccoli"

getIngredientName(user_inputted_string) For example, "carrot". Uses getUserInputtedIngredientNameString and converts it to a "standard" singular form.

getAmountString(unit, amount) Stringifies the amount.

getUnitString(unit, amount) Correctly formats the unit (including pluralization). Helper func. If the amount is 1, we output the singular version of the unit; otherwise, the plural.

getUnitAmountString(unit, amount) Helper function. Given a unit and an amount, output the correct pluralized formatted string. For example, "2 heads". For example, "1/3 cups". Outputs "{getAmountString(unit, amount)} {getUnitString(unit, amount)}".

Quantity Class:

Contains the data model dictionary and also the following methods. Note that these methods must deal with COMBINING data from multiple user-inputted strings.

getUnitToAmountDict() Returns an unit -> amount dictionary. For each unique unit, we have a key of getUnit() and a value which is the sum of all the amounts (getAmount) corresponding to that unit. For example, {"heads": 3}. For example, {"lbs": 1, "tbsp": 1}. The dictionary will be a singleton like 96% of the time, but can handle multiple units.

getUnitAmountString() Takes the this.getUnitToAmountDict() dictionary and then outputs the concatenation of getUnitAmountString(unit, amount) for each key-value pair.

getIngredientName() Just like getUnit, but for the ingredient name instead.

getDisplayString() optional addition: If this.userInputtedStrings is a singleton, output the original user-inputted string (for the sake of not switching things up on the user when we don't have to). In the case of one unit, output "{this.getUnitAmountString()} {this.getIngredientName()}".

MareoRaft commented 4 years ago

@abrobbins90 Here is the design proposal. Can you take a look? You only need to read VERSION 2.

abrobbins90 commented 4 years ago

Ok, there are so many methods in the Atomic Quantity, it took me a moment to get them straight, It seems there is a method to do 2 orthogonal things: 1) Return amount, quantity, or ingredient 2) Return user input, get interpreted value, and get string of interpreted value (with except of things that are already strings) Then are various methods for handling unit pluralization And finally there is one method to return amount of unit together

Overall, this looks good. I think it would be helpful to see how they are organized within a file, and how they're used, but I can see their utility.

One comment though. Are you thinking of only storing the user inputted string? I would vote we store the parsed version (in addition to whatever the user inputted). For one, I think we can have a way to edit the amount and unit tied to an ingredient explicitly, so this should be stored and interacted with in a straightforward way. (This could be only if the user wants to. Normal use can be free form. With that said, it seems it would make sense to have separate groups of methods, and I'm not sure if these would be different classes. One set for parsing, and another set for dealing with parsed data. Practically, I think it'll be important to have a data set that relies on having the amount/unit specified (i.e. they are properties that are assumed to be there). Thus it may make sense to separate the classes.

For the quantity class, am I interpreting this correctly that it's always tied to a particular ingredient? How would this work exactly in the grand scheme of things. How does this quantity class interface with the rest of the framework such that you'd have 2 (or more) different unit/amount pairs?

MareoRaft commented 4 years ago

Are you thinking of only storing the user inputted string?

Yes. But the parsed version can be generated at any time, on demand. You have a good point though. What if they want to edit the amount? Well if I wanted to keep the current architecture of only storing the original string, then when they click on the quantity to edit it, it would display their original string, which they could edit, resulting in a new user-inputted-string, and we could recalculate everything.

It is important that we have a single source of truth. If for some reason we decided to use the processed amount/unit/ingr as the source of truth, then we should throw away the original user string once the processed stuff is calculated.

(random thought) What if they want to edit a quantity that is actually a combined quantity of two separate values? In that case, I would say that they are not allowed. They have to edit one of the source quantities. Imagine if their recipe calls for "1 apple" 4 times, but then they edited the combined quantity to read "3 apples" instead of "4 apples"? Worlds would collide.


Well so the interesting case is when you have something like "2 apples". That case affects how we address both Practically, I think it'll be important to have a data set that relies on having the amount/unit specified and it's always tied to a particular ingredient.

If the user inputs "1 apple" at the beginning of the recipe and "1 apple" at the end, if I want to generate the output "2 apples", then I have to involve the ingredient name because I have to pluralize it. Also, in this case, the unit name is "". Alternatively, the unit name could be "whole".

Thought:

whoah so this sortof blew up at the end

abrobbins90 commented 4 years ago

Single Source of Truth

Absolutely also advocate for this. Though I don't think we need to banish the original user string necessarily. But I think the single source of truth should be stored in terms of what matters. And what matters for the program is the ingredient and quantity. That is what the rest of the program relies on. I think we need to interpret the original input and commit. Practically, I think storing the original string could be useful, particularly in development. Though I'm not wedded to this. I don't think we really need it. I don't see any reason to keep things in the form of the string. Do you? In fact, if we ever need to show the user this again, I think showing in the interpreted form will be infinitely more valuable to the user. Imagine the default view being your original text, but it's being interpreted incorrectly. Good luck figuring that out or how to fix it!

Dealing with multiple amount/unit pairs

As for your random thought, In line with my thoughts on how the recipe data architecture should work, I think a quantity should only ever have a single amount/unit pair. I think a rule to impose that would be very helpful would be that a single ingredient could only be tied to one part of the recipe. If the user is just entering everything in a big pile, they get one set of apples. If they want to have two sets, one needs to be in a different section or task. While this adds some restriction, I think it is necessary to have boundaries so that the code can work more smoothly. I think this small imposition in particular will go a long way.

Now it also matters what purpose we're talking about. For just recording the ingredients and their quantities in the recipe, I think this is no great hardship. When it comes to trying to add other features that say recognize when and how much an ingredient is needed (within a recipe), I think we can still use the language processing to identify that apples are needed in 2 places, even if it's stored as a single quantity and ingredient. Might get tricky, but again, the functionality to separate them will be there (and honestly super easy to use), so I don't think we should be any more flexible.

I think we can deal with things that don't have a volume/weight unit. They're typically referred to as each or something. But whole, or just listing nothing is fine. Can store as "each/whole" and know to display nothing. Same with the plurals. Can build all that up, but as for what is being stored, I think the amount/unit is straightforward, consistent, and easy. Actually displaying, combining, etc. is entirely separate.

Thoughts

For your PB&J, if the user inputs 2 separate ingredients, I don't want to mess with that. This is standard for recipes. There may be something listed, but you might make substitutions, or even combinations, but that is up to the user and is often done on the fly or up to discretion at the time.

For the embedded meal, this is something we discussed. I have this occurrence in my recipes already. And I've thought about this issue, especially as I was working on the node development. I haven't fleshed it out yet, but my running idea is to have a special ingredient node that fronts for a meal node. It can maybe be a subclass of the ingredient node. There will be some special handling required when it comes to say, generating a grocery list, but that should be easy enough. I like the idea of having a front because then the meal nodes themselves can remain as they are, and the relationship between meal and ingredient nodes can remain as well. Now because there will be many different situations where this can come up, the ingredient tag node could be utilized. Sometimes, this ingredient/meal hybrid will be a sauce, or sometimes a side. Thus one might enter into the recipe, while the other might be totally independent. Having some predefined tags could be helpful in using this automatically in various features. But anyway, when the user is entering ingredients however they do it, there can be the option to mark an ingredient as being linked to an existing recipe. An auto-search can present this option when a match is found as they are entering it, and all of this could be modified in the "node editing" view I have envisioned. Anyway, so in summary... I think the solution is a special ingredient that serves as the official ambassador of a meal. All the same classes and relationships that have always existed can remain, unchanged.

MareoRaft commented 4 years ago

Single Source of Truth

I'm okay with using the amount/unit as the source of truth.

Dealing with multiple amount/unit pairs

A quantity should allow more than one unit/amount pair because a quantity can be the combination of many uses of the same ingredient.

For example, consider that Jack has an Apple Pie recipe that uses 5 large apples, and Jack also has an Apple Tart (yum) recipe that uses 2 pounds of apples. When the grocery list is made, it will says that Jack needs to buy "5 large and 2 pounds of apples".

So there are 3 Quantity objects in the above situation:

(1)

Quantity(5 large apples) (for the Apple Pie)

(2)

Quantity(2 pounds of apples) (for the Apple Tart)

(3)

Quantity(5 large and 2 pounds of apples) (for the entire grocery list)

I have designed the Quantity objects to be "combinable". Perhaps this is not what you originally envisioned, but I think this will work very well. And it will be totally up to us when we want to actually combine them. We can still keep things separate whenever we want to.

MareoRaft commented 4 years ago

VERSION 3:

Quantity Data Model:

{
    unitToAmount: {
        "head": 2,
    },
}

For example, the user may have typed "2 heads of broccoli". For example, "1/3 cup sugar".

For a single thing that a user has typed, the dictionary will be a singleton. But when we combine together ingredients, the dictionary may have more keys.

conversion helper functions

convertUnitToPlural(unit) Given a unit, convert it to its plural form. At first, this will be merely adding an "s" if there is none. In the future, it can be smarter. if (_.endsWith(unit, 's')) { return unit } else { return {unit}s }

convertUnitToSingular(unit) Given a unit, convert it to its singular form. At first, this will be merely removing an "s" if there is one. In the future, it can be smarter. if (_.endsWith(unit, 's')) { return unit.substr(0, unit.length - 1) } else { return unit }

Atomic Quantity Helper Functions:

These functions take in a single user-inputted string, and are used by the Quantity class.

getUserInputtedAmountString(user_inputted_string) Uses a regex to find and return the amount that the user typed. For example, "2" or even "1 /3" (note the space). // TODO user_inputted_amount = regex.find(/^[\s-\d\/]*/) return user_inputted_amount

getAmount(user_inputted_string) Uses getUserInputtedAmountString and then processes it into a JavaScript decimal. For example, 2 or 0.33. user_inputted_amount = getUserInputtedAmountString(user_inputted_string) trimmed_amount = user_inputted_amount.trim() amount = parseFloat(trimmed_amount) return amount

getUserInputtedUnit(user_inputted_string) Uses a regex to find and return the unit that the user typed. For example, "heads". For example, "head". For example, "cup". user_inputted_amount = getUserInputtedAmountString(user_inputted_string) user_inputted_unit_and_more = user_inputted_string.substr(user_inputted_amount.length) // TODO user_inputted_unit = regex.find(user_inputted_unit_and_more, /^\w*/) return user_inputted_unit

getUnit(user_inputted_string) Uses getUserInputtedUnit and then converts it to a "standard" plural form using convertUnitToPlural. user_inputted_unit = getUserInputtedUnit(user_inputted_string) standardized_unit = convertUnitToPlural(user_inputted_unit) return standardized_unit

stringification helper functions

getAmountString(unit, amount) Given a unit and an amount, outputs a stringified amount. return {amount}

getUnitString(unit, amount) Correctly formats the unit (including pluralization). if (unit === '' || unit === 'whole') { return '' } else if (unit === 'small' || unit === 'large') { return unit } else if (amount === 1) { return convertUnitToSingular(unit) } else { return convertUnitToPlural(unit) }

getUnitAmountString(unit, amount) Helper function. Given a unit and an amount, output the correct pluralized formatted string. For example, "2 heads". For example, "1/3 cups". return {getAmountString(unit, amount)} {getUnitString(unit, amount)}

Quantity Class:

Contains the data model dictionary and also the following methods. Note that these methods must deal with COMBINING data from multiple quantities.

constructFromUserInputtedString(user_inputted_string) Instantiates a Quantity object. Calls the "userInputted" functions above in order to get the unit and amount. Then constructs the {unitToAmount:{}} thingy. unit = getUnit(user_inputted_string) amount = getAmount(user_inputted_string) this.unitToAmount = {[unit]: amount}

constructFromDictionary(dictionary) Instantiates a Quantity object. Given an already-good {unitToAmount:{}} dictionary (from the DB), populate the object. this.unitToAmount = dictionary.unitToAmount

constructFromObjects(quantity_objects) Instantiates a Quantity object. Given an iterable of Quantity objects, create a new Quantity object which combines them. unit_to_amountdictionaries = .map(quantity_objects, 'unitToAmount') this.unitToAmount = mergeDictionariesAddingValues(unit_to_amount_dictionaries)

getCombinedUnitToAmountDict() Returns an unit -> amount dictionary. For each unique unit, we have a key of getUnit() and a value which is the sum of all the amounts (getAmount) corresponding to that unit. For example, {"heads": 3}. For example, {"lbs": 1, "tbsp": 1}. The dictionary will typically be a singleton, but is capable of handling multiple units. return this.unitToAmount

getCombinedUnitAmountString() Takes the this.getUnitToAmountDict() dictionary and then outputs the concatenation of getUnitAmountString(unit, amount) for each key-value pair.

MareoRaft commented 4 years ago

@abrobbins90 I'm pretty much ready to put this in the code. I removed the "ingredientName" stuff because quantities can be used for meal nodes too (not just ingredient nodes), and because the names of those things are already stored on the nodes themselves (i don't want redundant info).

MareoRaft commented 4 years ago

so we can have a separate utility for singularizing/pluralizing the names themselves