# Task
Follow the instructions in each failing test case to write logic that calculates the total price when ringing items up at a cash register.

# Purpose
Practice writing maintainable and extendable code. 

# Intent
This kata is meant to emulate the real world where requirements change over time. This kata does not provide a specification for the final end-state up front. Instead, it walks you through a series of requirements, modifying and extending them via test cases as you go. This kata is about the journey, not the destination. Your ability to write maintainable and extendable code will affect how difficult this kata is. 

# Provided Utility Function
You may use the following preloaded function:
```python
get_price(item)
"""Accepts a str specifying an item. Returns the price of
the item (float) or raises a KeyError if no price is available.

Example: 

    >>> get_price(apple)
    0.4
"""
```

# Acknowledgement
Inspired by http://codekata.com/kata/kata01-supermarket-pricing/. To get the most benefit from this kata, I would recommend visiting that page only after completing, not before.

Source: codewars | Difficulty: EASY

Starter code: def __init__(self, d={}):
	

Solutions:
import re

class Checkout(object):

	def __init__(self, d={}):
		(self.pricing, self.total, self.fruits, self.free) = (d, 0, {}, {})

	def scan(self, n, qty=1):
		item = get_price(n)
		for i in range(qty):
			if not self.free.get(n, 0):
				self.total += item
				self.fruits[n] = self.fruits.get(n, 0) + 1
			else:
				self.free[n] -= 1
				continue
			if n in self.pricing:
				m = self.pricing[n]
				if 'for' in m:
					(how, f) = m.split('for')
					if not self.fruits[n] % int(how):
						self.total = self.total - item * int(how) + float(f)
				elif 'buy' in m:
					(how, f) = re.sub('buy|get', ' ', m).strip().split()
					if not self.fruits[n] % int(how):
						self.free[n] = int(f)
				elif 'off' in m:
					(how, f) = re.sub('off|ormore', ' ', m).strip().split()
					if self.fruits[n] == int(f):
						self.total -= float(how)
			self.total = round(self.total, 2)


from collections import defaultdict

class Checkout(object):
	prices = {'onion': 0.99, 'leek': 2.99, 'apple': 0.4, 'orange': 0.75, 'tart': 5.0, 'potato': 0.8, 'rice': 1.1, 'carrot': 0.25, 'celery': 1.2}

	def __init__(self, sale=None):
		self.sale = sale if sale else {}
		self.count = defaultdict(int)

	def scan(self, s, x=1):
		self.count[s] += x

	@property
	def total(self):
		res = 0
		for (k, v) in self.count.items():
			s = self.sale.get(k, '')
			if 'for' in s:
				(x, y) = map(float, s.split('for'))
				(q, v) = divmod(v, x)
				res += q * y
			elif 'buy' in s:
				(x, y) = map(int, s[3:].split('get'))
				v -= v // (x + y)
			elif 'off' in s:
				(x, y) = map(float, s[:-6].split('off'))
				if v >= y:
					res -= x
			res += v * Checkout.prices[k]
		return res


from collections import Counter
from re import compile, fullmatch

class Checkout:
	offers = {compile('(\\d+)for([.\\d]+)'): lambda m: lambda qty, price: qty // int(m[1]) * float(m[2]) + qty % int(m[1]) * price, compile('buy(\\d+)get(\\d+)'): lambda m: lambda qty, price: (qty // (int(m[1]) + int(m[2])) * int(m[1]) + qty % (int(m[1]) + int(m[2]))) * price, compile('([.\\d]+)off(\\d+)ormore'): lambda m: lambda qty, price: qty * price - float(m[1]) * (qty >= int(m[2]))}

	def _parse_sale(self, sale):
		for (offer, func) in list(self.offers.items()):
			match = offer.fullmatch(sale)
			if match:
				return func(match)

	def __init__(self, sales=None):
		self.products = Counter()
		self.sales = {product: self._parse_sale(sale) for (product, sale) in list((sales or {}).items())}

	def scan(self, product, quantity=1):
		self.products[product] += quantity

	def calculate_cost(self, product):
		try:
			return self.sales[product](self.products[product], get_price(product))
		except KeyError:
			return self.products[product] * get_price(product)

	@property
	def total(self):
		return sum((self.calculate_cost(p) for p in self.products))


import re

class Checkout(object):
	ip = {'onion': 0.99, 'leek': 2.99, 'apple': 0.4, 'orange': 0.75, 'tart': 5, 'potato': 0.8, 'rice': 1.1, 'carrot': 0.25, 'celery': 1.2}

	def __init__(self, promos={}):
		self.ci = []
		self.promo_ci = []
		self.ormore = []
		self.promos = promos
		print(promos)

	def extract_quant_doll(self, promo):
		(x, y) = (promo.split('for')[0], promo.split('for')[-1])
		try:
			x = int(x)
			y = float(y)
			return (x, y, 'for')
		except:
			try:
				spromo = promo.split('buy')[1]
				spromo = spromo.split('get')
				(x, y) = (spromo[0], spromo[-1])
				if x.isdigit() and y.isdigit():
					return (int(x), int(y), 'buy')
			except:
				spromo = promo.split('off')
				x = float(spromo[0])
				spromo = spromo[-1]
				y = spromo.split('ormore')[0]
				y = int(y)
				return (y, x, 'ormore')

	def scan(self, item, nitems=1):
		print((item, nitems))
		price = self.get_price(item)
		i = 0
		while i < nitems:
			i += 1
			print((i, nitems))
			self.ci.append((item, price))
			if item in self.promos:
				xfory = self.promos[item]
				(quant, doll, type) = self.extract_quant_doll(xfory)
				if type == 'for':
					count_items = len([x for x in self.ci if x[0] == item])
					print(('for', quant, doll, item, self.ci, count_items))
					if quant == count_items:
						self.ci = [x for x in self.ci if x[0] != item]
						self.promo_ci.append((item, doll))
				elif type == 'buy':
					items_sofar = [x for x in self.ci if x[0] == item]
					price_total = sum([x[1] for x in items_sofar])
					count_items = len(items_sofar)
					print(('get', quant, doll, item, self.ci, count_items))
					if quant + doll == count_items:
						self.ci = [x for x in self.ci if x[0] != item]
						self.promo_ci.append((item, quant * self.get_price(item)))
				elif type == 'ormore':
					if item not in self.ormore:
						items_sofar = [x for x in self.ci if x[0] == item]
						price_total = sum([x[1] for x in items_sofar])
						count_items = len(items_sofar)
						print(('ormore', quant, doll, item, self.ci, count_items))
						if count_items >= quant:
							self.promo_ci.append((item, -doll))
							self.ormore.append(item)

	def get_price(self, item):
		return self.ip.get(item, 0)

	@property
	def total(self):
		total_prices = [x[1] for x in self.ci]
		total_promos = [x[1] for x in self.promo_ci]
		print((self.ci, self.promo_ci))
		total_sum = sum(total_prices) if self.ci != [] else 0.0
		total_sum += sum(total_promos) if self.promo_ci != [] else 0.0
		return total_sum


from collections import Counter
import re

class Checkout(object):

	def __init__(self, offers={}):
		self.purchased = Counter()
		self.offers = {'for': {}, 'get': {}, 'off': {}}
		for (k, v) in offers.items():
			if 'for' in v:
				(qty, price) = v.split('for')
				self.offers['for'][k] = (int(qty), float(price))
			if 'get' in v:
				r = re.findall('\\d+', v)
				self.offers['get'][k] = (int(r[0]), int(r[1]))
			if 'off' in v:
				r = re.findall('\\d+\\.\\d+|\\d+', v)
				self.offers['off'][k] = (float(r[0]), int(r[1]))

	@property
	def total(self):
		ans = 0.0
		for (k, v) in self.purchased.items():
			if k in self.offers['for']:
				(q, r) = divmod(v, self.offers['for'][k][0])
				ans += self.offers['for'][k][1] * q
				v = r
			if k in self.offers['get']:
				(q, r) = divmod(v, sum(self.offers['get'][k]))
				ans += get_price(k) * q * self.offers['get'][k][0]
				v = r
			if k in self.offers['off']:
				if v >= self.offers['off'][k][1]:
					ans -= self.offers['off'][k][0]
			ans += get_price(k) * v
		return ans

	def scan(self, item, qty=1):
		self.purchased[item] += qty


import re

class Checkout(object):

	def __init__(self, sales={}):
		self.total = 0.0
		self.items = {}
		self.sales = {}
		self.buyget = {}
		self.off = {}
		for (k, v) in sales.items():
			if 'for' in v:
				(x, y) = v.split('for')
				self.sales[k] = (int(x), float(y))
			elif 'buy' in v:
				(x, y) = v.split('get')
				self.buyget[k] = (int(x.replace('buy', '')), int(y))
			elif 'off' in v:
				(x, y) = v.split('off')
				self.off[k] = (float(x), int(y.replace('ormore', '')))

	def scan(self, item, q=1):
		self.items[item] = self.items.get(item, 0) + q
		self.total += get_price(item) * q
		if item in self.sales:
			if self.sales[item][0] <= self.items[item]:
				x = self.items[item] // self.sales[item][0]
				self.total += x * self.sales[item][1] - x * self.sales[item][0] * get_price(item)
				self.items[item] %= self.sales[item][0]
		elif item in self.buyget:
			(x, y) = self.buyget[item]
			if self.items[item] >= x + y:
				z = self.items[item] // (x + y)
				self.total -= z * y * get_price(item)
			self.items[item] %= x + y
		elif item in self.off:
			if self.items[item] == self.off[item][1]:
				self.total -= self.off[item][0]
		self.total = round(self.total, 2)


class Checkout(object):

	def __init__(self, sale_codes={}):
		self.total = 0
		self.scanned_items = {}
		self.sale_codes = sale_codes

	def update_total(self):
		tot = 0
		for (item, count) in list(self.scanned_items.items()):
			if item in self.sale_codes:
				if self.sale_codes[item].startswith('buy'):
					(buy, get) = [int(x) for x in self.sale_codes[item][3:].split('get')]
					tot += count // (buy + get) * (buy * get_price(item))
					tot += count % (buy + get) * get_price(item)
				elif 'for' in self.sale_codes[item]:
					(sale_count, sale_for) = [float(x) for x in self.sale_codes[item].split('for')]
					tot += count // sale_count * sale_for
					tot += count % sale_count * get_price(item)
				elif 'ormore' in self.sale_codes[item]:
					(price_off, min_count) = [float(x) for x in self.sale_codes[item][:-6].split('off')]
					tot += count * get_price(item)
					if count >= min_count:
						tot += -price_off
			else:
				tot += get_price(item) * count
		self.total = tot

	def scan(self, item, quantity=1):
		if item not in self.scanned_items:
			self.scanned_items[item] = quantity
		else:
			self.scanned_items[item] += quantity
		self.update_total()


class Checkout(object):

	def __init__(self, *args):
		self._sales = list(args)[0] if len(args) else {}
		self._total = 0.0
		self._basket = {}

	def scan(self, prod, count=1):
		self._basket[prod] = self._basket.get(prod, 0) + count
		self.calc_total()

	@property
	def total(self):
		for prod in self._basket:
			if 'off' in self._sales.get(prod, ''):
				(x, y) = self._sales[prod].replace('ormore', '').split('off')
				if self._basket[prod] >= int(y):
					self._total -= float(x)
		return self._total

	def calc_total(self):
		self._total = 0.0
		for prod in self._basket:
			count = self._basket[prod]
			if 'for' in self._sales.get(prod, ''):
				(x, y) = self._sales[prod].split('for')
				self._total += count // int(x) * float(y)
				count = count % int(x)
			elif 'get' in self._sales.get(prod, ''):
				(x, y) = self._sales[prod].replace('buy', '').split('get')
				self._total += count // (int(x) + int(y)) * int(x) * get_price(prod)
				count = count % (int(x) + int(y))
			self._total += count * get_price(prod)


class Checkout:
	items = {'onion': 0.99, 'leek': 2.99, 'apple': 0.4, 'orange': 0.75, 'tart': 5.0, 'potato': 0.8, 'rice': 1.1, 'carrot': 0.25, 'celery': 1.2}

	def __init__(self, deals=None):
		self.scanned = []
		self.deals = deals or {}

	def scan(self, item, count=1):
		self.scanned.extend([item] * count)

	@property
	def total(self):
		total = 0
		for (item, deal) in self.deals.items():
			if item in self.scanned:
				item_count = self.scanned.count(item)
				if 'for' in deal:
					(count, price) = deal.split('for')
					(a, b) = divmod(item_count, int(count))
					total += a * float(price) + b * self.items[item]
				elif 'buy' in deal:
					(count, free) = deal[3:].split('get')
					(a, b) = divmod(item_count, int(count) + int(free))
					total += (item_count - a * int(free)) * self.items[item]
				elif 'off' in deal:
					(discount, threshold) = deal[:-6].split('off')
					total += item_count * self.items[item]
					if item_count >= int(threshold):
						total -= float(discount)
		total += sum((self.items[i] for i in self.scanned if i not in self.deals))
		return total


from collections import defaultdict
import re

class Checkout:

	def __init__(self, deals={}):
		self.bought = defaultdict(int)
		self.deals = deals

	def scan(self, s, n=1):
		self.bought[s] += n

	@property
	def total(self):

		def calculate_price(k, v, deal):
			if deal == None:
				return get_price(k) * v
			elif re.match('^([\\d\\.]+)for([\\d\\.]+)$', deal):
				(c, d) = re.match('^([\\d\\.]+)for([\\d\\.]+)$', deal).groups()
				return float(d) * (v // int(c)) + get_price(k) * (v % int(c))
			elif re.match('^buy([\\d\\.]+)get([\\d\\.]+)$', deal):
				(f, t) = re.match('^buy([\\d\\.]+)get([\\d\\.]+)$', deal).groups()
				return get_price(k) * (v // (int(f) + int(t)) * int(f) + v % (int(f) + int(t)))
			elif re.match('^([\\d\\.]+)off([\\d\\.]+)ormore$', deal):
				(d, c) = re.match('^([\\d\\.]+)off([\\d\\.]+)ormore$', deal).groups()
				return get_price(k) * v - float(d) * (v >= int(c))
			else:
				return get_price(k) * v
		return sum((calculate_price(k, v, self.deals.get(k, None)) for (k, v) in self.bought.items()))