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.UnaryFunction;
22  import org.apache.commons.functor.adapter.BinaryFunctionUnaryFunction;
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.ConditionalUnaryFunction;
27  import org.apache.commons.functor.core.composite.UnaryCompositeBinaryFunction;
28  import org.junit.Test;
29  
30  /**
31   * Dave Thomas's Kata One asks us to think about how one might
32   * implement pricing rules:
33   *
34   * "Some things in supermarkets have simple prices: this can of
35   * beans costs $0.65. Other things have more complex prices.
36   * For example:
37   *
38   * o three for a dollar (so what?s the price if I buy 4, or 5?)
39   *
40   * o $1.99/pound (so what does 4 ounces cost?)
41   *
42   * o buy two, get one free (so does the third item have a price?)"
43   *
44   * Functors provide one approach to this sort of problem, and in
45   * this example we'll demonstrate some simple cases.
46   *
47   * See http://pragprog.com/pragdave/Practices/Kata/KataOne.rdoc,v
48   * for more information on this Kata.
49   *
50   * @version $Revision: 1171255 $ $Date: 2011-09-15 22:27:39 +0200 (Thu, 15 Sep 2011) $
51   * @author Rodney Waldhoff
52   */
53  public class SupermarketPricingExample {
54  
55      // tests
56      //----------------------------------------------------------
57  
58      /*
59       * The simplest form of pricing is simply a constant
60       * rate.  In Dave's example, a can of beans costs $0.65,
61       * and n cans of beans cost n*0.65.
62       *
63       * This pricing rule simply multiplies the quantity by
64       * a constant, e.g.:
65       *   ToMoney.from(Multiply.by(65))
66       *
67       * This case is so common, we may want to introduce a
68       * special Product constructor to wrap up create the
69       * functors for us.
70       */
71      @Test
72      public void testConstantPricePerUnit() throws Exception {
73          {
74              Product beans = new Product(
75                  "Can of Beans",
76                  "SKU-0001",
77                  ToMoney.from(Multiply.by(65)));
78  
79              assertEquals(new Money(0*65),beans.getPrice(0));
80              assertEquals(new Money(1*65),beans.getPrice(1));
81              assertEquals(new Money(2*65),beans.getPrice(2));
82              assertEquals(new Money(3*65),beans.getPrice(3));
83          }
84          // or, using the speical constructor:
85          {
86              Product beans = new Product(
87                  "Can of Beans",
88                  "SKU-0001",
89                  65);
90  
91              assertEquals(new Money(0*65),beans.getPrice(0));
92              assertEquals(new Money(1*65),beans.getPrice(1));
93              assertEquals(new Money(2*65),beans.getPrice(2));
94              assertEquals(new Money(3*65),beans.getPrice(3));
95          }
96      }
97  
98      /*
99       * A slighly more complicated example is a bulk
100      * discount.  For example, bananas may be
101      * $0.33 cents each, or 4 for a dollar ($1.00).
102      *
103      * This rule is underspecified by itself, there are
104      * at least two ways to interpret this pricing rule:
105      *
106      * a) the cost is $0.33 cents for 3 or fewer, $0.25
107      *    for 4 or more
108      *
109      * or
110      *
111      * b) the cost is $1.00 for every group of 4, $0.33
112      *    each for anything left over
113      *
114      * although I think in practice, "4 for a dollar"
115      * usually means the former and not the latter.
116      *
117      * We can implement either:
118      */
119     @Test
120     public void testFourForADollar_A() throws Exception {
121         Product banana = new Product(
122             "Banana",
123             "SKU-0002",
124             ToMoney.from(
125                 new ConditionalUnaryFunction<Integer, Number>(
126                     IsGreaterThan.instance(new Integer(3)),
127                     Multiply.by(25),
128                     Multiply.by(33))));
129 
130         assertEquals(new Money(0*33),banana.getPrice(0));
131         assertEquals(new Money(1*33),banana.getPrice(1));
132         assertEquals(new Money(2*33),banana.getPrice(2));
133         assertEquals(new Money(3*33),banana.getPrice(3));
134         assertEquals(new Money(4*25),banana.getPrice(4));
135         assertEquals(new Money(5*25),banana.getPrice(5));
136         assertEquals(new Money(6*25),banana.getPrice(6));
137         assertEquals(new Money(7*25),banana.getPrice(7));
138         assertEquals(new Money(8*25),banana.getPrice(8));
139     }
140 
141 
142     @Test
143     public void testFourForADollar_B() throws Exception {
144         Product banana = new Product(
145             "Banana",
146             "SKU-0002",
147             ToMoney.from(
148                 new BinaryFunctionUnaryFunction<Integer, Number>(
149                     new UnaryCompositeBinaryFunction<Integer, Integer, Number>(
150                         Add.instance(),
151                         Composite.function(
152                             Multiply.by(100),
153                             Divide.by(4)),
154                         Composite.function(
155                             Multiply.by(33),
156                             Mod.by(4))))));
157         assertEquals(new Money(0*33+0*25),banana.getPrice(0));
158         assertEquals(new Money(1*33+0*25),banana.getPrice(1));
159         assertEquals(new Money(2*33+0*25),banana.getPrice(2));
160         assertEquals(new Money(3*33+0*25),banana.getPrice(3));
161         assertEquals(new Money(0*33+4*25),banana.getPrice(4));
162         assertEquals(new Money(1*33+4*25),banana.getPrice(5));
163         assertEquals(new Money(2*33+4*25),banana.getPrice(6));
164         assertEquals(new Money(3*33+4*25),banana.getPrice(7));
165         assertEquals(new Money(0*33+8*25),banana.getPrice(8));
166     }
167 
168 
169     /*
170      * Another interesting pricing rule is
171      * something like "buy 2, get 1 free".
172      *
173      * This may be implemented using a formula
174      * like:
175      *   costPerUnit * (quantity - quantity / 2)
176      *
177      * For example...
178      */
179     @Test
180     public void testBuyTwoGetOneFree_1() throws Exception {
181         Product apple = new Product(
182             "Apple",
183             "SKU-0003",
184             ToMoney.from(
185                     Composite.function(Multiply.by(40),
186                     BinaryFunctionUnaryFunction.adapt(new UnaryCompositeBinaryFunction<Number, Number, Number>(Subtract.instance(),
187                             new Identity<Number>(),
188                             Divide.by(3))))));
189 
190         assertEquals(new Money(0*40),apple.getPrice(0));
191         assertEquals(new Money(1*40),apple.getPrice(1));
192         assertEquals(new Money(2*40),apple.getPrice(2));
193         assertEquals(new Money(2*40),apple.getPrice(3));
194         assertEquals(new Money(3*40),apple.getPrice(4));
195         assertEquals(new Money(4*40),apple.getPrice(5));
196         assertEquals(new Money(4*40),apple.getPrice(6));
197         assertEquals(new Money(5*40),apple.getPrice(7));
198         assertEquals(new Money(6*40),apple.getPrice(8));
199         assertEquals(new Money(6*40),apple.getPrice(9));
200         assertEquals(new Money(7*40),apple.getPrice(10));
201     }
202 
203     /*
204      * ...but our pricing rule is starting to get ugly,
205      * and we haven't even considered things
206      * something like "buy 3, get 2 free", etc.
207      *
208      * Perhaps a special UnaryFunction instance is in
209      * order:
210      */
211 
212     class BuyNGetMFree implements UnaryFunction<Number, Number> {
213        public BuyNGetMFree(int n, int m, int costPerUnit) {
214            this.n = n;
215            this.m = m;
216            this.costPerUnit = costPerUnit;
217        }
218 
219        public Number evaluate(Number num) {
220            int quantity = num.intValue();
221            int cost = 0;
222 
223            while(quantity >= n) {
224                // buy n
225                cost += n * costPerUnit;
226                quantity -= n;
227                // get m (or fewer) free
228                quantity -= Math.min(quantity,m);
229            }
230            // buy less than n
231            cost += quantity * costPerUnit;
232 
233            return new Integer(cost);
234        }
235 
236        private int n, m, costPerUnit;
237    }
238 
239     @Test
240     public void testBuyTwoGetOneFree_2() throws Exception {
241         Product apple = new Product(
242             "Apple",
243             "SKU-0003",
244             ToMoney.from(new BuyNGetMFree(2,1,40)));
245 
246         assertEquals(new Money(0*40),apple.getPrice(0));
247         assertEquals(new Money(1*40),apple.getPrice(1));
248         assertEquals(new Money(2*40),apple.getPrice(2));
249         assertEquals(new Money(2*40),apple.getPrice(3));
250         assertEquals(new Money(3*40),apple.getPrice(4));
251         assertEquals(new Money(4*40),apple.getPrice(5));
252         assertEquals(new Money(4*40),apple.getPrice(6));
253         assertEquals(new Money(5*40),apple.getPrice(7));
254         assertEquals(new Money(6*40),apple.getPrice(8));
255         assertEquals(new Money(6*40),apple.getPrice(9));
256         assertEquals(new Money(7*40),apple.getPrice(10));
257     }
258 
259     @Test
260     public void testBuyThreeGetTwoFree() throws Exception {
261         Product apple = new Product(
262             "Apple",
263             "SKU-0003",
264             ToMoney.from(new BuyNGetMFree(3,2,40)));
265 
266         assertEquals(new Money(0*40),apple.getPrice(0));
267         assertEquals(new Money(1*40),apple.getPrice(1));
268         assertEquals(new Money(2*40),apple.getPrice(2));
269         assertEquals(new Money(3*40),apple.getPrice(3));
270         assertEquals(new Money(3*40),apple.getPrice(4));
271         assertEquals(new Money(3*40),apple.getPrice(5));
272         assertEquals(new Money(4*40),apple.getPrice(6));
273         assertEquals(new Money(5*40),apple.getPrice(7));
274         assertEquals(new Money(6*40),apple.getPrice(8));
275         assertEquals(new Money(6*40),apple.getPrice(9));
276         assertEquals(new Money(6*40),apple.getPrice(10));
277         assertEquals(new Money(7*40),apple.getPrice(11));
278     }
279 
280     @Test
281     public void testBuyTwoGetFiveFree() throws Exception {
282          Product apple = new Product(
283              "Apple",
284              "SKU-0003",
285              ToMoney.from(new BuyNGetMFree(2,5,40)));
286 
287          assertEquals(new Money(0*40),apple.getPrice(0));
288          assertEquals(new Money(1*40),apple.getPrice(1));
289          assertEquals(new Money(2*40),apple.getPrice(2));
290          assertEquals(new Money(2*40),apple.getPrice(3));
291          assertEquals(new Money(2*40),apple.getPrice(4));
292          assertEquals(new Money(2*40),apple.getPrice(5));
293          assertEquals(new Money(2*40),apple.getPrice(6));
294          assertEquals(new Money(2*40),apple.getPrice(7));
295          assertEquals(new Money(3*40),apple.getPrice(8));
296          assertEquals(new Money(4*40),apple.getPrice(9));
297          assertEquals(new Money(4*40),apple.getPrice(10));
298          assertEquals(new Money(4*40),apple.getPrice(11));
299          assertEquals(new Money(4*40),apple.getPrice(12));
300          assertEquals(new Money(4*40),apple.getPrice(13));
301          assertEquals(new Money(4*40),apple.getPrice(14));
302          assertEquals(new Money(5*40),apple.getPrice(15));
303      }
304 }