View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.functor.example.kata.one;
18  
19  import static org.junit.Assert.assertEquals;
20  
21  import org.apache.commons.functor.Function;
22  import org.apache.commons.functor.adapter.BinaryFunctionFunction;
23  import org.apache.commons.functor.core.Identity;
24  import org.apache.commons.functor.core.comparator.IsGreaterThan;
25  import org.apache.commons.functor.core.composite.Composite;
26  import org.apache.commons.functor.core.composite.ConditionalFunction;
27  import org.apache.commons.functor.core.composite.CompositeBinaryFunction;
28  import org.junit.Test;
29  
30  /**
31   * Dave Thomas's Kata One asks us to think about how one might implement pricing rules:
32   * 
33   * "Some things in supermarkets have simple prices: this can of beans costs $0.65. Other things have more complex
34   * prices. For example:
35   * 
36   * o three for a dollar (so what?s the price if I buy 4, or 5?)
37   * 
38   * o $1.99/pound (so what does 4 ounces cost?)
39   * 
40   * o buy two, get one free (so does the third item have a price?)"
41   * 
42   * Functors provide one approach to this sort of problem, and in this example we'll demonstrate some simple cases.
43   * 
44   * See http://pragprog.com/pragdave/Practices/Kata/KataOne.rdoc,v for more information on this Kata.
45   * 
46   * @version $Revision: 1541658 $ $Date: 2013-11-13 19:54:05 +0100 (Mi, 13 Nov 2013) $
47   */
48  public class SupermarketPricingExample {
49  
50      // tests
51      // ----------------------------------------------------------
52  
53      /*
54       * The simplest form of pricing is simply a constant rate. In Dave's example, a can of beans costs $0.65, and n cans
55       * of beans cost n*0.65.
56       * 
57       * This pricing rule simply multiplies the quantity by a constant, e.g.: ToMoney.from(Multiply.by(65))
58       * 
59       * This case is so common, we may want to introduce a special Product constructor to wrap up create the functors for
60       * us.
61       */
62      @Test
63      public void testConstantPricePerUnit() throws Exception {
64          {
65              Product beans = new Product("Can of Beans", "SKU-0001", ToMoney.from(Multiply.by(65)));
66  
67              assertEquals(new Money(0 * 65), beans.getPrice(0));
68              assertEquals(new Money(1 * 65), beans.getPrice(1));
69              assertEquals(new Money(2 * 65), beans.getPrice(2));
70              assertEquals(new Money(3 * 65), beans.getPrice(3));
71          }
72          // or, using the speical constructor:
73          {
74              Product beans = new Product("Can of Beans", "SKU-0001", 65);
75  
76              assertEquals(new Money(0 * 65), beans.getPrice(0));
77              assertEquals(new Money(1 * 65), beans.getPrice(1));
78              assertEquals(new Money(2 * 65), beans.getPrice(2));
79              assertEquals(new Money(3 * 65), beans.getPrice(3));
80          }
81      }
82  
83      /*
84       * A slighly more complicated example is a bulk discount. For example, bananas may be $0.33 cents each, or 4 for a
85       * dollar ($1.00).
86       * 
87       * This rule is underspecified by itself, there are at least two ways to interpret this pricing rule:
88       * 
89       * a) the cost is $0.33 cents for 3 or fewer, $0.25 for 4 or more
90       * 
91       * or
92       * 
93       * b) the cost is $1.00 for every group of 4, $0.33 each for anything left over
94       * 
95       * although I think in practice, "4 for a dollar" usually means the former and not the latter.
96       * 
97       * We can implement either:
98       */
99      @Test
100     public void testFourForADollar_A() throws Exception {
101         Product banana =
102             new Product("Banana", "SKU-0002", ToMoney.from(new ConditionalFunction<Integer, Number>(IsGreaterThan
103                 .instance(Integer.valueOf(3)), Multiply.by(25), Multiply.by(33))));
104 
105         assertEquals(new Money(0 * 33), banana.getPrice(0));
106         assertEquals(new Money(1 * 33), banana.getPrice(1));
107         assertEquals(new Money(2 * 33), banana.getPrice(2));
108         assertEquals(new Money(3 * 33), banana.getPrice(3));
109         assertEquals(new Money(4 * 25), banana.getPrice(4));
110         assertEquals(new Money(5 * 25), banana.getPrice(5));
111         assertEquals(new Money(6 * 25), banana.getPrice(6));
112         assertEquals(new Money(7 * 25), banana.getPrice(7));
113         assertEquals(new Money(8 * 25), banana.getPrice(8));
114     }
115 
116     @Test
117     public void testFourForADollar_B() throws Exception {
118         Product banana =
119             new Product("Banana", "SKU-0002", ToMoney.from(new BinaryFunctionFunction<Integer, Number>(
120                 new CompositeBinaryFunction<Integer, Integer, Number>(Add.instance(), Composite.function(
121                     Multiply.by(100), Divide.by(4)), Composite.function(Multiply.by(33), Mod.by(4))))));
122         assertEquals(new Money(0 * 33 + 0 * 25), banana.getPrice(0));
123         assertEquals(new Money(1 * 33 + 0 * 25), banana.getPrice(1));
124         assertEquals(new Money(2 * 33 + 0 * 25), banana.getPrice(2));
125         assertEquals(new Money(3 * 33 + 0 * 25), banana.getPrice(3));
126         assertEquals(new Money(0 * 33 + 4 * 25), banana.getPrice(4));
127         assertEquals(new Money(1 * 33 + 4 * 25), banana.getPrice(5));
128         assertEquals(new Money(2 * 33 + 4 * 25), banana.getPrice(6));
129         assertEquals(new Money(3 * 33 + 4 * 25), banana.getPrice(7));
130         assertEquals(new Money(0 * 33 + 8 * 25), banana.getPrice(8));
131     }
132 
133     /*
134      * Another interesting pricing rule is something like "buy 2, get 1 free".
135      * 
136      * This may be implemented using a formula like: costPerUnit * (quantity - quantity / 2)
137      * 
138      * For example...
139      */
140     @Test
141     public void testBuyTwoGetOneFree_1() throws Exception {
142         Product apple =
143             new Product("Apple", "SKU-0003", ToMoney.from(Composite.function(Multiply.by(40), BinaryFunctionFunction
144                 .adapt(new CompositeBinaryFunction<Number, Number, Number>(Subtract.instance(), new Identity<Number>(),
145                     Divide.by(3))))));
146 
147         assertEquals(new Money(0 * 40), apple.getPrice(0));
148         assertEquals(new Money(1 * 40), apple.getPrice(1));
149         assertEquals(new Money(2 * 40), apple.getPrice(2));
150         assertEquals(new Money(2 * 40), apple.getPrice(3));
151         assertEquals(new Money(3 * 40), apple.getPrice(4));
152         assertEquals(new Money(4 * 40), apple.getPrice(5));
153         assertEquals(new Money(4 * 40), apple.getPrice(6));
154         assertEquals(new Money(5 * 40), apple.getPrice(7));
155         assertEquals(new Money(6 * 40), apple.getPrice(8));
156         assertEquals(new Money(6 * 40), apple.getPrice(9));
157         assertEquals(new Money(7 * 40), apple.getPrice(10));
158     }
159 
160     /*
161      * ...but our pricing rule is starting to get ugly, and we haven't even considered things something like
162      * "buy 3, get 2 free", etc.
163      * 
164      * Perhaps a special Function instance is in order:
165      */
166 
167     class BuyNGetMFree implements Function<Number, Number> {
168         public BuyNGetMFree(int n, int m, int costPerUnit) {
169             this.n = n;
170             this.m = m;
171             this.costPerUnit = costPerUnit;
172         }
173 
174         public Number evaluate(Number num) {
175             int quantity = num.intValue();
176             int cost = 0;
177 
178             while (quantity >= n) {
179                 // buy n
180                 cost += n * costPerUnit;
181                 quantity -= n;
182                 // get m (or fewer) free
183                 quantity -= Math.min(quantity, m);
184             }
185             // buy less than n
186             cost += quantity * costPerUnit;
187 
188             return Integer.valueOf(cost);
189         }
190 
191         private int n, m, costPerUnit;
192     }
193 
194     @Test
195     public void testBuyTwoGetOneFree_2() throws Exception {
196         Product apple = new Product("Apple", "SKU-0003", ToMoney.from(new BuyNGetMFree(2, 1, 40)));
197 
198         assertEquals(new Money(0 * 40), apple.getPrice(0));
199         assertEquals(new Money(1 * 40), apple.getPrice(1));
200         assertEquals(new Money(2 * 40), apple.getPrice(2));
201         assertEquals(new Money(2 * 40), apple.getPrice(3));
202         assertEquals(new Money(3 * 40), apple.getPrice(4));
203         assertEquals(new Money(4 * 40), apple.getPrice(5));
204         assertEquals(new Money(4 * 40), apple.getPrice(6));
205         assertEquals(new Money(5 * 40), apple.getPrice(7));
206         assertEquals(new Money(6 * 40), apple.getPrice(8));
207         assertEquals(new Money(6 * 40), apple.getPrice(9));
208         assertEquals(new Money(7 * 40), apple.getPrice(10));
209     }
210 
211     @Test
212     public void testBuyThreeGetTwoFree() throws Exception {
213         Product apple = new Product("Apple", "SKU-0003", ToMoney.from(new BuyNGetMFree(3, 2, 40)));
214 
215         assertEquals(new Money(0 * 40), apple.getPrice(0));
216         assertEquals(new Money(1 * 40), apple.getPrice(1));
217         assertEquals(new Money(2 * 40), apple.getPrice(2));
218         assertEquals(new Money(3 * 40), apple.getPrice(3));
219         assertEquals(new Money(3 * 40), apple.getPrice(4));
220         assertEquals(new Money(3 * 40), apple.getPrice(5));
221         assertEquals(new Money(4 * 40), apple.getPrice(6));
222         assertEquals(new Money(5 * 40), apple.getPrice(7));
223         assertEquals(new Money(6 * 40), apple.getPrice(8));
224         assertEquals(new Money(6 * 40), apple.getPrice(9));
225         assertEquals(new Money(6 * 40), apple.getPrice(10));
226         assertEquals(new Money(7 * 40), apple.getPrice(11));
227     }
228 
229     @Test
230     public void testBuyTwoGetFiveFree() throws Exception {
231         Product apple = new Product("Apple", "SKU-0003", ToMoney.from(new BuyNGetMFree(2, 5, 40)));
232 
233         assertEquals(new Money(0 * 40), apple.getPrice(0));
234         assertEquals(new Money(1 * 40), apple.getPrice(1));
235         assertEquals(new Money(2 * 40), apple.getPrice(2));
236         assertEquals(new Money(2 * 40), apple.getPrice(3));
237         assertEquals(new Money(2 * 40), apple.getPrice(4));
238         assertEquals(new Money(2 * 40), apple.getPrice(5));
239         assertEquals(new Money(2 * 40), apple.getPrice(6));
240         assertEquals(new Money(2 * 40), apple.getPrice(7));
241         assertEquals(new Money(3 * 40), apple.getPrice(8));
242         assertEquals(new Money(4 * 40), apple.getPrice(9));
243         assertEquals(new Money(4 * 40), apple.getPrice(10));
244         assertEquals(new Money(4 * 40), apple.getPrice(11));
245         assertEquals(new Money(4 * 40), apple.getPrice(12));
246         assertEquals(new Money(4 * 40), apple.getPrice(13));
247         assertEquals(new Money(4 * 40), apple.getPrice(14));
248         assertEquals(new Money(5 * 40), apple.getPrice(15));
249     }
250 }