Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Time != Money #10

Merged
merged 25 commits into from
Mar 5, 2019
Merged
Show file tree
Hide file tree
Changes from 6 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
2 changes: 1 addition & 1 deletion .swift-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
4.1
4.1.1
20 changes: 11 additions & 9 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,20 @@ import PackageDescription
let package = Package(
name: "Tagged",
products: [
.library(
name: "Tagged",
targets: ["Tagged"]),
.library(name: "Tagged", targets: ["Tagged"]),
.library(name: "TaggedMoney", targets: ["TaggedMoney"]),
.library(name: "TaggedTime", targets: ["TaggedTime"]),
],
dependencies: [
],
targets: [
.target(
name: "Tagged",
dependencies: []),
.testTarget(
name: "TaggedTests",
dependencies: ["Tagged"]),
.target(name: "Tagged", dependencies: []),
.testTarget(name: "TaggedTests", dependencies: ["Tagged"]),

.target(name: "TaggedMoney", dependencies: ["Tagged"]),
.testTarget(name: "TaggedMoneyTests", dependencies: ["TaggedMoney"]),

.target(name: "TaggedTime", dependencies: ["Tagged"]),
.testTarget(name: "TaggedTimeTests", dependencies: ["TaggedTime"]),
]
)
54 changes: 54 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ A wrapper type for safer, expressive code.
- [Handling tag collisions](#handling-tag-collisions)
- [Accessing raw values](#accessing-raw-values)
- [Features](#features)
- [Nanolibraries](#nanolibraries)
- [FAQ](#faq)
- [Installation](#installation)
- [Interested in learning more?](#interested-in-learning-more)
Expand Down Expand Up @@ -300,6 +301,59 @@ struct Product {
let totalCents = products.reduce(0) { $0.amount + $1.amount }
```

## Nanolibraries

The `Tagged` library also comes with a few nanolibraries for handling common types in a type safe way.

### `TaggedTime`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wonder if we want "safe" conversion to Date, since that's a common thing to do with seconds/milliseconds and prone to errors?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good idea, I did that but don't know if I did it correctly. check it out 257eed1


The API's we interact with often return timestamps in seconds or milliseconds measured from an epoch time. Keeping track of the units can be messy, either being done via documentation or by naming fields in a particular way, e.g. `publishedAtMs`. Mixing up the units on accident can lead to wildly inaccurate logic.

By importing `TaggedTime` you will get access to two generic types, `Milliseconds<A>` and `Seconds<A>`, that allow the compiler to sort out the differences for you. You can use them in your models:

```swift
struct BlogPost: Decodable {
typealias Id = Tagged<BlogPost, Int>

let id: Id
let publishedAt: Seconds<Int>
let title: String
}
```

Now you have documentation of the unit in the type automatically, and you can never accidentally compare seconds to milliseconds:

```swift
let futureTime: Milliseconds = 1528378451000

breakingBlogPost.publishedAt < futureTime
// 🛑 Binary operator '<' cannot be applied to operands of type
// 'Tagged<SecondsTag, Double>' and 'Tagged<MillisecondsTag, Double>'
```

Read more on our blog post: [Tagged Seconds and Milliseconds](https://www.pointfree.co/blog/posts/6-tagged-seconds-and-milliseconds).

### `TaggedMoney`

API's can also send back money amounts in two standard units: whole dollar amounts or cents (1/100 of a dollar). Keeping track of this distinction can also be messy and error prone.

Importing the `TaggedMoney` libary gives you access to two generic types, `Dollars<A>` and `Cents<A>`, that give you compile-time guarantees in keeping the two units separate.

```swift
struct Prize {
let amount: Dollars<Int>
let name: String
}

let moneyRaised: Cents<Int> = 50_000

theBigPrize.amount < moneyRaised
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could also provide working (non-protocol) overloads for some of these operators if we want and if they're useful, though maybe it's always better to show an error fun accidental type overlap.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah something worth thinking about. for the time being I just updated the readme to show what it looks like to fix those compiler errors.

// 🛑 Binary operator '<' cannot be applied to operands of type
// 'Tagged<DollarsTag, Int>' and 'Tagged<CentsTag, Int>'
```

It is important to note that these types do not encapsulate _currency_, but rather just the abstract notion of the whole and fractional unit of money. You will still need to track particular currencies, like USD, EUR, MXN, alongside these values.

## FAQ

- **Why not use a type alias?**
Expand Down
19 changes: 19 additions & 0 deletions Sources/TaggedMoney/TaggedMoney.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Tagged

public enum CentsTag {}
public typealias Cents<A> = Tagged<CentsTag, A>

public enum DollarsTag {}
public typealias Dollars<A> = Tagged<DollarsTag, A>

extension Tagged where Tag == CentsTag, RawValue: BinaryFloatingPoint {
public var dollars: Dollars<RawValue> {
return .init(rawValue: self.rawValue / 100)
}
}

extension Tagged where Tag == DollarsTag, RawValue: Numeric {
public var cents: Cents<RawValue> {
return .init(rawValue: self.rawValue * 100)
}
}
19 changes: 19 additions & 0 deletions Sources/TaggedTime/TaggedTime.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Tagged

public enum MillisecondsTag {}
public typealias Milliseconds<A> = Tagged<MillisecondsTag, A>

public enum SecondsTag {}
public typealias Seconds<A> = Tagged<SecondsTag, A>

extension Tagged where Tag == MillisecondsTag, RawValue: BinaryFloatingPoint {
public var seconds: Seconds<RawValue> {
return .init(rawValue: self.rawValue / 1000)
}
}

extension Tagged where Tag == SecondsTag, RawValue: Numeric {
public var milliseconds: Milliseconds<RawValue> {
return .init(rawValue: self.rawValue * 1000)
}
}
2 changes: 1 addition & 1 deletion Tagged.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,5 @@ Pod::Spec.new do |s|
s.tvos.deployment_target = "9.0"
s.watchos.deployment_target = "2.0"

s.source_files = "Sources", "Sources/**/*.swift"
s.source_files = "Sources", "Sources/Tagged/*.swift"
end
25 changes: 25 additions & 0 deletions Tagged.xcodeproj/TaggedMoneyTests_Info.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSPrincipalClass</key>
<string></string>
</dict>
</plist>
25 changes: 25 additions & 0 deletions Tagged.xcodeproj/TaggedMoney_Info.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSPrincipalClass</key>
<string></string>
</dict>
</plist>
25 changes: 25 additions & 0 deletions Tagged.xcodeproj/TaggedTimeTests_Info.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSPrincipalClass</key>
<string></string>
</dict>
</plist>
25 changes: 25 additions & 0 deletions Tagged.xcodeproj/TaggedTime_Info.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSPrincipalClass</key>
<string></string>
</dict>
</plist>
Loading