333 lines
8.0 KiB
Java
333 lines
8.0 KiB
Java
package com.mousetech.gourmetj;
|
|
|
|
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(unext[0]);
|
|
break;
|
|
case 2:
|
|
ing.setUnit(unext[0]);
|
|
ing.setItem(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;
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
}
|