blob: 6b3a5395d3bcedb5479713a7b405c35bfd119614 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.functor.example.kata.one;
import static org.junit.Assert.assertEquals;
import org.apache.commons.functor.Function;
import org.apache.commons.functor.adapter.BinaryFunctionFunction;
import org.apache.commons.functor.core.Identity;
import org.apache.commons.functor.core.comparator.IsGreaterThan;
import org.apache.commons.functor.core.composite.Composite;
import org.apache.commons.functor.core.composite.ConditionalFunction;
import org.apache.commons.functor.core.composite.CompositeBinaryFunction;
import org.junit.Test;
/**
* Dave Thomas's Kata One asks us to think about how one might implement pricing rules:
*
* "Some things in supermarkets have simple prices: this can of beans costs $0.65. Other things have more complex
* prices. For example:
*
* o three for a dollar (so what?s the price if I buy 4, or 5?)
*
* o $1.99/pound (so what does 4 ounces cost?)
*
* o buy two, get one free (so does the third item have a price?)"
*
* Functors provide one approach to this sort of problem, and in this example we'll demonstrate some simple cases.
*
* See http://pragprog.com/pragdave/Practices/Kata/KataOne.rdoc,v for more information on this Kata.
*
* @version $Revision$ $Date$
*/
public class SupermarketPricingExample {
// tests
// ----------------------------------------------------------
/*
* The simplest form of pricing is simply a constant rate. In Dave's example, a can of beans costs $0.65, and n cans
* of beans cost n*0.65.
*
* This pricing rule simply multiplies the quantity by a constant, e.g.: ToMoney.from(Multiply.by(65))
*
* This case is so common, we may want to introduce a special Product constructor to wrap up create the functors for
* us.
*/
@Test
public void testConstantPricePerUnit() throws Exception {
{
Product beans = new Product("Can of Beans", "SKU-0001", ToMoney.from(Multiply.by(65)));
assertEquals(new Money(0 * 65), beans.getPrice(0));
assertEquals(new Money(1 * 65), beans.getPrice(1));
assertEquals(new Money(2 * 65), beans.getPrice(2));
assertEquals(new Money(3 * 65), beans.getPrice(3));
}
// or, using the speical constructor:
{
Product beans = new Product("Can of Beans", "SKU-0001", 65);
assertEquals(new Money(0 * 65), beans.getPrice(0));
assertEquals(new Money(1 * 65), beans.getPrice(1));
assertEquals(new Money(2 * 65), beans.getPrice(2));
assertEquals(new Money(3 * 65), beans.getPrice(3));
}
}
/*
* A slighly more complicated example is a bulk discount. For example, bananas may be $0.33 cents each, or 4 for a
* dollar ($1.00).
*
* This rule is underspecified by itself, there are at least two ways to interpret this pricing rule:
*
* a) the cost is $0.33 cents for 3 or fewer, $0.25 for 4 or more
*
* or
*
* b) the cost is $1.00 for every group of 4, $0.33 each for anything left over
*
* although I think in practice, "4 for a dollar" usually means the former and not the latter.
*
* We can implement either:
*/
@Test
public void testFourForADollar_A() throws Exception {
Product banana =
new Product("Banana", "SKU-0002", ToMoney.from(new ConditionalFunction<Integer, Number>(IsGreaterThan
.instance(Integer.valueOf(3)), Multiply.by(25), Multiply.by(33))));
assertEquals(new Money(0 * 33), banana.getPrice(0));
assertEquals(new Money(1 * 33), banana.getPrice(1));
assertEquals(new Money(2 * 33), banana.getPrice(2));
assertEquals(new Money(3 * 33), banana.getPrice(3));
assertEquals(new Money(4 * 25), banana.getPrice(4));
assertEquals(new Money(5 * 25), banana.getPrice(5));
assertEquals(new Money(6 * 25), banana.getPrice(6));
assertEquals(new Money(7 * 25), banana.getPrice(7));
assertEquals(new Money(8 * 25), banana.getPrice(8));
}
@Test
public void testFourForADollar_B() throws Exception {
Product banana =
new Product("Banana", "SKU-0002", ToMoney.from(new BinaryFunctionFunction<Integer, Number>(
new CompositeBinaryFunction<Integer, Integer, Number>(Add.instance(), Composite.function(
Multiply.by(100), Divide.by(4)), Composite.function(Multiply.by(33), Mod.by(4))))));
assertEquals(new Money(0 * 33 + 0 * 25), banana.getPrice(0));
assertEquals(new Money(1 * 33 + 0 * 25), banana.getPrice(1));
assertEquals(new Money(2 * 33 + 0 * 25), banana.getPrice(2));
assertEquals(new Money(3 * 33 + 0 * 25), banana.getPrice(3));
assertEquals(new Money(0 * 33 + 4 * 25), banana.getPrice(4));
assertEquals(new Money(1 * 33 + 4 * 25), banana.getPrice(5));
assertEquals(new Money(2 * 33 + 4 * 25), banana.getPrice(6));
assertEquals(new Money(3 * 33 + 4 * 25), banana.getPrice(7));
assertEquals(new Money(0 * 33 + 8 * 25), banana.getPrice(8));
}
/*
* Another interesting pricing rule is something like "buy 2, get 1 free".
*
* This may be implemented using a formula like: costPerUnit * (quantity - quantity / 2)
*
* For example...
*/
@Test
public void testBuyTwoGetOneFree_1() throws Exception {
Product apple =
new Product("Apple", "SKU-0003", ToMoney.from(Composite.function(Multiply.by(40), BinaryFunctionFunction
.adapt(new CompositeBinaryFunction<Number, Number, Number>(Subtract.instance(), new Identity<Number>(),
Divide.by(3))))));
assertEquals(new Money(0 * 40), apple.getPrice(0));
assertEquals(new Money(1 * 40), apple.getPrice(1));
assertEquals(new Money(2 * 40), apple.getPrice(2));
assertEquals(new Money(2 * 40), apple.getPrice(3));
assertEquals(new Money(3 * 40), apple.getPrice(4));
assertEquals(new Money(4 * 40), apple.getPrice(5));
assertEquals(new Money(4 * 40), apple.getPrice(6));
assertEquals(new Money(5 * 40), apple.getPrice(7));
assertEquals(new Money(6 * 40), apple.getPrice(8));
assertEquals(new Money(6 * 40), apple.getPrice(9));
assertEquals(new Money(7 * 40), apple.getPrice(10));
}
/*
* ...but our pricing rule is starting to get ugly, and we haven't even considered things something like
* "buy 3, get 2 free", etc.
*
* Perhaps a special Function instance is in order:
*/
class BuyNGetMFree implements Function<Number, Number> {
public BuyNGetMFree(int n, int m, int costPerUnit) {
this.n = n;
this.m = m;
this.costPerUnit = costPerUnit;
}
public Number evaluate(Number num) {
int quantity = num.intValue();
int cost = 0;
while (quantity >= n) {
// buy n
cost += n * costPerUnit;
quantity -= n;
// get m (or fewer) free
quantity -= Math.min(quantity, m);
}
// buy less than n
cost += quantity * costPerUnit;
return Integer.valueOf(cost);
}
private int n, m, costPerUnit;
}
@Test
public void testBuyTwoGetOneFree_2() throws Exception {
Product apple = new Product("Apple", "SKU-0003", ToMoney.from(new BuyNGetMFree(2, 1, 40)));
assertEquals(new Money(0 * 40), apple.getPrice(0));
assertEquals(new Money(1 * 40), apple.getPrice(1));
assertEquals(new Money(2 * 40), apple.getPrice(2));
assertEquals(new Money(2 * 40), apple.getPrice(3));
assertEquals(new Money(3 * 40), apple.getPrice(4));
assertEquals(new Money(4 * 40), apple.getPrice(5));
assertEquals(new Money(4 * 40), apple.getPrice(6));
assertEquals(new Money(5 * 40), apple.getPrice(7));
assertEquals(new Money(6 * 40), apple.getPrice(8));
assertEquals(new Money(6 * 40), apple.getPrice(9));
assertEquals(new Money(7 * 40), apple.getPrice(10));
}
@Test
public void testBuyThreeGetTwoFree() throws Exception {
Product apple = new Product("Apple", "SKU-0003", ToMoney.from(new BuyNGetMFree(3, 2, 40)));
assertEquals(new Money(0 * 40), apple.getPrice(0));
assertEquals(new Money(1 * 40), apple.getPrice(1));
assertEquals(new Money(2 * 40), apple.getPrice(2));
assertEquals(new Money(3 * 40), apple.getPrice(3));
assertEquals(new Money(3 * 40), apple.getPrice(4));
assertEquals(new Money(3 * 40), apple.getPrice(5));
assertEquals(new Money(4 * 40), apple.getPrice(6));
assertEquals(new Money(5 * 40), apple.getPrice(7));
assertEquals(new Money(6 * 40), apple.getPrice(8));
assertEquals(new Money(6 * 40), apple.getPrice(9));
assertEquals(new Money(6 * 40), apple.getPrice(10));
assertEquals(new Money(7 * 40), apple.getPrice(11));
}
@Test
public void testBuyTwoGetFiveFree() throws Exception {
Product apple = new Product("Apple", "SKU-0003", ToMoney.from(new BuyNGetMFree(2, 5, 40)));
assertEquals(new Money(0 * 40), apple.getPrice(0));
assertEquals(new Money(1 * 40), apple.getPrice(1));
assertEquals(new Money(2 * 40), apple.getPrice(2));
assertEquals(new Money(2 * 40), apple.getPrice(3));
assertEquals(new Money(2 * 40), apple.getPrice(4));
assertEquals(new Money(2 * 40), apple.getPrice(5));
assertEquals(new Money(2 * 40), apple.getPrice(6));
assertEquals(new Money(2 * 40), apple.getPrice(7));
assertEquals(new Money(3 * 40), apple.getPrice(8));
assertEquals(new Money(4 * 40), apple.getPrice(9));
assertEquals(new Money(4 * 40), apple.getPrice(10));
assertEquals(new Money(4 * 40), apple.getPrice(11));
assertEquals(new Money(4 * 40), apple.getPrice(12));
assertEquals(new Money(4 * 40), apple.getPrice(13));
assertEquals(new Money(4 * 40), apple.getPrice(14));
assertEquals(new Money(5 * 40), apple.getPrice(15));
}
}