Skip to content
This repository has been archived by the owner on Jul 2, 2018. It is now read-only.

Post 1.0.0 issues #16

Merged
merged 6 commits into from
Nov 6, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 45 additions & 9 deletions Money/FX.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@


import Foundation
import ValueCoding
import Result
import SwiftyJSON

Expand All @@ -51,7 +52,7 @@ public protocol MoneyPairType {
The minimum interface required to perform a foreign
currency exchange.
*/
public class FXQuote {
public class FXQuote: NSObject, NSCoding {

/// The exchange rate, stored as a `BankersDecimal`.
public let rate: BankersDecimal
Expand All @@ -63,6 +64,14 @@ public class FXQuote {
self.rate = rate
}

public required init?(coder aDecoder: NSCoder) {
rate = BankersDecimal.decode(aDecoder.decodeObjectForKey("rate"))!
}

public func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(rate.encoded, forKey: "rate")
}

/**
## Calculate transaction value
Lets assume we want to convert EUR 100 in to USD. The
Expand Down Expand Up @@ -216,27 +225,54 @@ extension FXRemoteProviderType {

extension FXRemoteProviderType where BaseMoney.DecimalStorageType == BankersDecimal.DecimalStorageType, CounterMoney.DecimalStorageType == BankersDecimal.DecimalStorageType {

internal static func fxFromQuoteWithBase(base: BaseMoney) -> FXQuote -> CounterMoney {
return { $0.transactionValueForBaseValue(base) }
internal static func fxFromQuoteWithBase(base: BaseMoney) -> FXQuote -> (BaseMoney, FXQuote, CounterMoney) {
return { (base, $0, $0.transactionValueForBaseValue(base)) }
}

/**
# FX
# FX - Get Quote
This is the primary API used to determine for Foreign Exchange transactions. Using the
`Yahoo` FX Provider as an example, we would use it like this..

Yahoo<GBP, USD>.quote(100) { result in
guard let (pounds, quote, usd) = result.value else {
error("Received an `FXError`")
}
print("Exchanged \(pounds) into \(usd) with a rate of \(quote.rate)")
}

let gbp: GBP = 100 // We have £100
Yahoo<GBP, USD>.fx(gbp) { result in
- parameter base: the `BaseMoney` which is a `MoneyType`. Because it's literal
convertible, this can receive a literal if you're just playing.
- parameter completion: a completion block which receives a `Result<T, E>`.
The error is an `FXError` value, and the result "value" is a tuple, of the
base money, the quote, and the counter money, or `(BaseMoney, FXQuote, CounterMoney)`.
- returns: an `NSURLSessionDataTask`.
*/
public static func quote(base: BaseMoney, completion: Result<(BaseMoney, FXQuote, CounterMoney), FXError> -> Void) -> NSURLSessionDataTask {
let client = FXServiceProviderNetworkClient(session: session())
let fxFromQuote = fxFromQuoteWithBase(base)
return client.get(request(), adaptor: quoteFromNetworkResult) { completion($0.map(fxFromQuote)) }
}

/**
# FX - Get Counter Money
This is a convenience API used to determine for Foreign Exchange transactions. Using the
`Yahoo` FX Provider as an example, we would use it like this..

Yahoo<GBP, USD>.fx(100) { result in
guard let usd = result.value?.counter else {
print("Received an `FXError`")
}
print("We have \(usd)") // We have $119 (or whatever)
}
- parameter base: the `BaseMoney` which is a `MoneyType`. Because it's literal
convertible, this can receive a literal if you're just playing.
- parameter completion: a completion block which receives a `Result<T, E>`.
The error is an `FXError` value, and the result "value" is the `CounterMoney`.
- returns: an `NSURLSessionDataTask`.
*/
public static func fx(base: BaseMoney, completion: Result<CounterMoney, FXError> -> Void) -> NSURLSessionDataTask {
let client = FXServiceProviderNetworkClient(session: session())
let fxFromQuote = fxFromQuoteWithBase(base)
return client.get(request(), adaptor: quoteFromNetworkResult) { completion($0.map(fxFromQuote)) }
return quote(base) { completion($0.map { $0.2 }) }
}
}

Expand Down
37 changes: 29 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Money is a Swift framework for iOS, watchOS, tvOS and OS X. It provides types an

## Usage

The Money framework defines the type, `Money` which represents money in the device’s current locale. The following code:
The Money framework defines the type `Money`, which represents money in the device’s current locale. The following code:

```swift
let money: Money = 100
Expand Down Expand Up @@ -57,24 +57,37 @@ Because the currencies are typed, it means that they cannot be combined together
```swift
let money = pounds + euros
```
> // Binary operator '+' cannot be applied to operands of type 'GBP' (aka '_Money&lt;Currency.GBP&gt;') and 'EUR' (aka '_Money&lt;Currency.EUR&gt;')
> Binary operator '+' cannot be applied to operands of type 'GBP' (aka '_Money&lt;Currency.GBP&gt;') and 'EUR' (aka '_Money&lt;Currency.EUR&gt;')

Of course, `Money` supports the usual suspects of decimal arithmetic operators, so you can add, subtract, multiply, divide values of the same type, and values with `Int` and `Double` with some limitations.

## Foreign Currency Exchange (FX)
To represent a foreign exchange transaction, i.e. converting `USD` to `EUR`, use a FX service provider. There is built in support for [Yahoo](https://finance.yahoo.com/currency-converter/#from=USD;to=EUR;amt=1) and [OpenExchangeRates.org](https://openexchangerates.org) services. But it’s possible for consumers to create their own.
To represent a foreign exchange transaction, i.e. converting `USD` to `EUR`, use a FX service provider. There is built in support for [Yahoo](https://finance.yahoo.com/currency-converter/#from=USD;to=EUR;amt=1) and [OpenExchangeRates.org](https://openexchangerates.org) services. But it’s possible for consumers to create their own too.

The following code snippet represent a currency exchange using Yahoo’s currency converter.

```swift
Yahoo<USD,EUR>.quote(100) { result in
if let (dollars, quote, euros) = result.value {
print("Exchanged \(dollars) into \(euros) with a rate of \(quote.rate)")
}
}
```

> Exchanged US$ 100.00 into € 92.15 with a rate of 0.9215

The result, delivered asynchronously, uses [`Result`](http://github.com/antitypical/Result) to encapsulate either a tuple value `(BaseMoney, FXQuote, CounterMoney)` or an `FXError` value. Obviously, in real code - you’d need to check for errors ;)

There is a neat convenience function which just returns the `CounterMoney` as its `Result` value type.

```swift
Yahoo<USD,EUR>.fx(100) { euros in
print("You got \(euros)")
}
```

> You got .Success(€ 92.00)
> You got .Success(€ 92.15)

The result, delivered asynchronously, uses [`Result`](http://github.com/antitypical/Result) to encapsulate either the `FXProviderType.CounterMoney` or an `FXError` value. Obviously, in real code - you’d need to check for errors ;)

### Creating custom FX service providers

Expand Down Expand Up @@ -120,6 +133,10 @@ public static func quoteFromNetworkResult(result: Result<(NSData?, NSURLResponse

Note that the provider doesn’t need to perform any networking itself. It is all done by the framework. This is a deliberate architectural design as it makes it much easier to unit test the adaptor code.

Additionally FX APIs will be added shortly,
1. To calculate the reverse exchange, i.e. how many dollars would I need to get so many euros.
2. For the two (forward & reverse) exchanges, I’ll also add a `quote` function, which will return the `FXQuote` object. This might be useful if your app needs to persist the quote used for an exchange.

# Creating custom currencies

If your app has its own currency e.g. ⭐️s or 💎s or even 🐝s, you might want to consider making a type for it.
Expand Down Expand Up @@ -162,15 +179,17 @@ print(“I have \(bees)”)

And of course if you have an IAP for purchasing in-app currency, then I’m sure a custom FX provider would be handy.

Take a look at the example project, Custom Money, for a an example of a custom local FX provider to exchange your 🐝s.
Take a look at the example project, Custom Money, for an example of a custom local FX provider to exchange your 🐝s.

## Installation
Money builds as a cross platform (iOS, OS X, watchOS) extension compatible framework. It is also available via CocoaPods
Money builds as a cross platform (iOS, OS X, watchOS) extension compatible framework. It is compatible with [Carthage](https://github.com/carthage/carthage). It is also available via CocoaPods

```ruby
pod ‘Money’
```

At of writing there seems to be issues with the CocoaDocs generator for pure Swift 2 projects. This means that the project doesn’t have a page/docs in CocoaPods sites.

## Architectural style
Swift is designed to have a focus on safety, enabled primarily through strong typing. This framework fully embraces this ethos and uses generics heavily to achieve this goal.

Expand All @@ -182,7 +201,7 @@ Finally, we auto-generate the code which defines all the currencies and money ty

Cocoa has two type which can perform decimal arithmetic, these are `NSDecimalNumber` and `NSDecimal`. `NSDecimal` is faster, but is trickier to work with, and doesn’t have support for limiting the scale of the numbers (which is pretty important when working with currencies).

`DecimalNumberType` is a protocol which refines refines `SignedNumberType` and defines some functions (`add`, `subtract` etc to support the arithmetic). It is also generic over two types, the underlying storage, and the behaviors.
`DecimalNumberType` is a protocol which refines `SignedNumberType` and defines its own functions, `add`, `subtract` etc to support the arithmetic. It is generic over two types, the underlying storage, and the behaviors.

`DecimalNumberType.DecimalStorageType` exists so that conforming types can utilize either `NSDecimalNumber` or `NSDecimal` as their underling storage type.

Expand Down Expand Up @@ -219,6 +238,8 @@ Daniel Thorpe [@danthorpe](https://twitter.com/danthorpe).

Feel free to get in contact if you have questions, queries, or need help.

I wrote an introductory blog post about money [here](http://danthorpe.me/posts/money.html).

## License

Money is available under the MIT license. See the LICENSE file for more info.
Expand Down
9 changes: 3 additions & 6 deletions Tests/FXOpenExchangeRatesTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,6 @@ class FXFreeOpenExchangeRatesTests: FXProviderTests {
typealias TestableProvider = TestableFXRemoteProvider<Provider>
typealias FaultyProvider = FaultyFXRemoteProvider<Provider>

let usd: USD = 100

func test__name() {
XCTAssertEqual(Provider.name(), "OpenExchangeRates.org USDEUR")
}
Expand Down Expand Up @@ -104,7 +102,7 @@ class FXFreeOpenExchangeRatesTests: FXProviderTests {
func test__faulty_provider() {
let expectation = expectationWithDescription("Test: \(__FUNCTION__)")

FaultyProvider.fx(usd) { result in
FaultyProvider.fx(100) { result in
guard let error = result.error else {
XCTFail("Should have received a network error.")
return
Expand All @@ -121,11 +119,10 @@ class FXFreeOpenExchangeRatesTests: FXProviderTests {
waitForExpectationsWithTimeout(1, handler: nil)
}

func test__exhange_usd_to_eur() {

func test__fx() {
let expectation = expectationWithDescription("Test: \(__FUNCTION__)")

TestableProvider.fx(usd) { result in
TestableProvider.fx(100) { result in
if let usd = result.value {
XCTAssertEqual(usd, 92.09)
}
Expand Down
24 changes: 18 additions & 6 deletions Tests/FXTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -109,12 +109,6 @@ class FXErrorTests: XCTestCase {

class FXProviderTests: XCTestCase {

func createGarbageData() -> NSData {
let path = NSBundle(forClass: self.dynamicType).pathForResource("Troll", ofType: "png")
let data = NSData(contentsOfFile: path!)
return data!
}

func dvrJSONFromCassette(name: String) -> JSON? {
guard let path = NSBundle(forClass: self.dynamicType).pathForResource(name, ofType: "json"),
data = NSData(contentsOfFile: path) else {
Expand All @@ -134,3 +128,21 @@ class FXLocalProviderTests: XCTestCase {
XCTAssertEqual(usd, 11)
}
}

class FXQuoteTests: XCTestCase {

var quote: FXQuote!

func archiveEncodedQuote() -> NSData {
return NSKeyedArchiver.archivedDataWithRootObject(quote)
}

func unarchive(archive: NSData) -> FXQuote? {
return NSKeyedUnarchiver.unarchiveObjectWithData(archive) as? FXQuote
}

func test__quote_encodes() {
quote = FXQuote(rate: 1.5409)
XCTAssertEqual(unarchive(archiveEncodedQuote())!.rate, quote.rate)
}
}
5 changes: 2 additions & 3 deletions Tests/FXYahooTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,9 @@ class FXYahooTests: FXProviderTests {
}

func test__faulty_provider() {
let gbp: GBP = 100
let expectation = expectationWithDescription("Test: \(__FUNCTION__)")

FaultyProvider.fx(gbp) { result in
FaultyProvider.fx(100) { result in
guard let error = result.error else {
XCTFail("Should have received a network error.")
return
Expand All @@ -82,7 +81,7 @@ class FXYahooTests: FXProviderTests {
waitForExpectationsWithTimeout(1, handler: nil)
}

func test__exhange_gbp_to_eur() {
func test__fx() {
let expectation = expectationWithDescription("Test: \(__FUNCTION__)")

TestableProvider.fx(100) { result in
Expand Down
12 changes: 12 additions & 0 deletions Tests/MoneyTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,18 @@
import XCTest
@testable import Money

func createGarbageData() -> NSData {
return MoneyTestHelper.createGarbageData()
}

class MoneyTestHelper {
static func createGarbageData() -> NSData {
let path = NSBundle(forClass: MoneyTestHelper.self).pathForResource("Troll", ofType: "png")
let data = NSData(contentsOfFile: path!)
return data!
}
}

class MoneyInitializerTests: XCTestCase {

var money: Money!
Expand Down