ソースを参照

Merge branch 'refactor/model/ingredient'

* refactor/model/ingredient:
  Add back RecipeComponent exclusively for internal use (providing default functions).
  Move preparation into Ingredient, allow repeating ingredients with different preparations.
  Extract out ingredient amounts.
Sam Jaffe 5 年 前
コミット
29e741e850

+ 104 - 0
src/main/lombok/org/leumasjaffe/recipe/model/Amount.java

@@ -0,0 +1,104 @@
+package org.leumasjaffe.recipe.model;
+
+import java.util.Optional;
+import java.util.stream.Stream;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonValue;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+@Data @AllArgsConstructor
+public class Amount {
+	@AllArgsConstructor
+	public static enum Volume {
+		ml("ml", 1),
+		tsp("tsp", 5),
+		Tbsp("Tbsp", 15),
+		cup("cup", 240),
+		pinch("pinch", 0.3125),
+		dash("dash", 0.625);
+
+		final String displayName;
+		final double atomicUnits;
+	}
+	
+	@AllArgsConstructor
+	public static enum Weight {
+		g("g", 1),
+		oz("oz", 28.3495),
+		lb("lb", 453.592),
+		kg("kg", 1000);
+		
+		final String displayName;
+		final double atomicUnits;
+	}
+	
+	public static enum Unit {
+		COUNT, WEIGHT, VOLUME;
+	}
+	
+	Unit unit;
+	double value;
+	Volume vol;
+	Weight wgt;
+	
+	@JsonCreator
+	public Amount(final String serial) {
+		final String[] tokens = serial.split(" ", 2);
+		value = Double.parseDouble(tokens[0]);
+		final Optional<Volume> rv = Stream.of(Volume.values())
+				.filter(v -> v.displayName.equals(tokens[1])).findFirst();
+		if (rv.isPresent()) {
+			vol = rv.get();
+			return;
+		}
+		final Optional<Weight> rw = Stream.of(Weight.values())
+				.filter(w -> w.displayName.equals(tokens[1])).findFirst();
+		if (rw.isPresent()) {
+			wgt = rw.get();
+			return;
+		}
+	}
+	
+	@JsonValue @Override
+	public String toString() {
+		StringBuilder build = new StringBuilder();
+		build.append(value);
+		if (unit != Unit.COUNT) {
+			build.append(' ');
+			build.append(unitName());
+		}
+		return build.toString();
+	}
+	
+	private String unitName() {
+		switch (unit) {
+		case WEIGHT:
+			return wgt.displayName;
+		case VOLUME:
+			return vol.displayName;
+		default:
+			return "";
+		}
+	}
+
+	public Amount plus(final Amount amount) {
+		if (unit != amount.unit) {
+			throw new IllegalArgumentException("Cannot merge mass/volume/count amounts together");
+		}
+		return new Amount(unit, value + amount.value * scale(amount), vol, wgt);
+	}
+
+	private double scale(final Amount amount) {
+		switch (unit) {
+		case WEIGHT:
+			return amount.wgt.atomicUnits / wgt.atomicUnits;
+		case VOLUME:
+			return amount.vol.atomicUnits / vol.atomicUnits;
+		default:
+			return 1.0;
+		}
+	}
+}

+ 9 - 12
src/main/lombok/org/leumasjaffe/recipe/model/Card.java

@@ -1,18 +1,14 @@
 package org.leumasjaffe.recipe.model;
 
 import java.util.ArrayList;
-import java.util.Collection;
 import java.util.List;
 import java.util.Optional;
-import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
-import com.fasterxml.jackson.annotation.JsonIgnore;
-
 import lombok.Data;
 
 @Data
