Skip to content

Instantly share code, notes, and snippets.

@MarioBajr
Last active February 1, 2017 18:25
Show Gist options
  • Save MarioBajr/139703d71a4a214d30f2b3104458b886 to your computer and use it in GitHub Desktop.
Save MarioBajr/139703d71a4a214d30f2b3104458b886 to your computer and use it in GitHub Desktop.
//: 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