# 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.

Difficulty: introductory

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(r'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(r'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.40,
              "orange":0.75, "tart":5.00, "potato":0.80,
              "rice":1.10, "carrot":0.25, "celery":1.20}
    
    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)


# A blank slate
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]
                      #print('updated', self.ci)
                      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]
                      #print('updated', self.ci)
                      self.promo_ci.append((item, quant*self.get_price(item)))
                      #self.free.append(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.ci = [x for x in self.ci if x[0]!=item]
                          #print('updated', self.ci)
                          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(r'\d+', v)
                self.offers['get'][k] = (int(r[0]), int(r[1]))
            if 'off' in v:
                r = re.findall(r'\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):
        #Implement a "XoffYormore"
        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]            
            #Implement a "XforY"
            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)
            #Implement a "buyXgetY"
            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)

# A blank slate
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(r'^([\d\.]+)for([\d\.]+)$',deal):
                c,d = re.match(r'^([\d\.]+)for([\d\.]+)$',deal).groups()
                return float(d)*(v//int(c)) + get_price(k)*(v%int(c))
            elif re.match(r'^buy([\d\.]+)get([\d\.]+)$',deal):
                f,t = re.match(r'^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(r'^([\d\.]+)off([\d\.]+)ormore$',deal):
                d,c = re.match(r'^([\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())