-public class Card {
+public class Card implements CompoundRecipeComponent {
 	int id = 0; // TODO Fix this
 	int[] dependsOn = {}; // decltype(id)[]
 	String vessel = "";
@@ -20,14 +16,15 @@ public class Card {
 	List<Step> cooking = new ArrayList<>();
 	Optional<Rest> rest = Optional.empty();
 	
-	@JsonIgnore
-	Collection<Ingredient> getIngredients() {
-		return getIngredientsAsStream().collect(Collectors.toList());
+	public Stream<Ingredient> getIngredientsAsStream() {
+		return getComponents().flatMap(RecipeComponent::getIngredientsAsStream);
 	}
 	
-	@JsonIgnore
-	Stream<Ingredient> getIngredientsAsStream() {
-		return preparation.map(p -> p.getIngredients().stream())
-				.orElse(cooking.stream().flatMap(s -> s.getIngredients().stream()));
+	public Stream<? extends RecipeComponent> getComponents() {
+		if (preparation.isPresent()) {
+			return Stream.of(preparation.get());
+		} else {
+			return cooking.stream();
+		}
 	}
 }

+ 31 - 0
src/main/lombok/org/leumasjaffe/recipe/model/CompoundRecipeComponent.java

@@ -0,0 +1,31 @@
+package org.leumasjaffe.recipe.model;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.stream.Stream;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+@JsonIgnoreProperties({"duration", "ingredients", "components", "ingredientsAsStream"})
+interface CompoundRecipeComponent extends RecipeComponent {
+	Stream<? extends RecipeComponent> getComponents();
+	Stream<Ingredient> getIngredientsAsStream();
+	
+	@Override
+	default Duration getDuration() {
+		return getComponents().map(RecipeComponent::getDuration)
+				.reduce(Duration.ZERO, Duration::plus);
+	}
+	
+	@Override
+	default Collection<Ingredient> getIngredients() {
+		final Map<String, Ingredient> map = new HashMap<>();
+        getIngredientsAsStream().forEach(i -> {
+        	final String key = i.key();
+            map.computeIfPresent(key, (k, v) -> v.plus(i));
+            map.computeIfAbsent(key, k -> i);
+        });
+        return map.values();
+	}
+}

+ 7 - 56
src/main/lombok/org/leumasjaffe/recipe/model/Ingredient.java

@@ -1,73 +1,24 @@
 package org.leumasjaffe.recipe.model;
 
-import org.leumasjaffe.container.Either;
-
-import com.fasterxml.jackson.annotation.JsonIgnore;
-import com.fasterxml.jackson.annotation.JsonProperty;
-
 import lombok.AllArgsConstructor;
 import lombok.Data;
-import lombok.Getter;
 
 @Data @AllArgsConstructor
 public class Ingredient {
-	@AllArgsConstructor @Getter
-	public static enum Volume {
-		ml("ml", 1),
-		tsp("tsp", 5),
-		Tbsp("Tbsp", 15),
-		cup("cup", 240);
-
-		final String displayName;
-		final double atomicUnits;
-	}
-	
-	@AllArgsConstructor @Getter
-	public static enum Weight {
-		g("g", 1),
-		oz("oz", 28.3495),
-		lb("lb", 453.592),
-		kg("kg", 1000),
-		pinch("pinch", 0.36),
-		dash("dash", 0.72);
-		
-		final String displayName;
-		final double atomicUnits;
-	}
-	
 	String name;
-	double value = 0;
-	@JsonIgnore Either<Volume, Weight> measure;
+	String preparation;
+	Amount amount;
 	
 	Ingredient plus(final Ingredient rhs) {
 		if (!name.equals(rhs.name)) {
 			throw new IllegalArgumentException("Combining ingredients of differing types");
-		} else if (measure.getState() != rhs.measure.getState()) {
-			throw new IllegalArgumentException("Cannot merge mass and volume together");
+		} else if (!preparation.equals(rhs.preparation)) {
+			throw new IllegalArgumentException("Cannot combine items with different preparations");
 		}
-		return new Ingredient(name, value + (rhs.value * conversionRatio(rhs.measure)), measure);
-	}
-
-	private double conversionRatio(Either<Volume, Weight> rhs) {
-		return units(rhs) / units(measure);
-	}
-
-	private static double units(Either<Volume, Weight> measure) {
-		return measure.unify(Volume::getAtomicUnits, Weight::getAtomicUnits);
+		return new Ingredient(name, preparation, amount.plus(rhs.amount));
 	}
 	
-	@JsonProperty("unit")
-	private String[] measureToString() {
-		String[] rval = {null, null};
-		return rval;
-	}
-	
-	@JsonProperty("unit")
-	private void measureFromString(String[] s) {
-		if (s[0] != null) {
-			measure = Either.ofLeft(Volume.valueOf(s[0]));
-		} else if (s[1] != null) {
-			measure = Either.ofRight(Weight.valueOf(s[1]));
-		}
+	String key() {
+		return getName() + getPreparation() + amount.getUnit().toString();
 	}
 }

+ 1 - 4
src/main/lombok/org/leumasjaffe/recipe/model/Preparation.java

@@ -1,15 +1,12 @@
 package org.leumasjaffe.recipe.model;
 
-import java.util.HashMap;
 import java.util.HashSet;
-import java.util.Map;
 import java.util.Set;
 
 import lombok.Data;
 
 @Data
-public class Preparation {
+public class Preparation implements RecipeComponent {
 	Set<Ingredient> ingredients = new HashSet<>();
-	Map<String, String> instructions = new HashMap<>();
 	Duration duration;
 }

+ 9 - 13
src/main/lombok/org/leumasjaffe/recipe/model/Product.java

@@ -1,26 +1,22 @@
 package org.leumasjaffe.recipe.model;
 
-import java.util.Collection;
 import java.util.List;
-import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
-import com.fasterxml.jackson.annotation.JsonIgnore;
-
 import lombok.Data;
 
 @Data
-public class Product {
+public class Product implements CompoundRecipeComponent {
 	String name;
 	List<Card> cards;
-	
-	@JsonIgnore
-	Collection<Ingredient> getIngredients() {
-		return getIngredientsAsStream().collect(Collectors.toList());
-	}
-	
-	@JsonIgnore
-	Stream<Ingredient> getIngredientsAsStream() {
+
+	@Override
+	public Stream<Ingredient> getIngredientsAsStream() {
 		return cards.stream().flatMap(Card::getIngredientsAsStream);
 	}
+
+	@Override
+	public Stream<? extends RecipeComponent> getComponents() {
+		return cards.stream();
+	}
 }

+ 11 - 9
src/main/lombok/org/leumasjaffe/recipe/model/Recipe.java

@@ -1,28 +1,30 @@
 package org.leumasjaffe.recipe.model;
 
 import java.util.ArrayList;
-import java.util.Collection;
 import java.util.List;
 import java.util.Optional;
-import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 import javax.swing.ImageIcon;
 
-import com.fasterxml.jackson.annotation.JsonIgnore;
-
 import lombok.Data;
 
 @Data
-public class Recipe {
+public class Recipe implements CompoundRecipeComponent {
 	String title;
 	String description;
-	Object nutrition;
 	int servings;
+	// TODO: Nutrition information
 	Optional<ImageIcon> photo; // TODO JSONIZATION	
 	List<Product> products = new ArrayList<>();
 	
-	@JsonIgnore
-	Collection<Ingredient> getIngredients() {
-		return products.stream().flatMap(Product::getIngredientsAsStream).collect(Collectors.toList());
+	@Override
+	public Stream<? extends RecipeComponent> getComponents() {
+		return products.stream();
+	}
+	
+	@Override
+	public Stream<Ingredient> getIngredientsAsStream() {
+		return products.stream().flatMap(Product::getIngredientsAsStream);
 	}
 }

+ 13 - 0
src/main/lombok/org/leumasjaffe/recipe/model/RecipeComponent.java

@@ -0,0 +1,13 @@
+package org.leumasjaffe.recipe.model;
+
+import java.util.Collection;
+import java.util.stream.Stream;
+
+interface RecipeComponent {
+	Collection<Ingredient> getIngredients();
+	Duration getDuration();
+
+	default Stream<Ingredient> getIngredientsAsStream() {
+		return getIngredients().stream();
+	}
+}

+ 1 - 1
src/main/lombok/org/leumasjaffe/recipe/model/Step.java

@@ -6,7 +6,7 @@ import java.util.Set;
 import lombok.Data;
 
 @Data
-public class Step {
+public class Step implements RecipeComponent {
 	Set<Ingredient> ingredients = new HashSet<>();
 	Duration duration;
 	String instruction;

+ 12 - 40
src/main/lombok/org/leumasjaffe/recipe/view/IngredientPanel.java

@@ -5,18 +5,12 @@ import java.awt.GridBagLayout;
 import javax.swing.JTextField;
 import java.awt.GridBagConstraints;
 import java.awt.Insets;
-import java.awt.event.ItemEvent;
 import java.text.NumberFormat;
 import java.util.Locale;
 
-import javax.swing.JToggleButton;
 import javax.swing.event.DocumentListener;
 import javax.swing.text.NumberFormatter;
 
-import org.leumasjaffe.recipe.model.Ingredient;
-
-import javax.swing.DefaultComboBoxModel;
-import javax.swing.JComboBox;
 import javax.swing.JFormattedTextField;
 import java.awt.Font;
 import javax.swing.JLabel;
@@ -24,12 +18,13 @@ import javax.swing.JLabel;
 @SuppressWarnings("serial")
 public class IngredientPanel extends JPanel implements AutoGrowPanel.DocumentListenable {
 	private JTextField txtName;
+	private JTextField txtUnit;
 	
 	public IngredientPanel() {
 		GridBagLayout gridBagLayout = new GridBagLayout();
-		gridBagLayout.columnWidths = new int[]{0, 100, 40, 0, 0, 0};
+		gridBagLayout.columnWidths = new int[]{0, 100, 40, 40, 0};
 		gridBagLayout.rowHeights = new int[]{0, 0};
-		gridBagLayout.columnWeights = new double[]{0.0, 1.0, 0.0, 0.0, 0.0, Double.MIN_VALUE};
+		gridBagLayout.columnWeights = new double[]{0.0, 1.0, 0.0, 0.0, Double.MIN_VALUE};
 		gridBagLayout.rowWeights = new double[]{0.0, Double.MIN_VALUE};
 		setLayout(gridBagLayout);
 		
@@ -64,38 +59,15 @@ public class IngredientPanel extends JPanel implements AutoGrowPanel.DocumentLis
 		add(txtAmount, gbc_txtAmount);
 		txtAmount.setColumns(4);
 		
-		JToggleButton tglbtnUnits = new JToggleButton("vol");
-		tglbtnUnits.setFont(new Font("Source Code Pro", Font.PLAIN, 10));
-		tglbtnUnits.setMargin(new Insets(2, 5, 2, 5));
-		GridBagConstraints gbc_tglbtnUnits = new GridBagConstraints();
-		gbc_tglbtnUnits.insets = new Insets(0, 0, 0, 5);
-		gbc_tglbtnUnits.gridx = 3;
-		gbc_tglbtnUnits.gridy = 0;
-		add(tglbtnUnits, gbc_tglbtnUnits);
-		
-		JComboBox<Ingredient.Weight> weights = new JComboBox<>(new DefaultComboBoxModel<>(Ingredient.Weight.values()));
-		weights.setFont(new Font("Source Code Pro", Font.PLAIN, 10));
-		JComboBox<Ingredient.Volume> volumes = new JComboBox<>(new DefaultComboBoxModel<>(Ingredient.Volume.values()));
-		volumes.setFont(new Font("Source Code Pro", Font.PLAIN, 10));
-		GridBagConstraints gbc_comboBox = new GridBagConstraints();
-		gbc_comboBox.fill = GridBagConstraints.HORIZONTAL;
-		gbc_comboBox.gridx = 4;
-		gbc_comboBox.gridy = 0;
-		add(volumes, gbc_comboBox);
-//		add(weights, gbc_comboBox);
-
-		tglbtnUnits.addItemListener(e -> {
-			if (e.getStateChange() == ItemEvent.SELECTED) {
-				tglbtnUnits.setText("wgt");
-				remove(volumes);
-				add(weights, gbc_comboBox);
-			} else {
-				tglbtnUnits.setText("vol");
-				remove(weights);
-				add(volumes, gbc_comboBox);
-			}
-			repaint();
-		});
+		txtUnit = new JTextField();
+		txtUnit.setFont(new Font("Source Code Pro", Font.PLAIN, 10));
+		GridBagConstraints gbc_txtUnit = new GridBagConstraints();
+		gbc_txtUnit.anchor = GridBagConstraints.ABOVE_BASELINE;
+		gbc_txtUnit.fill = GridBagConstraints.HORIZONTAL;
+		gbc_txtUnit.gridx = 3;
+		gbc_txtUnit.gridy = 0;
+		add(txtUnit, gbc_txtUnit);
+		txtUnit.setColumns(10);
 	}
 
 	@Override

+ 2 - 8
src/test/resources/example.json

@@ -13,11 +13,8 @@
             "ingredients": [
               {
                 "name": "onion",
-                "value": 100,
-                "unit": [
-                  null,
-                  "g"
-                ]
+                "preparation": "",
+                "amount": "100 g"
               }
             ],
             "duration": {
@@ -25,9 +22,6 @@
               "isApproximate": true,
               "minSeconds": 300,
               "maxSeconds": 600
-            },
-            "instructions": {
-              
             }
           }
         }