Web implementation of the Gourmet Recipe Manager
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

348 lines
8.4 KiB

package com.mousetech.gourmetj.utils;
import java.text.DecimalFormat;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.mousetech.gourmetj.persistence.model.Ingredient;
/**
* Service for taking an ingredients line, parsing it and
* returning a populated Ingredients object. Understands fraction
* characters in the amounts fields.
*
* @author timh
* @since Dec 1, 2021 TestedBy @see IngredientDigesterTest
*/
public class IngredientDigester {
/**
* @author timh
* @since Dec 5, 2021
*/
public enum IngredientAmountFormat {
IA_DECIMAL, // 5.75
IA_TEXT, // 5 3/4
IA_SYMBOLS, // 5¾
}
final static String amountPatternTxt =
"([0-9¼⅓½⅔¾⅛⅜⅝⅞][\\s\\-\\.]*[/0-9¼⅓½⅔¾⅛⅜⅝⅞]*)";
final static Pattern amountPattern =
Pattern.compile(amountPatternTxt);
final static Pattern fractionPattern =
Pattern.compile("(\\d*)[\\s\\-]*(\\d+)/(\\d+)");
/**
* Digest an ingredient text line and construct an Ingredient
* object
*
* @param inputText Input text line
* @return Ingredient
*/
public static Ingredient digest(String inputText) {
inputText = inputText.trim();
final boolean optional =
inputText.toLowerCase().contains("optional");
Ingredient ing = new Ingredient();
ing.setAmount(0.0);
// Split for amount, unit and rest of string.
String[] pamt = parseFancyNumber(inputText);
if (pamt == null) {
// Can't parse
ing.setAmount(null);
ing.setUnit(null);
ing.setItem(inputText);
} else {
final Double[] amount = digestAmount(pamt[0]);
ing.setAmount(amount[0]);
ing.setRangeamount(amount[1]);
String[] unext = pamt[1].split("\\s", 2);
switch (unext.length) {
case 1:
ing.setUnit(null);
ing.setItem(nonoise(unext[0]));
break;
case 2:
ing.setUnit(unext[0]);
ing.setItem(nonoise(unext[1]));
break;
}
}
ing.setPosition(null);
ing.setOptional(optional ? 1 : 0);
// Extract the item type from the item name
// Assume possible prep details ("parsley, chopped")
String istring = ing.getItem();
String iclass = istring.split("\\s*,",2)[0];
ing.setIngkey(iclass);
return ing;
}
/**
* Remove "noise" from ingredient title. For best results,
* should be adaptable for incoming language.
*
* @param string Item string with noise
* @return Item string with noise removed
*/
private static String nonoise(String string) {
String xstring = string;
if ( xstring.startsWith("of ")) {
xstring = xstring.substring(3);
}
// Todo: remote "optional" from string, if present.
return xstring;
}
/**
* Break down ingredients line into 2 parts. First part is
* numeric text representing amount and optional range,
* second part is text remainder, including units.
*
* @param instring String to parse
* @return 2-dimensional String array with amount and
* remainder or <code>null</code> if instring does
* not parse.
*
*/
static String[] parseFancyNumber(String instring) {
Matcher m = amountPattern.matcher(instring);
if (m.find()) {
// System.out.println("GROUPS=" + m.groupCount());
String rs = m.group(1);
return new String[] { rs.trim(),
instring.substring(m.end()).trim() };
}
return null;
}
// TODO: Remove "optional", if present, including
// brackets/parentheses
/**
* Convert instring into a number with optional fractions.
* Can take decimals, unicode ratio characters, and n/m form
* with embedded spaces and dashes.
*
* @param instring String to analyze
* @return number equivalent pair. Note that thirds are not
* precise in binary floating-point! First element is
* low range amount. Second element is null unless a
* high range was given as well.
*/
public static Double[] digestAmount(String instring) {
String[] amtParts = instring.split("\\s*-\\s*");
Double[] result = new Double[2];
result[0] = parseSingleAmount(amtParts[0]);
if (amtParts.length == 1) {
return result;
}
// dash separates low, high values OR integer and a
// fraction.
result[1] = parseSingleAmount(amtParts[1]);
if (result[1] < result[0]) {
result[0] += result[1];
result[1] = 0.0d;
}
return result;
}
/**
* Parse a single amount. May be integer, decimal number,
* integer + fraction character or [integer] integer /
* integer.
*
* @param instring amount string to parse
* @return parsed value.
* @throws NumberFormatException if bad decimal value given.
*/
static Double parseSingleAmount(String instring) {
double value = 0;
double frac = 0;
if (instring.contains("/")) {
// Break down a/b fraction
frac = digestFraction(instring);
return frac;
}
if (instring.contains(".")) {
frac = Double.valueOf(instring);
return frac;
}
// Look for integer and possibly a symbol;
for (int i = 0; i < instring.length(); i++) {
final Character ch = instring.charAt(i);
switch (ch) {
case '⅛':
frac = 0.125d;
break;
case '⅓':
frac = 1.0 / 3.0;
break;
case '¼':
frac = 0.25d;
break;
case '⅜':
frac = 3.0 * 0.125d;
break;
case '½':
frac = 0.5d;
break;
case '⅝':
frac = 5.0 * 0.125d;
break;
case '⅔':
frac = 2.0 / 3.0;
break;
case '⅞':
frac = 7.0 * 0.125d;
break;
default:
if (Character.isDigit(ch)) {
value = value * 10.0 + (ch - '0');
}
// Ignore spaces, dashes, etc.
break;
}
}
value += frac;
return value;
}
/**
* Pull fraction from tail-end of string and evaluate it.
*
* @param instring String in the form dddxxxddd/ddd, where d
* is digit, x is dash or space (optional)
* @return results of evaluation. May need to throw
* Exception???
*/
static double digestFraction(String instring) {
Matcher m = fractionPattern.matcher(instring);
if (!m.find()) {
return 0.0;
}
String lead = m.group(1);
String snumerator = m.group(2);
String sdenominator = m.group(3);
double numerator;
double denominator;
try {
numerator = Double.parseDouble(snumerator);
denominator = Double.parseDouble(sdenominator);
} catch (NumberFormatException e) {
// TODO Auto-generated catch block
e.printStackTrace();
return 0.0;
}
if (denominator == 0.0d) {
return 0.0d;
}
double result = numerator / denominator;
if (!lead.isEmpty()) {
result += Double.valueOf(lead); // integral part
}
return result;
}
/**
* Convert floating-point amounts to display-friendly form.
*
* @param format Amount formatting scheme
* @param amt Amount to display
* @param ext amount range high value (can be null)
* @return Displayable String. Zero returns an empty string.
*
*/
public static String displayAmount(
IngredientAmountFormat format, Double amt,
Double ext) {
if (amt == 0.0d) {
return "";
}
String amountRange = displayAmountPart(format, amt);
if (ext != null) {
amountRange += "-" + displayAmountPart(format, ext);
}
return amountRange;
}
/**
* @param format
* @param amt Amount to format
* @return Amount formatted in decimal, text, or text/symbol.
* Values that don't conform to the common fractions
* report as decimal values. Returns empty string for
* 0.
*/
static String displayAmountPart(
IngredientAmountFormat format, Double amt) {
if (amt == 0.0d) {
return "";
}
DecimalFormat df = new DecimalFormat("#.###");
String fnum = df.format(amt);
if (format == IngredientAmountFormat.IA_DECIMAL) {
return fnum;
}
String part[] = fnum.split("\\.");
if (part.length == 1) {
return part[0]; // Integer
}
String p2 = part[1];
String fs = "";
if (format == IngredientAmountFormat.IA_TEXT) {
if (p2.equals("125")) {
fs = "1/8";
} else if (p2.equals("333")) {
fs = "1/3";
} else if (p2.equals("25")) {
fs = "1/4";
} else if (p2.equals("5")) {
fs = "1/2";
} else if (p2.equals("667")) {
fs = "2/3";
} else if (p2.equals("75")) {
fs = "3/4";
} else { // 3, 5, 7/8
return fnum;
}
if (!"0".equals(part[0])) {
fs = ' ' + fs;
}
} else {
if (p2.equals("125")) {
fs = "⅛";
} else if (p2.equals("333")) {
fs = "⅓";
} else if (p2.equals("25")) {
fs = "¼";
} else if (p2.equals("5")) {
fs = "½";
} else if (p2.equals("667")) {
fs = "⅔";
} else if (p2.equals("75")) {
fs = "¾";
} else { // 3, 5, 7/8
return fnum;
}
}
if ("0".equals(part[0])) {
part[0] = "";
}
return part[0] + fs;
}
}