Last active
February 1, 2017 18:25
-
-
Save MarioBajr/139703d71a4a214d30f2b3104458b886 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//: Playground - noun: a place where people can play | |
import UIKit | |
// Helper | |
extension String { | |
func matchingStrings(regex: String) -> [[String]] { | |
guard let regex = try? NSRegularExpression(pattern: regex, options: []) else { return [] } | |
let nsString = self as NSString | |
let results = regex.matches(in: self, options: [], range: NSMakeRange(0, nsString.length)) | |
return results.map { result in | |
(0..<result.numberOfRanges).map { result.rangeAt($0).location != NSNotFound | |
? nsString.substring(with: result.rangeAt($0)) : "" | |
} | |
} | |
} | |
} | |
extension Float { | |
func format(_ f: String) -> String { | |
return String(format: "%\(f)f", self) | |
} | |
func roundFive() -> Float { | |
return ceil(Float(self) * 20) / 20 | |
} | |
} | |
// Domain | |
enum Category { | |
case Book | |
case Food | |
case Medical | |
case Goods | |
} | |
struct Product { | |
let quantity:Int | |
let packing:String? | |
let name:String | |
let category:Category | |
let price:Float | |
let imported:Bool | |
} | |
// Definitions | |
let categories:[String: Category] = ["book": .Book, | |
"chocolate bar": .Food, | |
"chocolates": .Food, | |
"headache pills": .Medical] | |
//Protocols | |
protocol ProductFactory { | |
associatedtype InputFormat | |
func buildProduct(from input:InputFormat) -> Product? | |
} | |
protocol TaxCalculator { | |
func tax(of product:Product) -> Float | |
} | |
// Logic | |
struct RegexBasedProductFactory:ProductFactory { | |
typealias InputFormat = String | |
private enum ProductParseKey { | |
case Quantity | |
case Name | |
case Price | |
case Imported | |
case Packing | |
} | |
private let patterns:[(String, [ProductParseKey])] = [ | |
("(\\d) ([\\w]+) of (imported )?([\\w\\s]+) at (\\d.+)", [.Quantity, .Packing, .Imported, .Name, .Price]), | |
("(\\d) (imported )?([\\w]+) of ([\\w\\s]+) at (\\d.+)", [.Quantity, .Imported, .Packing, .Name, .Price]), | |
("(\\d) ([\\w\\s]+) at (\\d.+)", [.Quantity, .Name, .Price]) | |
] | |
private let categories:[String:Category] | |
init(categories:[String:Category]) { | |
self.categories = categories | |
} | |
func buildProduct(from input:String) -> Product? { | |
var product:Product? | |
for (pattern, format) in self.patterns { | |
if let group = input.matchingStrings(regex: pattern).first, | |
group.count == format.count + 1 { | |
var quantity:Int? | |
var imported = false | |
var packing:String? | |
var name:String? | |
var price:Float? | |
for (index, key) in format.enumerated() { | |
let value = group[index+1] | |
switch key { | |
case .Imported: | |
imported = value.characters.count > 0 | |
case .Name: | |
name = value | |
case .Packing: | |
packing = value | |
case .Price: | |
price = Float(value) | |
case .Quantity: | |
quantity = Int(value) | |
} | |
} | |
if let quantity = quantity, let name = name, let price = price { | |
let category = self.categories[name] ?? .Goods | |
product = Product(quantity: quantity, packing: packing, name: name, category: category, price: price, imported: imported) | |
break | |
} | |
} | |
} | |
return product | |
} | |
} | |
struct DefaultTaxCalculator: TaxCalculator { | |
func tax(of product:Product) -> Float { | |
func importedTax(of product:Product) -> Float { | |
return product.imported ? 0.05 : 0.0 | |
} | |
func basicTax(of product:Product) -> Float { | |
let tax:Float | |
switch product.category { | |
case .Goods: | |
tax = 0.1 | |
case .Book, .Food, .Medical: | |
tax = 0.0 | |
} | |
return tax | |
} | |
return (product.price * (importedTax(of:product) + basicTax(of:product))).roundFive() | |
} | |
} | |
struct RecipePrinter { | |
let taxCalculator:TaxCalculator | |
init (taxCalculator:TaxCalculator) { | |
self.taxCalculator = taxCalculator | |
} | |
func printOrder(with products:[Product]) -> String { | |
let priceFormat = ".2" | |
let printedProducts = products.map { (product) -> String in | |
let imported = product.imported ? " imported" : "" | |
let packing:String | |
if let productPacking = product.packing { | |
packing = " \(productPacking) of" | |
} else { | |
packing = "" | |
} | |
let totalPrice = (product.price + self.taxCalculator.tax(of: product)) * Float(product.quantity) | |
return "\(product.quantity)\(imported)\(packing) \(product.name): \(totalPrice.format(priceFormat))" | |
} | |
let (tax, total) = products.reduce((0.0, 0.0)){ (accumulator, next) -> (Float, Float) in | |
let (salesTax, total) = accumulator | |
let totalTax = self.taxCalculator.tax(of: next) * Float(next.quantity) | |
let totalPrice = next.price * Float(next.quantity) + totalTax | |
return (salesTax + totalTax, total + totalPrice) | |
} | |
return "\(printedProducts.joined(separator: "\n"))\nSales Taxes: \(tax.format(priceFormat))\nTotal: \(total.format(priceFormat))" | |
} | |
} | |
func scan(input:[String]) -> [Product] { | |
return input.map{(string) -> Product? in | |
RegexBasedProductFactory(categories:categories).buildProduct(from:string) | |
}.flatMap{$0} | |
} | |
//////////////////// | |
let printer = RecipePrinter(taxCalculator:DefaultTaxCalculator()) | |
print(printer.printOrder(with: scan(input:["1 book at 12.49", | |
"1 music CD at 14.99", | |
"1 chocolate bar at 0.85"]))) | |
print("") | |
print(printer.printOrder(with: scan(input:["1 imported box of chocolates at 10.00", | |
"1 imported bottle of perfume at 47.50"]))) | |
print("") | |
print(printer.printOrder(with: scan(input:["1 imported bottle of perfume at 27.99", | |
"1 bottle of perfume at 18.99", | |
"1 packet of headache pills at 9.75", | |
"1 box of imported chocolates at 11.25"]))) | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment