From 0342495f8245917dda267a827a353a8e9a373476 Mon Sep 17 00:00:00 2001 From: Jason Jobe Date: Wed, 22 Oct 2025 10:31:20 -0400 Subject: [PATCH 1/3] Measurement FormatStyle (#1) * FormatStyle WIP * added FormatStyle tests * updated README and @available --------- Co-authored-by: Jason Jobe --- README.md | 50 ++++++++++++ Sources/Units/Measurement/Measurement.swift | 55 +++++++++++++ .../Measurement/Percent+Measurement.swift | 79 ++++++++++++++++++- Tests/UnitsTests/MeasurementTests.swift | 8 ++ Tests/UnitsTests/PercentTests.swift | 18 ++++- 5 files changed, 207 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 42ce9cd..5a62065 100644 --- a/README.md +++ b/README.md @@ -194,6 +194,56 @@ let weeklyCartons = try (workforce * personPickRate).convert(to: carton / .week) print(weeklyCartons) // Prints '350.0 carton/week' ``` +### Percent + +While technically not a `Measurement`, the use of the percent symbol ('%') is still useful +in conveying the "semanantics" of a scaler value so we include it in this package. + +Here’s how math operators work with percentages in typical calculations: + +Math operators with percentages treat the percent as its decimal equivalent +(e.g., 25% = 0.25) but in the case of `+` and `-` the calculation is less direct. + +#### Multiplication (100 * 25%) + + When you multiply a number by a percentage, you’re finding that percent of the number. + • 25% is the same as 0.25. + • So, 100 * 25% = 100 * 0.25 = 25. + +#### Division (100 / 30%) + + Dividing by a percentage means dividing by its decimal form. + • 30% is 0.3. + • So, 100 / 30% = 100 / 0.3 ≈ 333.33. + +#### Addition (100 + 10%) + + Adding a percentage to a number is less direct, but usually means increasing the number by that percent. + • 10% of 100 is 10. + • So, 100 + 10% = 100 + (100 * 0.10) = 110. + +#### General Rule + • Percent means “per hundred,” so 25% = 25/100 = 0.25. + • Replace the percent with its decimal equivalent before performing the operation. + + +### FormatStyle + +The `Measurement.Formatter` provides the `formatted` method to enable the +setting of the `NumberFormatStyleConfiguration.Precision` for String output. + +Example Use: + +``` + let measure = 28.123.measured(in: .meter) + + measure.formatted() // -> "28.123 m" + measure.formatted(precision: .significantDigits(1) // -> "30 m" + measure.formatted(precision: .significantDigits(3)) // -> "28.1 m" + measure.formatted(precision: + .integerAndFractionLength(integer: 2, fraction: 0)) // -> "28 m" +``` + ## CLI The easiest way to install the CLI is with brew: diff --git a/Sources/Units/Measurement/Measurement.swift b/Sources/Units/Measurement/Measurement.swift index c6fa885..e74cc5d 100644 --- a/Sources/Units/Measurement/Measurement.swift +++ b/Sources/Units/Measurement/Measurement.swift @@ -194,3 +194,58 @@ extension Measurement: ExpressibleByFloatLiteral { } extension Measurement: Sendable {} + +// MARK: FormatStyle +@available(macOS 12.0, iOS 15.0, *) +public extension Measurement { + + func formatted( + _ style: Style + ) -> Style.FormatOutput where Style.FormatInput == Self { + style.format(self) + } +} + +@available(macOS 12.0, iOS 15.0, *) +public extension Measurement { + typealias Precision = NumberFormatStyleConfiguration.Precision + + struct Formatter { + let format: (Measurement) -> Output + } + + func formatted(_ formatter: Formatter) -> Output { + formatter.format(self) + } + + func formatted(_ formatter: Formatter = .measurement()) -> String { + formatter.format(self) + } + + @_disfavoredOverload + func formatted(precision: Precision? = nil) -> String { + formatted(.measurement(precision: precision)) + } +} + +@available(macOS 12.0, iOS 15.0, *) +extension Measurement.Formatter where Output == String { + public typealias Precision = NumberFormatStyleConfiguration.Precision + + public static func measurement(precision: Precision? = nil) -> Self { + .init { value in + switch (value.unit, precision) { + case (.none, .none): + "\(value.value.formatted(.number))" + case (.none, .some(let p)): + "\(value.value.formatted(.number.precision(p)))" + case (let u, .none): + "\(value.value.formatted(.number)) \(u)" + + case (let u, .some(let p)): + "\(value.value.formatted(.number.precision(p))) \(u)" + } + } + } +} + diff --git a/Sources/Units/Measurement/Percent+Measurement.swift b/Sources/Units/Measurement/Percent+Measurement.swift index 154bee8..c325987 100644 --- a/Sources/Units/Measurement/Percent+Measurement.swift +++ b/Sources/Units/Measurement/Percent+Measurement.swift @@ -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 + } +} + extension Measurement { public var isPercent: Bool { self.unit == Percent.unit @@ -201,3 +209,72 @@ 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.init(magnitude: .random(in: range.lowerBound.magnitude...range.upperBound.magnitude)) + } +} + +// MARK: FormatStyle +@available(macOS 12.0, iOS 15.0, *) +public extension Percent { + + func formatted( + _ style: Style + ) -> Style.FormatOutput where Style.FormatInput == Self { + style.format(self) + } +} + +public extension Percent { + struct Formatter { + let format: (Percent) -> Output + } + + func formatted(_ formatter: Formatter) -> Output { + formatter.format(self) + } +} + +@available(macOS 12.0, iOS 15.0, *) +public extension Percent { + func formatted(_ formatter: Formatter = .percent) -> String { + formatter.format(self) + } + + func formatted(fractionDigits: Int) -> String { + Formatter(fractionDigits: fractionDigits).format(self) + } +} + +@available(macOS 12.0, iOS 15.0, *) +public extension Percent.Formatter where Output == String { + + init (fractionDigits: Int) { + self.init { value in + value.magnitude + .formatted(.percent.precision(.fractionLength(fractionDigits))) + } + } + + static var percent: Self { + .init { value in + return value.magnitude.formatted(.percent) + } + } +} diff --git a/Tests/UnitsTests/MeasurementTests.swift b/Tests/UnitsTests/MeasurementTests.swift index cbb59d8..de120fb 100644 --- a/Tests/UnitsTests/MeasurementTests.swift +++ b/Tests/UnitsTests/MeasurementTests.swift @@ -600,4 +600,12 @@ final class MeasurementTests: XCTestCase { accuracy: accuracy ) } + + func testFormatStyle() { + let measure = 28.123.measured(in: .meter) + XCTAssertEqual(measure.formatted(), "28.123 m") + XCTAssertEqual(measure.formatted(precision: .significantDigits(1)), "30 m") + XCTAssertEqual(measure.formatted(precision: .significantDigits(3)), "28.1 m") + XCTAssertEqual(measure.formatted(precision: .integerAndFractionLength(integer: 2, fraction: 0)), "28 m") + } } diff --git a/Tests/UnitsTests/PercentTests.swift b/Tests/UnitsTests/PercentTests.swift index 2cd673f..f72a679 100644 --- a/Tests/UnitsTests/PercentTests.swift +++ b/Tests/UnitsTests/PercentTests.swift @@ -9,6 +9,7 @@ import XCTest final class PercentTests: XCTestCase { + func testParse() throws { XCTAssertEqual( try Expression("10m + 25%"), @@ -38,7 +39,20 @@ final class PercentTests: XCTestCase { try Expression("10m / 25%").solve(), 40.measured(in: .meter) ) - + } + + func testPercentCalculation() { + XCTAssertEqual(50% * 50%, 25%) + XCTAssertEqual(50% + 5.8%, 55.8%) + XCTAssertEqual(50% - 50%, 0%) + XCTAssertEqual(50% - 5.8%, 44.2%) + } + + func testPercentFormat() { + XCTAssertEqual(30%.formatted(), "30%") + XCTAssertEqual(28.5%.formatted(), "28.5%") + XCTAssertEqual(28.33%.formatted(), "28.33%") + XCTAssertEqual(28.33%.formatted(fractionDigits: 1), "28.3%") + XCTAssertEqual(28.33%.formatted(fractionDigits: 0), "28%") } } - From b1de652971bfc807f7a38ee120b2918ee78cf560 Mon Sep 17 00:00:00 2001 From: Jason Jobe Date: Sat, 25 Oct 2025 00:06:53 -0400 Subject: [PATCH 2/3] simplified Formatting to support linux --- README.md | 9 +- Sources/Units/Measurement/Formatter.swift | 100 ++++++++++++++++++ Sources/Units/Measurement/Measurement.swift | 55 ---------- .../Measurement/Percent+Measurement.swift | 49 --------- Tests/UnitsTests/MeasurementTests.swift | 6 +- 5 files changed, 107 insertions(+), 112 deletions(-) create mode 100644 Sources/Units/Measurement/Formatter.swift diff --git a/README.md b/README.md index 5a62065..dd64c86 100644 --- a/README.md +++ b/README.md @@ -237,11 +237,10 @@ Example Use: ``` let measure = 28.123.measured(in: .meter) - measure.formatted() // -> "28.123 m" - measure.formatted(precision: .significantDigits(1) // -> "30 m" - measure.formatted(precision: .significantDigits(3)) // -> "28.1 m" - measure.formatted(precision: - .integerAndFractionLength(integer: 2, fraction: 0)) // -> "28 m" + measure.formatted() // -> "28.123 m" + measure.formatted(minimumFractionDigits: 4) // -> "28.1230 m" + measure.formatted(maximumFractionDigits: 0) // -> "28 m" + measure.formatted(maximumFractionDigits: 1) // -> "28.1 m" ``` ## CLI diff --git a/Sources/Units/Measurement/Formatter.swift b/Sources/Units/Measurement/Formatter.swift new file mode 100644 index 0000000..f2dc401 --- /dev/null +++ b/Sources/Units/Measurement/Formatter.swift @@ -0,0 +1,100 @@ +// +// Formatter.swift +// Units +// (aka Fountation.FormatStyle) +// +// Created by Jason Jobe on 10/24/25. +// + +public extension Measurement { + struct Formatter { + let format: (Measurement) -> Output + } + + func formatted(_ formatter: Formatter) -> Output { + formatter.format(self) + } + + func formatted(_ formatter: Formatter = .measurement()) -> String { + formatter.format(self) + } + + func formatted( + minimumFractionDigits: Int = 0, + maximumFractionDigits: Int = 4 + ) -> String { + Formatter + .measurement( + minimumFractionDigits: minimumFractionDigits, + maximumFractionDigits: maximumFractionDigits) + .format(self) + } +} + +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 { + let format: (Percent) -> Output + } + + func formatted(_ formatter: Formatter) -> 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 + } +} diff --git a/Sources/Units/Measurement/Measurement.swift b/Sources/Units/Measurement/Measurement.swift index e74cc5d..c6fa885 100644 --- a/Sources/Units/Measurement/Measurement.swift +++ b/Sources/Units/Measurement/Measurement.swift @@ -194,58 +194,3 @@ extension Measurement: ExpressibleByFloatLiteral { } extension Measurement: Sendable {} - -// MARK: FormatStyle -@available(macOS 12.0, iOS 15.0, *) -public extension Measurement { - - func formatted( - _ style: Style - ) -> Style.FormatOutput where Style.FormatInput == Self { - style.format(self) - } -} - -@available(macOS 12.0, iOS 15.0, *) -public extension Measurement { - typealias Precision = NumberFormatStyleConfiguration.Precision - - struct Formatter { - let format: (Measurement) -> Output - } - - func formatted(_ formatter: Formatter) -> Output { - formatter.format(self) - } - - func formatted(_ formatter: Formatter = .measurement()) -> String { - formatter.format(self) - } - - @_disfavoredOverload - func formatted(precision: Precision? = nil) -> String { - formatted(.measurement(precision: precision)) - } -} - -@available(macOS 12.0, iOS 15.0, *) -extension Measurement.Formatter where Output == String { - public typealias Precision = NumberFormatStyleConfiguration.Precision - - public static func measurement(precision: Precision? = nil) -> Self { - .init { value in - switch (value.unit, precision) { - case (.none, .none): - "\(value.value.formatted(.number))" - case (.none, .some(let p)): - "\(value.value.formatted(.number.precision(p)))" - case (let u, .none): - "\(value.value.formatted(.number)) \(u)" - - case (let u, .some(let p)): - "\(value.value.formatted(.number.precision(p))) \(u)" - } - } - } -} - diff --git a/Sources/Units/Measurement/Percent+Measurement.swift b/Sources/Units/Measurement/Percent+Measurement.swift index c325987..f18782d 100644 --- a/Sources/Units/Measurement/Percent+Measurement.swift +++ b/Sources/Units/Measurement/Percent+Measurement.swift @@ -229,52 +229,3 @@ extension Percent { self.init(magnitude: .random(in: range.lowerBound.magnitude...range.upperBound.magnitude)) } } - -// MARK: FormatStyle -@available(macOS 12.0, iOS 15.0, *) -public extension Percent { - - func formatted( - _ style: Style - ) -> Style.FormatOutput where Style.FormatInput == Self { - style.format(self) - } -} - -public extension Percent { - struct Formatter { - let format: (Percent) -> Output - } - - func formatted(_ formatter: Formatter) -> Output { - formatter.format(self) - } -} - -@available(macOS 12.0, iOS 15.0, *) -public extension Percent { - func formatted(_ formatter: Formatter = .percent) -> String { - formatter.format(self) - } - - func formatted(fractionDigits: Int) -> String { - Formatter(fractionDigits: fractionDigits).format(self) - } -} - -@available(macOS 12.0, iOS 15.0, *) -public extension Percent.Formatter where Output == String { - - init (fractionDigits: Int) { - self.init { value in - value.magnitude - .formatted(.percent.precision(.fractionLength(fractionDigits))) - } - } - - static var percent: Self { - .init { value in - return value.magnitude.formatted(.percent) - } - } -} diff --git a/Tests/UnitsTests/MeasurementTests.swift b/Tests/UnitsTests/MeasurementTests.swift index de120fb..1e0a746 100644 --- a/Tests/UnitsTests/MeasurementTests.swift +++ b/Tests/UnitsTests/MeasurementTests.swift @@ -604,8 +604,8 @@ final class MeasurementTests: XCTestCase { func testFormatStyle() { let measure = 28.123.measured(in: .meter) XCTAssertEqual(measure.formatted(), "28.123 m") - XCTAssertEqual(measure.formatted(precision: .significantDigits(1)), "30 m") - XCTAssertEqual(measure.formatted(precision: .significantDigits(3)), "28.1 m") - XCTAssertEqual(measure.formatted(precision: .integerAndFractionLength(integer: 2, fraction: 0)), "28 m") + XCTAssertEqual(measure.formatted(minimumFractionDigits: 4), "28.1230 m") + XCTAssertEqual(measure.formatted(maximumFractionDigits: 0), "28 m") + XCTAssertEqual(measure.formatted(maximumFractionDigits: 1), "28.1 m") } } From e00c56802231c6afc22239379b5113aba69d1d0a Mon Sep 17 00:00:00 2001 From: Jason Jobe Date: Wed, 28 Jan 2026 12:03:02 -0500 Subject: [PATCH 3/3] added Quantum and Inventory --- Sources/Units/Inventory.swift | 157 ++++++++++++++++++++++ Sources/Units/Measurement/Formatter.swift | 21 +-- Sources/Units/Quantum.swift | 93 +++++++++++++ 3 files changed, 263 insertions(+), 8 deletions(-) create mode 100644 Sources/Units/Inventory.swift create mode 100644 Sources/Units/Quantum.swift diff --git a/Sources/Units/Inventory.swift b/Sources/Units/Inventory.swift new file mode 100644 index 0000000..d29c1da --- /dev/null +++ b/Sources/Units/Inventory.swift @@ -0,0 +1,157 @@ +// +// Inventory.swift +// Units +// +// Created by Jason Jobe on 1/28/26. +// +import Foundation + +// MARK: SKU for Inventory +public typealias SKU = URL +public typealias Stock = Quantum + +public extension QuantumType { + var sku: SKU { URL(sku: "\(self)") } +} + +// MARK: Inventory - a collection of Quamtum +public struct Inventory { + public private(set) var items: [Stock] + public var count: Int { items.count } +} + +public extension Inventory { + + func adding(_ q: Quantum) -> Self { + var newSelf = self + newSelf.add(q) + return newSelf + } + + func subtracting(_ q: Quantum) -> Self { + var newSelf = self + newSelf.subtract(q) + return newSelf + } + + mutating func subtract(_ q: Quantum) { + if let ndx = items.firstIndex(where: { $0.qtype == q.qtype }) { + items[ndx].magnitude -= q.magnitude + if items[ndx].magnitude == 0 { + items.remove(at: ndx) + } + } else { + items.append(q) + } + } + + mutating func add(_ q: Quantum) { + if let ndx = items.firstIndex(where: { $0.qtype == q.qtype }) { + items[ndx].magnitude += q.magnitude + if items[ndx].magnitude == 0 { + items.remove(at: ndx) + } + } else { + items.append(q) + } + } + + func amount(of qt: QuantumType) -> Double { + items.reduce(0) { partial, elem in partial + (elem.qtype == qt ? elem.magnitude : 0) } + } +} + +public extension Inventory { + static func += (lhs: inout Self, rhs: Quantum) { + lhs.add(rhs) + } + static func -= (lhs: inout Self, rhs: Quantum) { + lhs.subtract(rhs) + } +} + +public extension Inventory { + static func += (lhs: inout Self, rhs: Inventory) { + rhs.items.forEach { lhs.add($0) } + } + + static func -= (lhs: inout Self, rhs: Inventory) { + rhs.items.forEach { lhs.subtract($0) } + } + + static func *= (lhs: inout Self, rhs: Double) { + for i in lhs.items.indices { + lhs.items[i].magnitude *= rhs + } + } + + static func /= (lhs: inout Self, rhs: Double) { + for i in lhs.items.indices { + lhs.items[i].magnitude /= rhs + } + } + + static func + (lhs: Self, rhs: Inventory) -> Inventory { + var result = lhs + for element in rhs.items { + result.add(element) + } + return result + } +} + +// MARK: SKU Extenstion to URL +extension URL { + /** + A SKU URL has the scheme 'sku' and has the general format of + sku:/:=material[25units] + Guarenteed to create something + */ + public init(sku: String) { + self = Self.parse(sku: sku) + } + + public static func parse(sku: String) -> URL { + if sku.lowercased().hasPrefix("sku:") { + if let url = URL(string: sku) { + return url + } + } + + // Ensure we always create a valid URL with the `sku` scheme. + // We build components and percent-encode the path so arbitrary SKU tokens are safe. + var comps = URLComponents() + comps.scheme = "sku" + + // Treat the whole string as the URL path + // Valid URL MUST have single leading slash + let rawPath = sku.hasPrefix("/") ? sku : "/" + sku + // URLComponents expects path to be unescaped + // it will handle encoding when producing .url + comps.path = rawPath + + if let url = comps.url { + return url + } + + // Absolute fallback: construct from a minimally encoded string. + // Replace spaces with %20 and remove illegal characters conservatively. + let allowed = CharacterSet.urlPathAllowed.union(CharacterSet(charactersIn: "/")) + let encodedPath = rawPath.unicodeScalars.map { allowed.contains($0) ? String($0) : String(format: "%%%02X", $0.value) }.joined() + return URL(string: "sku:\\" + encodedPath) + ?? URL(string: "sku:/UNKOWN")! + } +} + +#if PLAY_TIME +import Playgrounds +#Playground { + if let sku = URL(string: "sku:joe@wildthink.com/role[3]:=12345") + { + print("SKU:", sku) + print("SKU.path:", sku.path) + } else { + print("BAD") + } +} +#endif diff --git a/Sources/Units/Measurement/Formatter.swift b/Sources/Units/Measurement/Formatter.swift index f2dc401..90f92aa 100644 --- a/Sources/Units/Measurement/Formatter.swift +++ b/Sources/Units/Measurement/Formatter.swift @@ -1,11 +1,3 @@ -// -// Formatter.swift -// Units -// (aka Fountation.FormatStyle) -// -// Created by Jason Jobe on 10/24/25. -// - public extension Measurement { struct Formatter { let format: (Measurement) -> Output @@ -48,6 +40,19 @@ extension Measurement.Formatter where Output == String { // MARK: Percent Formatter +// Implementation +import Foundation +extension NumberFormatter { + func string(from measurement: Measurement) -> String { + return "\(self.string(from: .init(value: measurement.value)) ?? "BAD") \(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` public extension Percent { struct Formatter { diff --git a/Sources/Units/Quantum.swift b/Sources/Units/Quantum.swift new file mode 100644 index 0000000..dc81072 --- /dev/null +++ b/Sources/Units/Quantum.swift @@ -0,0 +1,93 @@ +// +// Quantum.swift +// Units +// +// Created by Jason Jobe on 1/17/26. +// + +import Foundation + +/** + Quantum means that which is divisible into two or more constituent parts, + of which each is by nature a one and a this. A quantum is a plurality if it is numerable, + a magnitude if it is measurable. Plurality means that which is divisible potentially into + non-continuous parts, magnitude that which is divisible into continuous parts; + of magnitude, that which is continuous in one dimension is length; + in two breadth, in three depth. + + Of these, limited plurality is number, limited length is a line, breadth a surface, depth a solid. + +— Aristotle, Metaphysics, Book V, Ch. 11-14 + */ + +/* + Plurality - Numerable, Countable + Magnitude - Measurable in Units + Compound/Mixture - + */ + +public struct QuantumType: Equatable, Identifiable, Codable, Sendable { + public var id: String { token } + public let token: String + public let unit: Unit +} + +extension QuantumType: ExpressibleByStringLiteral { + public init(stringLiteral value: String) { + token = value + unit = .none + } + + public init(_ value: any StringProtocol, unit: Unit = .none) { + token = value.description + self.unit = unit + } +} + +public struct Quantum: Equatable { + public let qtype: QuantumType + public var magnitude: Double +} + +public extension Quantum { + var unit: Unit { qtype.unit } + var count: Double { magnitude } + var isPlurality: Bool { unit == .none } + var isMeasurable: Bool { unit != .none } + var countable: Bool { unit == .none } +} + +/* + Equation - Mathematical expressions with single character variables + + Money - System of Currency exchange + Currency: EU, USD, etc + + ## ExtendedSwiftMath + https://swiftpackageindex.com/ChrisGVE/ExtendedSwiftMath + + An extended version of SwiftMath with comprehensive LaTeX + symbol coverage, adding missing mathematical symbols, + blackboard bold, delimiter sizing, amssymb equivalents, + and automatic line wrapping. + + ## thales + https://swiftpackageindex.com/ChrisGVE/thales + Full Documentation on docs.rs + + A comprehensive Computer Algebra System (CAS) library for + symbolic mathematics, equation solving, calculus, and numerical + methods. Named after Thales of Miletus, the first mathematician + in the Greek tradition. + + Features + + - Expression Parsing - Parse mathematical expressions with full operator precedence + - Equation Solving - Linear, quadratic, polynomial, transcendental, and systems of equations + - Calculus - Differentiation, integration, limits, Taylor series, ODEs + - Numerical Methods - Newton-Raphson, bisection, Brent's method when symbolic fails + - Coordinate Systems - 2D/3D transformations, complex numbers, De Moivre's theorem + - Units & Dimensions - Dimensional analysis and unit conversion + - iOS Support - FFI bindings for Swift via swift-bridge + */ +