-
Notifications
You must be signed in to change notification settings - Fork 64
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
Time != Money #10
Changes from 6 commits
6904832
917e890
7053e96
4c28929
2f0ccf8
52c42b2
257eed1
101f151
278e89b
922a7ad
40b6abc
f4c2afe
8894749
0e7b343
e635bf7
979b5be
fe1918e
2738726
3dac478
e51d643
87291db
e71cd12
020f135
f6a6fbd
0261d1b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
4.1 | ||
4.1.1 |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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) | ||
|
@@ -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` | ||
|
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?** | ||
|
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) | ||
} | ||
} |
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) | ||
} | ||
} |
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> |
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> |
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> |
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> |
There was a problem hiding this comment.
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?There was a problem hiding this comment.
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