diff --git a/packages/parser/antlr4/lib/src/main/antlr/FoodDescription.g4 b/packages/parser/antlr4/lib/src/main/antlr/FoodDescription.g4 index c14543e8..83b9faa5 100644 --- a/packages/parser/antlr4/lib/src/main/antlr/FoodDescription.g4 +++ b/packages/parser/antlr4/lib/src/main/antlr/FoodDescription.g4 @@ -16,7 +16,7 @@ quantity ; unit - : WORD+ | WORD* LEFT_PAREN (WHOLE_NUMBER | FRACTION | WORD | SLASH | PERIOD)+ RIGHT_PAREN + : WORD+ | WORD* LEFT_PAREN (WHOLE_NUMBER | FRACTION | WORD | SLASH | PERIOD)* RIGHT_PAREN? ; WORD diff --git a/src/features/suggestions/generate/autoSuggestion.test.ts b/src/features/suggestions/generate/autoSuggestion.test.ts index ab2d93a6..8ee8088b 100644 --- a/src/features/suggestions/generate/autoSuggestion.test.ts +++ b/src/features/suggestions/generate/autoSuggestion.test.ts @@ -259,3 +259,59 @@ test("unknown unit from input, choose the measurement whose unit is also unknown } }); }) + +test("unit with parenthesis should generate serving suggestion", () => { + const autoCompletion = { + foodName: "Doughnut", + amount: "1 (3 1/4 inch dia.)" + } + const suggestions = [ + { + foodName: "Doughnut, cake (plain)", + amount: "1 (3 1/4 inch dia.)", + serving: { + carbohydrate: 0.5, + fat: 3, + sweet: 0.5, + } + } + ] + const result = generateAutoSuggestion(autoCompletion, suggestions); + expect(result).toEqual({ + foodName: "Doughnut", + amount: "1 (3 1/4 inch dia.)", + serving: { + carbohydrate: 0.5, + fat: 3, + sweet: 0.5, + } + }); +}) + +test("unit with open parenthesis should not generate serving suggestion", () => { + const autoCompletion = { + foodName: "Doughnut", + amount: "1 (" + } + const suggestions = [ + { + foodName: "Doughnut, cake (plain)", + amount: "1 (3 1/4 inch dia.)", + serving: { + carbohydrate: 0.5, + fat: 3, + sweet: 0.5, + } + } + ] + const result = generateAutoSuggestion(autoCompletion, suggestions); + expect(result).toEqual({ + foodName: "Doughnut", + amount: "1 (3 1/4 inch dia.)", + serving: { + carbohydrate: 0.5, + fat: 3, + sweet: 0.5, + } + }); +}) diff --git a/src/features/suggestions/generate/autoSuggestion.ts b/src/features/suggestions/generate/autoSuggestion.ts index c6d85082..632077d7 100644 --- a/src/features/suggestions/generate/autoSuggestion.ts +++ b/src/features/suggestions/generate/autoSuggestion.ts @@ -2,6 +2,7 @@ import _ from 'lodash'; import { Suggestion } from "../Suggestion"; import baseOn from './calculateServing'; import { PredefinedSuggestion } from "../search/PredefinedSuggestion"; +import { isAmountParsable } from './isAmountParsable'; export function generateAutoSuggestion(autoCompletion: Suggestion, suggestions: PredefinedSuggestion[]) { if (_.size(suggestions) === 0) return null; @@ -16,7 +17,7 @@ function createAutoSuggestion(nameSuggestion: Suggestion, suggestion: Predefined ...suggestion, foodName }; - if (amount) { + if (amount && isAmountParsable(amount)) { const serving = baseOn(suggestion).servingFor(amount); return { ...autoSuggestion, diff --git a/src/features/suggestions/generate/generateSuggestion.ts b/src/features/suggestions/generate/generateSuggestion.ts index e946a98d..d7db2d93 100644 --- a/src/features/suggestions/generate/generateSuggestion.ts +++ b/src/features/suggestions/generate/generateSuggestion.ts @@ -5,16 +5,18 @@ import findAutoCompletions from '../search/autoCompletion'; import { findSuggestions } from '../search/foodNameSearch'; import { Suggestion } from '../Suggestion'; import { generateAutoSuggestion } from './autoSuggestion'; +import { isAmountParsable } from './isAmountParsable'; function unitOfMeasurement(amount?: string) { - if (_.isUndefined(amount)) return undefined; - - const { measurement } = parseAmount(amount); - return measurement.unit; + if (amount && isAmountParsable(amount)) { + const { measurement } = parseAmount(amount); + return measurement.unit; + } + return undefined; } export function generateSuggestions( - foodDescriptionRef: React.MutableRefObject, + foodDescriptionRef: React.RefObject, callback: (suggestions: Suggestion[]) => void ) { const foodDescription = decompose(foodDescriptionRef.current + ""); @@ -22,7 +24,8 @@ export function generateSuggestions( const firstAutoCompletion = autoCompletions[0]; const servingSuggestions = findSuggestions(foodDescription.foodName, { convertibleFrom: unitOfMeasurement(firstAutoCompletion.amount) }); - const allSuggestions = _.concat(autoCompletions, generateAutoSuggestion(firstAutoCompletion, servingSuggestions), servingSuggestions); + const autoSuggestions = generateAutoSuggestion(firstAutoCompletion, servingSuggestions); + const allSuggestions = _.concat(autoCompletions, autoSuggestions, servingSuggestions); const results = _.uniqWith(_.compact(allSuggestions), _.isEqual) .slice(0, 5); return callback(results); diff --git a/src/features/suggestions/generate/isAmountParsable.ts b/src/features/suggestions/generate/isAmountParsable.ts new file mode 100644 index 00000000..8f64ef7d --- /dev/null +++ b/src/features/suggestions/generate/isAmountParsable.ts @@ -0,0 +1,14 @@ +import _ from 'lodash'; + +/** + * Amount string is parsable if it has matching parentheses. Default to true if there is no parentheses. + * + * @param amount Amount string to check if it is parsable + * @returns true if amount is parsable, false otherwise + */ +export function isAmountParsable(amount: string) { + if (_.endsWith(amount, ")")) { + return amount.includes("("); + } + return !amount.includes("("); +} diff --git a/src/features/suggestions/parser/foodDescription.test.js b/src/features/suggestions/parser/foodDescription.test.js index 3e56e5d0..756b02a1 100644 --- a/src/features/suggestions/parser/foodDescription.test.js +++ b/src/features/suggestions/parser/foodDescription.test.js @@ -247,4 +247,16 @@ test("parenthesis in unit includes /", () => { } } ); +}) + +test("parenthesis in unit with no content", () => { + expect(parseFoodDescription("Muffin 1 large (")).toMatchObject( + { + foodName: "Muffin", + amount: "1 large (", + measurement: { + unitText: "large (", + } + } + ); }) \ No newline at end of file diff --git a/src/features/suggestions/search/autoCompletion.ts b/src/features/suggestions/search/autoCompletion.ts index f3c3d1b1..3d53063a 100644 --- a/src/features/suggestions/search/autoCompletion.ts +++ b/src/features/suggestions/search/autoCompletion.ts @@ -4,16 +4,20 @@ import { createSuggestion, Suggestion } from '../Suggestion'; import autoCompleteUnit from './unitMiniSearch'; import { DecomposedFoodDescription } from '../parser/DecomposedFoodDescription'; import { findNameSuggestions } from './foodNameSearch'; +import { isAmountParsable } from '../generate/isAmountParsable'; export default function findAutoCompletions(foodDescription: DecomposedFoodDescription): Suggestion[] { const { foodName, amount, foodNameCompleted, unitCompleted } = foodDescription; if (foodNameCompleted) { if (amount && !unitCompleted) { const suggestionWithAmount = _.partial(createSuggestion, foodName); - const amountAutoCompletions = findAmountAutoCompletions(parseAmount(amount)) - .map(suggestionWithAmount); - if (_.size(amountAutoCompletions) > 0) - return amountAutoCompletions; + if (isAmountParsable(amount)) { + const amountAutoCompletions = findAmountAutoCompletions(parseAmount(amount)) + .map(suggestionWithAmount); + if (_.size(amountAutoCompletions) > 0) { + return amountAutoCompletions; + } + } } return [createSuggestion(foodName, amount)]; }