-
Notifications
You must be signed in to change notification settings - Fork 2
Measurement FormatStyle (#1) #22
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
base: main
Are you sure you want to change the base?
Changes from all commits
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 |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| // | ||
| // Formatter.swift | ||
| // Units | ||
| // (aka Fountation.FormatStyle) | ||
| // | ||
| // Created by Jason Jobe on 10/24/25. | ||
| // | ||
|
|
||
| public extension Measurement { | ||
| struct Formatter<Output> { | ||
| let format: (Measurement) -> Output | ||
| } | ||
|
|
||
| func formatted<Output>(_ formatter: Formatter<Output>) -> Output { | ||
| formatter.format(self) | ||
| } | ||
|
|
||
| func formatted(_ formatter: Formatter<String> = .measurement()) -> String { | ||
| formatter.format(self) | ||
| } | ||
|
|
||
| func formatted( | ||
| minimumFractionDigits: Int = 0, | ||
| maximumFractionDigits: Int = 4 | ||
| ) -> String { | ||
| Formatter | ||
| .measurement( | ||
| minimumFractionDigits: minimumFractionDigits, | ||
| maximumFractionDigits: maximumFractionDigits) | ||
| .format(self) | ||
| } | ||
| } | ||
|
Comment on lines
+9
to
+32
Owner
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. What do you think about using Swift's built-in // Implementation
extension NumberFormatter {
func string(from measurement: Measurement) {
return "\(self.string(from: .init(value: measurement.value)) \(measurement.unit.symbol)"
}
}
// Usage
let measurement = 28.123.measured(in: .meter)
let formatter = NumberFormatter()
formatter.maximumFractionDigits = 2
print(formatter.string(from: measurement)) // Prints `28.12 m`This seems like this approach would inherit very configurable options, while also simplifying the implementation and usage. Thoughts?
Author
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, I was leaning into the newer Format API. The custom implementation is limiting. I'll work on that. |
||
|
|
||
| extension Measurement.Formatter where Output == String { | ||
| public static func measurement( | ||
| minimumFractionDigits: Int = 0, | ||
| maximumFractionDigits: Int = 4 | ||
| ) -> Self { | ||
| self.init { value in | ||
| value.value | ||
| .formatted( | ||
| minimumFractionDigits: minimumFractionDigits, | ||
| maximumFractionDigits: maximumFractionDigits) | ||
| + " \(value.unit.symbol)" | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
||
| // MARK: Percent Formatter | ||
|
|
||
| public extension Percent { | ||
| struct Formatter<Output> { | ||
| let format: (Percent) -> Output | ||
| } | ||
|
|
||
| func formatted<Output>(_ formatter: Formatter<Output>) -> Output { | ||
| formatter.format(self) | ||
| } | ||
| } | ||
|
|
||
| public extension Percent { | ||
| func formatted(fractionDigits: Int = 2) -> String { | ||
| Formatter(fractionDigits: fractionDigits).format(self) | ||
| } | ||
| } | ||
|
|
||
| public extension Percent.Formatter where Output == String { | ||
|
|
||
| init (fractionDigits: Int) { | ||
| self.init { value in | ||
| (value.magnitude * 100) | ||
| .formatted( | ||
| minimumFractionDigits: 0, | ||
| maximumFractionDigits: fractionDigits) | ||
| + value.unit.symbol | ||
| } | ||
| } | ||
| } | ||
|
|
||
| public extension BinaryFloatingPoint { | ||
| func formatted(minimumFractionDigits: Int = 0, maximumFractionDigits: Int = 4) -> String { | ||
| let minDigits = max(0, minimumFractionDigits) | ||
| let maxDigits = max(minDigits, maximumFractionDigits) | ||
| let s = String(format: "%.\(maxDigits)f", Double(self)) | ||
| if maxDigits > minDigits, s.contains(".") { | ||
| var trimmed = s | ||
| while trimmed.last == "0" { trimmed.removeLast() } | ||
| if trimmed.last == "." { trimmed.removeLast() } | ||
| if let dotIndex = trimmed.firstIndex(of: ".") { | ||
| let fractionalCount = trimmed.distance(from: trimmed.index(after: dotIndex), to: trimmed.endIndex) | ||
| if fractionalCount < minDigits { | ||
| return String(format: "%.\(minDigits)f", Double(self)) | ||
| } | ||
| } | ||
| return trimmed | ||
| } | ||
| return s | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -43,7 +43,7 @@ import Foundation | |
|
|
||
| If you see a percent sign in a calculation, just convert it to a decimal and proceed as usual. If you want to know how subtraction works with percentages, or how to handle more complex expressions, let me know! | ||
| */ | ||
| public struct Percent: Numeric, Equatable, Codable { | ||
| public struct Percent: Numeric, Codable, Sendable { | ||
|
|
||
| public private(set) var magnitude: Double | ||
|
|
||
|
|
@@ -65,6 +65,14 @@ public struct Percent: Numeric, Equatable, Codable { | |
| } | ||
| } | ||
|
|
||
| extension Percent: Equatable { | ||
| /// Implemented as "nearly" equal | ||
| public static func ==(lhs: Percent, rhs: Percent) -> Bool { | ||
| lhs.magnitude >= rhs.magnitude.nextDown | ||
| && lhs.magnitude <= lhs.magnitude.nextUp | ||
| } | ||
| } | ||
|
Comment on lines
+68
to
+74
Owner
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. Why change Equatable implementation to nearly equal? Is this related to the tests that were added?
Author
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. The significant digits of a "percent" is usually small so representing them with a Double in the arithmetic can lead to values that might result in something like 37.989488484% leading to comparisons that break our intuition (like == 38%). This felt like a reasonable compromise. |
||
|
|
||
| extension Measurement { | ||
| public var isPercent: Bool { | ||
| self.unit == Percent.unit | ||
|
|
@@ -201,3 +209,23 @@ public extension Measurement { | |
| ) | ||
| } | ||
| } | ||
|
|
||
| extension Percent: Comparable { | ||
| public static func < (lhs: Self, rhs: Self) -> Bool { | ||
| lhs.magnitude < rhs.magnitude | ||
| } | ||
| } | ||
|
|
||
| extension Percent { | ||
| /** | ||
| Returns a random value within the given range. | ||
|
|
||
| ``` | ||
| Percent.random(in: 10%...20%) | ||
| // 10%, 11%, 12%, 19.98%, etc. | ||
| ``` | ||
| */ | ||
| public static func random(in range: ClosedRange<Self>) -> Self { | ||
| self.init(magnitude: .random(in: range.lowerBound.magnitude...range.upperBound.magnitude)) | ||
| } | ||
| } | ||
|
Comment on lines
+219
to
+231
Owner
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. nit: Is
Author
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. In one sense, it is just syntactic sugar but it makes the intent clearer and more succinct and encapsulates the internals reducing developer cognitive load. |
||
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.
No need for this header; we can track dates/contributions using Git.