Skip to content
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
38 changes: 38 additions & 0 deletions Justfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Units Justfile
# Command line support


# About Units Commands
_default:
@echo '{{ style("warning") }}Units Script Commands{{ NORMAL }}'
@echo @{{source_file()}}
@echo ""
@just -f {{source_file()}} --list

# Install 'units' in /usr/local/bin
install:
swift build -c release
cp .build/release/unit /usr/local/bin/

# rm 'units' from /usr/local/bin
uninstall:
rm /usr/local/bin/unit

# Add swiftformat to git/pre-commit
dev_setup:
echo "./Scripts/git_commit_hook.sh" > .git/hooks/pre-commit

# Open Documentation
docs:
open https://swiftpackageindex.com/NeedleInAJayStack/Units/v1.0.0/documentation/units

git_origin := `git remote get-url origin`

# repo/origin
repo:
@echo {{git_origin}}

# open repo/origin
open-repo:
open {{git_origin}}

1 change: 1 addition & 0 deletions Sources/CLI/Convert.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ struct Convert: ParsableCommand {
@Argument(help: """
The unit to convert to. This can either be a unit name, a unit symbol, or an equation of \
unit symbols.
Example: unit convert 1_ft meter -> 0.3048 m
""")
var to: Units.Unit

Expand Down
16 changes: 15 additions & 1 deletion Sources/CLI/List.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ struct List: ParsableCommand {
abstract: "Print a table of the available units, their symbols, and their dimensionality."
)

@Option(name: .shortAndLong,
help: "Substring to filter on dimensions and symbols")
var filter: String? = nil

func run() throws {
let units = registry.allUnits().sorted { u1, u2 in
u1.name <= u2.name
Expand All @@ -17,7 +21,9 @@ struct List: ParsableCommand {
"dimension",
]

let rows = units.map { unit in
let rows = units
.filter({ $0.contains(filter)})
.map { unit in
[
unit.name,
unit.symbol,
Expand Down Expand Up @@ -59,3 +65,11 @@ struct List: ParsableCommand {
}
}
}

extension Units.Unit {
func contains(_ substring: String?) -> Bool {
guard let substring else { return true }
return self.symbol.contains(substring)
|| self.dimensionDescription().contains(substring)
}
}
48 changes: 33 additions & 15 deletions Sources/Units/Expression.swift
Original file line number Diff line number Diff line change
Expand Up @@ -107,19 +107,27 @@ public final class Expression {
while let next = left.next {
let right = next.node
switch (left.value, right.value) {
case let (.measurement(leftMeasurement), .measurement(rightMeasurement)):
switch next.op {
case .add, .subtract: // Skip over operation
left = right
case .multiply: // Compute and absorb right node into left
left.value = .measurement(leftMeasurement * rightMeasurement)
left.next = right.next
case .divide: // Compute and absorb right node into left
left.value = .measurement(leftMeasurement / rightMeasurement)
left.next = right.next
}
default:
fatalError("Parentheses still present during multiplication phase")
case let (.measurement(leftMeasurement), .measurement(rightMeasurement)):
switch next.op {
case .add, .subtract: // Skip over operation
left = right
case .multiply: // Compute and absorb right node into left
if let percent = rightMeasurement.asPercent {
left.value = .measurement(leftMeasurement * percent)
} else {
left.value = .measurement(leftMeasurement * rightMeasurement)
}
left.next = right.next
case .divide: // Compute and absorb right node into left
if let percent = rightMeasurement.asPercent {
left.value = .measurement(leftMeasurement / percent)
} else {
left.value = .measurement(leftMeasurement / rightMeasurement)
}
left.next = right.next
}
default:
fatalError("Parentheses still present during multiplication phase")
}
}

Expand All @@ -130,11 +138,21 @@ public final class Expression {
switch (left.value, right.value) {
case let (.measurement(leftMeasurement), .measurement(rightMeasurement)):
switch next.op {

case .add: // Compute and absorb right node into left
left.value = try .measurement(leftMeasurement + rightMeasurement)
// NOTE: Exceptional handling of Percent
if let percent = rightMeasurement.asPercent {
left.value = .measurement(leftMeasurement + percent)
} else {
left.value = try .measurement(leftMeasurement + rightMeasurement)
}
left.next = right.next
case .subtract: // Compute and absorb right node into left
left.value = try .measurement(leftMeasurement - rightMeasurement)
if let percent = rightMeasurement.asPercent {
left.value = .measurement(leftMeasurement - percent)
} else {
left.value = try .measurement(leftMeasurement - rightMeasurement)
}
left.next = right.next
case .multiply, .divide:
fatalError("Multiplication still present during addition phase")
Expand Down
203 changes: 203 additions & 0 deletions Sources/Units/Measurement/Percent+Measurement.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import Foundation
/*
NOTE: Should consider introducing `protocol Scalar`
based on `VectorArithmetic`
*/

/**
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.

Here’s how math operators work with percentages in typical calculations:

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.

Example Table
===========
Expression Decimal Form Result
------------------------------
100 * 25% 100 * 0.25 25
100 / 30% 100 / 0.3 333.33
100 + 10% 100 + (100*0.10) 110


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 private(set) var magnitude: Double

/// Create a new Percent
/// - Parameters:
/// - value: The magnitude of the percent
public init(
magnitude: Double
) {
self.magnitude = magnitude
}

func percent(of measure: Measurement) -> Measurement {
.init(value: magnitude * measure.value, unit: measure.unit)
}

func percent(of other: Double) -> Double {
magnitude * other
}
}

extension Measurement {
public var isPercent: Bool {
self.unit == Percent.unit
}

public var asPercent: Percent? {
isPercent ? Percent(magnitude: self.value/100) : nil
}
}

// MARK: Percent as Unit
extension Percent {
public var unit: Unit { Self.unit }

public static let unit = Unit(
definedBy: DefaultUnits.percent)
}

// MARK: Numeric Conformance
public extension Percent {
init(integerLiteral value: Double) {
magnitude = value
}

static func *= (lhs: inout Percent, rhs: Percent) {
lhs.magnitude *= rhs.magnitude
}

static func - (lhs: Percent, rhs: Percent) -> Percent {
Percent(magnitude: lhs.magnitude - rhs.magnitude)
}

init?<T>(exactly source: T) where T : BinaryInteger {
magnitude = Double(source)
}

static func * (lhs: Percent, rhs: Percent) -> Percent {
Percent(magnitude: lhs.magnitude * rhs.magnitude)
}

static func + (lhs: Percent, rhs: Percent) -> Percent {
Percent(magnitude: lhs.magnitude + rhs.magnitude)
}
}

postfix operator %

extension BinaryInteger {
public static postfix func % (value: Self) -> Percent {
Percent(magnitude: Double(value)/100)
}
}

extension BinaryFloatingPoint {
public static postfix func % (value: Self) -> Percent {
Percent(magnitude: Double(value)/100)
}
}

// AdditiveArithmetic operations `*` and `/`

public extension Measurement {
/// Adds a percentage to a measurement by increasing its value by the given percent.
/// - Parameters:
/// - lhs: The base measurement.
/// - rhs: The percentage to add.
/// - Returns: A new `Measurement` with its value increased by the given percentage.
@_disfavoredOverload
static func + (lhs: Measurement, rhs: Percent) -> Measurement {
return Measurement(
value: lhs.value + rhs.percent(of: lhs.value),
unit: lhs.unit
)
}

/// Subtracts a percentage from a measurement by decreasing its value by the given percent.
/// - Parameters:
/// - lhs: The base measurement.
/// - rhs: The percentage to subtract.
/// - Returns: A new `Measurement` with its value decreased by the given percentage.
@_disfavoredOverload
static func - (lhs: Measurement, rhs: Percent) -> Measurement {
return Measurement(
value: lhs.value - rhs.percent(of: lhs.value),
unit: lhs.unit
)
}

/// Increases a measurement in place by the given percentage.
/// - Parameters:
/// - lhs: The measurement to modify.
/// - rhs: The percentage to add.
@_disfavoredOverload
static func += (lhs: inout Measurement, rhs: Percent) {
lhs = lhs + rhs
}

/// Decreases a measurement in place by the given percentage.
/// - Parameters:
/// - lhs: The measurement to modify.
/// - rhs: The percentage to subtract.
@_disfavoredOverload
static func -= (lhs: inout Measurement, rhs: Percent) {
lhs = lhs - rhs
}

}

// Scalar operations `*` and `/`
public extension Measurement {
/// Multiplies a measurement by a percentage, treating the percent as a scalar (e.g., 25% = 0.25).
/// - Parameters:
/// - lhs: The base measurement to scale.
/// - rhs: The percentage factor.
/// - Returns: A new `Measurement` whose value is `lhs.value * rhs.magnitude` with the same unit.
@_disfavoredOverload
static func * (lhs: Measurement, rhs: Percent) -> Measurement {
return Measurement(
value: lhs.value * rhs.magnitude,
unit: lhs.unit
)
}

/// Divides a measurement by a percentage, treating the percent as a scalar (e.g., 25% = 0.25).
/// - Parameters:
/// - lhs: The base measurement to scale.
/// - rhs: The percentage divisor.
/// - Returns: A new `Measurement` whose value is `lhs.value / rhs.magnitude` with the same unit.
@_disfavoredOverload
static func / (lhs: Measurement, rhs: Percent) -> Measurement {
return Measurement(
value: lhs.value / rhs.magnitude,
unit: lhs.unit
)
}
}
3 changes: 3 additions & 0 deletions Sources/Units/RegistryBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,9 @@ public class RegistryBuilder {
DefaultUnits.troyOunces,
DefaultUnits.slug,

// MARK: Percent
DefaultUnits.percent,

// MARK: Power

DefaultUnits.watt,
Expand Down
9 changes: 9 additions & 0 deletions Sources/Units/Unit/DefaultUnits.swift
Original file line number Diff line number Diff line change
Expand Up @@ -818,6 +818,15 @@ enum DefaultUnits {
coefficient: 14.5939
)

// MARK: Percent

static let percent = try! DefinedUnit(
name: "percent",
symbol: "%",
dimension: [:],
coefficient: 0.01
)

// MARK: Power

// Base unit: watt
Expand Down
4 changes: 4 additions & 0 deletions Sources/Units/Unit/Unit+DefaultUnits.swift
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,10 @@ public extension Unit {
static let troyOunces = Unit(definedBy: DefaultUnits.troyOunces)
static let slug = Unit(definedBy: DefaultUnits.slug)

// MARK: Percent

static let percent = Unit(definedBy: DefaultUnits.percent)

// MARK: Power

static let watt = Unit(definedBy: DefaultUnits.watt)
Expand Down
Loading