Skip to content

Instantly share code, notes, and snippets.

@trilliwon
Created November 29, 2016 01:54
Show Gist options
  • Save trilliwon/eb0a86c1a275cc8a458db8d33945a6da to your computer and use it in GitHub Desktop.
Save trilliwon/eb0a86c1a275cc8a458db8d33945a6da to your computer and use it in GitHub Desktop.
money class with TDD
public struct HashTable<Key: Hashable, Value> {
fileprivate typealias Element = (key: Key, value: Value)
fileprivate typealias Bucket = [Element]
fileprivate var buckets: [Bucket]
fileprivate(set) var count = 0
public init(capacity: Int) {
assert(capacity > 0)
buckets = .init(repeating: [], count: capacity)
}
public var isEmpty: Bool {
return count == 0
}
fileprivate func indexForKey(_ key: Key) -> Int {
return abs(key.hashValue) % buckets.count
}
}
// MARK: - Basic operations
extension HashTable {
public subscript(key: Key) -> Value? {
get {
return valueForKey(key)
}
set {
if let value = newValue {
_ = updateValue(value, forKey: key)
} else {
_ = removeValueForKey(key)
}
}
}
public func valueForKey(_ key: Key) -> Value? {
let index = indexForKey(key)
for element in buckets[index] {
if element.key == key {
return element.value
}
}
return nil // key not in hash table
}
public mutating func updateValue(_ value: Value, forKey key: Key) -> Value? {
let index = indexForKey(key)
// Do we already have this key in the bucket?
for (i, element) in buckets[index].enumerated() {
if element.key == key {
let oldValue = element.value
buckets[index][i].value = value
return oldValue
}
}
// This key isn't in the bucket yet; add it to the chain.
buckets[index].append((key: key, value: value))
count += 1
return nil
}
public mutating func removeValueForKey(_ key: Key) -> Value? {
let index = indexForKey(key)
// Find the element in the bucket's chain and remove it.
for (i, element) in buckets[index].enumerated() {
if element.key == key {
buckets[index].remove(at: i)
count -= 1
return element.value
}
}
return nil // key not in hash table
}
public mutating func removeAll() {
buckets = .init(repeating: [], count: buckets.count)
count = 0
}
}
// MARK: - Helper methods for inspecting the hash table
extension HashTable {
public var keys: [Key] {
var a = [Key]()
for bucket in buckets {
for element in bucket {
a.append(element.key)
}
}
return a
}
public var values: [Value] {
var a = [Value]()
for bucket in buckets {
for element in bucket {
a.append(element.value)
}
}
return a
}
}
// MARK: - For debugging
extension HashTable: CustomStringConvertible {
public var description: String {
return buckets.flatMap { b in b.map { e in "\(e.key) = \(e.value)" } }.joined(separator: ", ")
}
}
extension HashTable: CustomDebugStringConvertible {
public var debugDescription: String {
var s = ""
for (i, bucket) in buckets.enumerated() {
s += "bucket \(i): " + bucket.map { e in "\(e.key) = \(e.value)" }.joined(separator: ", ") + "\n"
}
return s
}
}
import Foundation
protocol Expression {
func reduce(_ bank: Bank, to: String) -> Money
func plus(_ addend: Expression) -> Expression
func times(_ multiplier: Int) -> Expression
}
class Money: Equatable, Expression {
internal var amount: Int
internal var currencyString: String
init(amount: Int, currencyString: String) {
self.amount = amount
self.currencyString = currencyString
}
static func dollar(_ amount: Int) -> Money {
return Money(amount: amount, currencyString: "USD")
}
static func frenc(_ amount: Int) -> Money {
return Money(amount: amount, currencyString: "CHF")
}
func currency() -> String {
return currencyString
}
func times(_ multiplier: Int) -> Expression {
return Money(amount: amount * multiplier, currencyString: currencyString)
}
func plus(_ addend: Expression) -> Expression {
return Sum(augend: self, addend: addend)
}
func reduce(_ bank: Bank, to: String) -> Money {
let rate = bank.rate(currency(), to: to)
return Money(amount: amount / rate, currencyString: to)
}
}
func ==(lhs: Money, rhs: Money) -> Bool {
if lhs.amount != rhs.amount {
return false
}
if lhs.currencyString != rhs.currencyString {
return false
}
return true
}
struct Pair: Equatable, Hashable {
fileprivate let from: String
fileprivate let to: String
init(from: String, to: String) {
self.from = from
self.to = to
}
var hashValue: Int = 0
}
func ==(lhs: Pair, rhs: Pair) -> Bool {
if lhs.from != rhs.from {
return false
}
if lhs.to != rhs.to {
return false
}
return true
}
class Bank {
var rates = HashTable<Pair, Int>(capacity: 10)
func reduce(_ source: Expression, to: String) -> Money {
return source.reduce(self, to: to)
}
func rate(_ from: String, to: String) -> Int {
if from == to { return 1 }
let rate: Int = rates.valueForKey(Pair(from: from, to: to))!
return rate
}
func addRate(_ from: String, to: String, rate: Int) {
rates[Pair(from: from, to: to)] = rate
}
}
struct Sum: Expression {
let augend: Expression
let addend: Expression
func reduce(_ bank: Bank, to: String) -> Money {
let amount = augend.reduce(bank, to: to).amount + addend.reduce(bank, to: to).amount
return Money(amount: amount, currencyString: to)
}
func plus(_ addend: Expression) -> Expression {
return Sum(augend: self, addend: addend)
}
func times(_ multiplier: Int) -> Expression {
return Sum(augend: addend.times(multiplier), addend: addend.times(multiplier))
}
}
import XCTest
@testable import TDDByExample
class MoneyTests: XCTestCase {
override func setUp() {
super.setUp()
}
override func tearDown() {
super.tearDown()
}
func test_Multiplication() {
let five: Money = Money.dollar(5)
let ten = five.times(2) as! Money
let fifteen = five.times(3) as! Money
XCTAssertEqual(Money.dollar(10), ten)
XCTAssertEqual(Money.dollar(15), fifteen)
}
func test_Currency() {
XCTAssertEqual("USD", Money.dollar(1).currency())
XCTAssertEqual("CHF", Money.frenc(1).currency())
}
func test_Equality() {
XCTAssertTrue((Money.dollar(5) == Money.dollar(5)))
XCTAssertFalse(Money.dollar(5) == Money.dollar(6))
XCTAssertFalse(Money.dollar(5) == Money.frenc(5))
}
func test_SimpleAddition() {
let five = Money.dollar(5)
let sum: Expression = five.plus(five)
let bank = Bank()
let reduced = bank.reduce(sum, to: "USD")
XCTAssertEqual(Money.dollar(10), reduced)
}
func test_PlusReturnsSum() {
let five = Money.dollar(5)
let result = five.plus(five)
let sum: Sum = result as! Sum
let augend = sum.augend as! Money
let addend = sum.addend as! Money
XCTAssertEqual(five, augend)
XCTAssertEqual(five, addend)
}
func test_ReduceSum() {
let sum = Sum(augend: Money.dollar(3), addend: Money.dollar(4))
let bank = Bank()
let result = bank.reduce(sum, to: "USD")
XCTAssertEqual(Money.dollar(7), result)
}
func test_ReduceMoney() {
let bank = Bank()
let result = bank.reduce(Money.dollar(1), to: "USD")
XCTAssertEqual(Money.dollar(1), result)
}
func test_ReduceMoneyDifferentCurrency() {
let bank = Bank()
bank.addRate("CHF", to: "USD", rate: 2)
let result = bank.reduce(Money.frenc(2), to: "USD")
XCTAssertEqual(Money.dollar(1), result)
}
func test_IdentityRate() {
XCTAssertEqual(1, Bank().rate("USD", to: "USD"))
}
func test_MixedAddtion() {
let fiveBucks: Expression = Money.dollar(5)
let tenFrances: Expression = Money.frenc(10)
let bank = Bank()
bank.addRate("CHF", to: "USD", rate: 2)
let result: Money = bank.reduce(fiveBucks.plus(tenFrances), to: "USD")
XCTAssertEqual(Money.dollar(10), result)
}
func test_SumPlusMoney() {
let fiveBucks: Expression = Money.dollar(5)
let tenFrances: Expression = Money.frenc(10)
let bank = Bank()
bank.addRate("CHF", to: "USD", rate: 2)
let sum: Expression = Sum(augend: fiveBucks, addend: tenFrances).plus(fiveBucks)
let result: Money = bank.reduce(sum, to: "USD")
XCTAssertEqual(Money.dollar(15), result)
}
func test_SumTimes() {
let fiveBucks: Expression = Money.dollar(5)
let tenFrances: Expression = Money.frenc(10)
let bank = Bank()
bank.addRate("CHF", to: "USD", rate: 2)
let sum: Expression = Sum(augend: fiveBucks, addend: tenFrances).times(2)
let result: Money = bank.reduce(sum, to: "USD")
XCTAssertEqual(Money.dollar(20), result)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment