Skip to content

Instantly share code, notes, and snippets.

@Lewiscowles1986
Last active August 10, 2016 08:43
Show Gist options
  • Save Lewiscowles1986/a7644e0cf7fd2671ba5b9e8b1f335531 to your computer and use it in GitHub Desktop.
Save Lewiscowles1986/a7644e0cf7fd2671ba5b9e8b1f335531 to your computer and use it in GitHub Desktop.
interview example in python

So yesterday I choked for no great reason at interview demonstrating my python knowledge. There was a test to implement a checkout and item in an hour with different discounts for different items. Annoyed by my own performance I took some time last night to revisit the problem space...

To be honest I'm still not sure if the offers should exist on items or be registered with the checkout (and therefore not need a item-code or item-code check)

Items could accept an offer at construct time and have methods to override assignment. I am sure offers should be a separate thing to an item, but an item with an offer could simply have its line total overridden if an offer existed.

If this were the case, then should the relationship be singular (would make things easy), or multiple? I am quite sure that having special item classes is a bad idea. There is an example of the objects own linetotal being overridden in a child class, having logic entirely contained within the objects.

I also wanted to experiment with different testing styles, so I built some doctests into the discounts

Setting up environment

pip install virtualenv
virtualenv .venv
source .venv/bin/activate

When done

pip freeze > requirements.txt

Editing

atom .

Testing

python3 tests.py
python3 discounts.py -v
import copy
from items import CheckoutItem
from discounts import ItemDiscount
class Checkout(object):
def __init__(self):
self.items = {}
self.offers = []
def addItem(self, item: CheckoutItem):
item_copy = copy.copy(item) ## I'd really like to pass by value
if item_copy.code in self.items:
self.items[item_copy.code].qty += item_copy.qty
else:
self.items[item_copy.code] = item_copy
def registerOffer(self, offer: ItemDiscount):
offer_copy = copy.copy(offer) ## I'd really like to pass by value
self.offers.append(offer_copy)
def total(self):
total = 0.0
for code in self.items:
if len(self.offers) > 0:
"""
Here we use the logic the greatest discount available will be
the one we apply for the customer, they don't stack,
potentially this could be avoided by constraining the storage
of discounts, preventing more than one offer per product, but
that would then interfere with general behaviour of offers
"""
subtotal = self.items[code].linetotal()
for offer in self.offers:
subtotal = min(offer.calc(self.items[code]), subtotal)
total += subtotal
else:
total += self.items[code].linetotal()
return total
import math
import copy
from items import CheckoutItem
class ItemDiscount(object):
"""Object containing structure for serialized discount
"""
op = "price"
amount = 0.0
min = 0.0
code = None
limit = 0
def __init__(self, op: str="price", amount: float=0.0,
min: float=0.0, code: str=None, limit: int=0):
self.op = op
self.amount = amount
self.min = min
self.code = code
self.limit = 0
def calc(self, anitem: CheckoutItem):
"""Calculate discount on checkout-line-item based upon state of object
If no code is supplied
provided minimum value is greater than or equal to threshold, apply
valid discount-code to every item
>>> discount = ItemDiscount("price_abs", .99)
>>> discount.calc(CheckoutItem("WHATEVERIWANT", qty=5, price=4.99))
20.0
If code is supplied
provided minimum value is greater than or equal to threshold, apply
valid discount-code to only item with matching code
>>> discount = ItemDiscount("price_abs", .99, code="ITEM")
>>> discount.calc(CheckoutItem("ITEM", qty=5, price=4.99))
20.0
>>> discount.calc(CheckoutItem("ITEM2", qty=2, price=7))
14.0
so long as no code is supplied or if code matches item code
we can set percentage based discount to line items
>>> discount = ItemDiscount("price", .90)
>>> discount.calc(CheckoutItem("ITEM", qty=3, price=5))
13.5
we can offer quantity based discounts
buy one get one free
>>> discount = ItemDiscount("qty_bulkdiscount", amount=2)
>>> discount.calc(CheckoutItem("ITEM", qty=4, price=5))
10.0
>>> discount.calc(CheckoutItem("ITEM", qty=3, price=5))
10.0
>>> discount.calc(CheckoutItem("ITEM", qty=2, price=5))
5.0
up-to-three for the price of one
>>> discount = ItemDiscount("qty_bulkdiscount", 3)
>>> discount.calc(CheckoutItem("ITEM", qty=1, price=10))
10.0
>>> discount.calc(CheckoutItem("ITEM", qty=2, price=10))
10.0
>>> discount.calc(CheckoutItem("ITEM", qty=3, price=10))
10.0
we can deduct an absolute value from line-total based upon linetotal
value
>>> discount = ItemDiscount("linetotal_abs", amount=2, min=10)
>>> discount.calc(CheckoutItem("ITEM", qty=2, price=5))
8.0
we can deduct an absolute value from line-total based upon quantity
>>> discount = ItemDiscount("linetotal_abs_qty", amount=3.5, min=2)
>>> discount.calc(CheckoutItem("ITEM", qty=2, price=10))
16.5
"""
item = copy.copy(anitem) ## I'd really like to pass by value
codeMatch = bool(item.code == self.code or self.code is None)
qtyMatch = bool(item.qty >= int(self.min))
# per-line-item discount
if codeMatch and qtyMatch:
if self.op == "price":
item.price *= self.amount
elif self.op == "price_abs":
item.price -= self.amount
elif self.op == "qty_bulkdiscount":
item.qty = math.ceil(float(item.qty) / self.amount)
total = item.linetotal() # always need the line-item total
# special cases
if self.op == "buy_2_get_3rd_half_price":
raise NotImplemented("Sorry pet, this one twisted me melon")
totalMatch = bool(total >= self.min)
# line-item sub-total discounts
if codeMatch:
# Handle sub-total discounts
if self.op == "linetotal_abs_qty" and qtyMatch:
total -= self.amount
elif self.op == "linetotal_abs" and totalMatch:
total -= self.amount
return total
if __name__ == "__main__":
import doctest
doctest.testmod()
import math
class CheckoutItem(object):
def __init__(self, code: str, qty: int=1, price: float=1.0):
self.code = code
self.qty = qty
self.price = price
def linetotal(self):
return float(self.qty) * float(self.price)
def __repr__(self):
return "Item: {0}, qty: {1}, price: {2}".format(self.code, self.qty,
self.price)
class Strawberry(CheckoutItem):
def __init__(self, qty: int=1):
self.code = "SB1"
self.price = 5.00
self.offer_price = 4.5
self.qty = qty
def linetotal(self):
if self.qty >= 3:
return float(self.offer_price) * float(self.qty)
return float(self.price) * float(self.qty)
class FruitTea(CheckoutItem):
def __init__(self, qty: int=1):
self.code = "FT1"
self.price = 4.00
self.qty = qty
def linetotal(self):
return float(self.price) * math.ceil(float(self.qty) / 2.0)
class Coffee(CheckoutItem):
def __init__(self, qty: int=1):
self.code = "CF1"
self.price = 2.5
self.qty = qty
import unittest
import random
from checkout import *
from items import *
from discounts import *
class DiscountTests(unittest.TestCase):
def testDiscountOnNonCheckoutItem(self):
discount = ItemDiscount(amount=4.00, op="price_abs")
self.assertRaises(AttributeError, lambda: discount.calc("string"))
def testDiscountWithNoMinOrCodeAlwaysApplied(self):
item = CheckoutItem("CODE", 1, 5.00)
discount = ItemDiscount(amount=4.00, op="price_abs")
self.assertNotEqual(item.linetotal(), discount.calc(item))
def testDiscountAppliedToAnyItemIfNoCode(self):
item1 = CheckoutItem("ITEM1", 1, 5.00)
item2 = CheckoutItem("ITEM2", 1, 5.00)
discount = ItemDiscount(amount=4.00, op="price_abs")
self.assertNotEqual(item1.linetotal(), discount.calc(item1))
self.assertNotEqual(item2.linetotal(), discount.calc(item2))
def testDiscountAppliedToSpecificItemWhenCodeSupplied(self):
item1 = CheckoutItem("ITEM1", 1, 5.00)
item2 = CheckoutItem("ITEM12", 1, 5.00)
discount = ItemDiscount(amount=.9, code="ITEM1")
self.assertNotEqual(item1.linetotal(), discount.calc(item1))
self.assertEqual(item2.linetotal(), discount.calc(item2))
def testDiscountHasNoEffectsOnItemPassed(self):
item = CheckoutItem("CODE", 1, 5.00)
discount = ItemDiscount(amount=.9)
linetotal_original = item.linetotal()
discount_total = discount.calc(item)
linetotal_after_discount = item.linetotal()
self.assertEqual(linetotal_original, linetotal_after_discount)
def testDiscountOnQuantityApplied(self):
item = CheckoutItem("CODE", 2, 5.00)
discount = ItemDiscount(amount=2.00, op="qty_bulkdiscount")
self.assertEqual(item.price, discount.calc(item))
def testDiscountOnQuantityForSpecificItemCodeApplied(self):
item1 = CheckoutItem("ITEM1", 3, 5.00)
item2 = CheckoutItem("ITEM2", 4, 5.00)
discount = ItemDiscount(amount=2, code="ITEM1",
op="qty_bulkdiscount")
self.assertEqual(item1.price*2, discount.calc(item1))
self.assertNotEqual(item1.linetotal(), discount.calc(item1))
self.assertEqual(item2.linetotal(), discount.calc(item2))
def testDiscountPerItemPricePercent(self):
item = CheckoutItem("CODE", 1, 5.00)
discount = ItemDiscount(amount=.9)
self.assertNotEqual(item.linetotal(), discount.calc(item))
self.assertEqual(item.linetotal()*.9, discount.calc(item))
def testDiscountPerItemPriceAbsolute(self):
item = CheckoutItem("CODE", 4, 5.00)
discount = ItemDiscount(amount=1, op="price_abs")
self.assertNotEqual(item.linetotal(), discount.calc(item))
self.assertEqual(item.linetotal()-4, discount.calc(item))
class InterviewTests(unittest.TestCase):
def setUp(self):
self.checkout = Checkout()
def testAddSingleStrawberryItemCorrectTotal(self):
item = Strawberry()
self.checkout.addItem(item)
self.assertEqual(item.price, self.checkout.total())
def testAddSingleItemCorrectTotal(self):
item = CheckoutItem(code="ST1", price=5)
self.checkout.registerOffer(ItemDiscount(code="ST1", amount=.5, min=3,
op="price_abs"))
self.checkout.addItem(item)
self.assertEqual(item.price, self.checkout.total())
self.checkout.addItem(item)
self.checkout.addItem(item)
self.assertEqual(13.5, self.checkout.total())
def testMultiDiscountLeadsToLowestTotal(self):
# setup items
item1 = CheckoutItem(code="ST1", price=5)
item2 = CheckoutItem(code="FT1", price=2.5)
# setup competing offers
self.checkout.registerOffer(ItemDiscount(code="ST1", amount=.8, min=2,
op="price"))
self.checkout.registerOffer(ItemDiscount(code="ST1", amount=.5, min=3,
op="price_abs"))
# initial setup and checks
self.checkout.addItem(item1)
self.assertEqual(item1.price, self.checkout.total())
self.checkout.addItem(item1)
self.checkout.addItem(item1)
# verify cart total
self.assertEqual(12, self.checkout.total())
# start with new checkout object to confirm same result
self.checkout = Checkout()
self.checkout.registerOffer(ItemDiscount(code="ST1", amount=.8, min=2,
op="price"))
self.checkout.addItem(item1)
self.checkout.addItem(item1)
self.checkout.addItem(item1)
self.assertEqual(12, self.checkout.total())
def testAddSingleFruitTeaItemCorrectTotal(self):
item = FruitTea()
self.checkout.addItem(item)
self.assertEqual(item.price, self.checkout.total())
def testAddSingleCoffeeItemCorrectTotal(self):
item = Coffee()
self.checkout.addItem(item)
self.assertEqual(item.price, self.checkout.total())
def testAddMultipleStrawberrySingleObjCorrectTotal(self):
item = Strawberry(2)
self.checkout.addItem(item)
self.assertEqual((item.price * 2), self.checkout.total())
def testAddMultipleStrawberryMultiObjCorrectTotal(self):
item = Strawberry(2)
self.checkout.addItem(item)
self.checkout.addItem(Strawberry())
self.checkout.addItem(Strawberry())
self.assertEqual((item.offer_price * 4), self.checkout.total())
def testAddMultipleStrawberryOfferCorrectTotal(self):
item = Strawberry(3)
self.checkout.addItem(item)
self.assertEqual((item.offer_price * 3), self.checkout.total())
def testAddSingleFruitTeaItemCorrectTotal(self):
item = FruitTea()
self.checkout.addItem(item)
self.assertEqual(item.price, self.checkout.total())
def testAddMultiFruitTeaSingleObjCorrectTotalWithOffer(self):
item = FruitTea(2)
self.checkout.addItem(item)
self.assertEqual(item.price, self.checkout.total())
def testAddMultiFruitTeaMultiObjCorrectTotalWithOffer(self):
item = FruitTea(2)
self.checkout.addItem(item)
self.checkout.addItem(FruitTea())
self.assertEqual(item.price*2, self.checkout.total())
self.checkout.addItem(FruitTea())
self.assertEqual(item.price*2, self.checkout.total())
def testAddSingleCoffeeItemCorrectTotal(self):
item = Coffee()
self.checkout.addItem(item)
self.assertEqual(item.price, self.checkout.total())
def testInterview1(self):
strawberry = Strawberry()
fruit_tea = FruitTea()
coffee = Coffee()
self.checkout.addItem(strawberry)
self.checkout.addItem(fruit_tea)
self.checkout.addItem(strawberry)
self.checkout.addItem(fruit_tea)
self.checkout.addItem(fruit_tea)
self.checkout.addItem(coffee)
self.assertEqual(
(strawberry.price * 2) + (fruit_tea.price * 2) + (coffee.price),
self.checkout.total())
if __name__ == "__main__":
unittest.main(verbosity=2)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment