-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #5 from NeedleInAJayStack/feature/cli-improvements
CLI improvements
- Loading branch information
Showing
16 changed files
with
312 additions
and
171 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import ArgumentParser | ||
import Units | ||
|
||
struct Convert: ParsableCommand { | ||
static var configuration = CommandConfiguration( | ||
abstract: "Convert a measurement to a specified unit.", | ||
discussion: """ | ||
This command uses the unit and measurement serialization format. For more details, see | ||
https://github.com/NeedleInAJayStack/Units/blob/main/README.md#serialization | ||
Note that for convenience, you may use an underscore `_` to represent the normally | ||
serialized space. Also, unless arguments are wrapped in quotes, the `*` character may | ||
need to be escaped. | ||
Run `unit list` to see the available symbols. | ||
EXAMPLES: | ||
unit convert 1_ft m | ||
unit convert 5.4_kW\\*hr J | ||
unit convert 5.4e-3_km/s mi/hr | ||
unit convert "12 kg*m/s^2" "N" | ||
unit convert 12_m^1\\*s^-1\\*kg^1 kg\\*m/s | ||
""" | ||
) | ||
|
||
@Argument(help: "The measurement to convert from") | ||
var from: Measurement | ||
|
||
@Argument(help: "The unit to convert from") | ||
var to: Units.Unit | ||
|
||
func run() throws { | ||
try print(from.convert(to: to)) | ||
} | ||
} | ||
|
||
extension Measurement: ExpressibleByArgument { | ||
public init?(argument: String) { | ||
let argument = argument.replacingOccurrences(of: "_", with: " ") | ||
guard let measurement = Measurement(argument) else { | ||
return nil | ||
} | ||
self = measurement | ||
} | ||
} | ||
|
||
extension Units.Unit: ExpressibleByArgument {} |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
import ArgumentParser | ||
import Units | ||
|
||
struct List: ParsableCommand { | ||
static var configuration = CommandConfiguration( | ||
abstract: "Print a table of the available units, their symbols, and their dimensionality." | ||
) | ||
|
||
func run() throws { | ||
let units = Units.Unit.allDefined().sorted { u1, u2 in | ||
u1.name <= u2.name | ||
} | ||
|
||
let columns = [ | ||
"name", | ||
"symbol", | ||
"dimension", | ||
] | ||
|
||
let rows = units.map { unit in | ||
[ | ||
unit.name, | ||
unit.symbol, | ||
unit.dimensionDescription(), | ||
] | ||
} | ||
|
||
let padding = 2 // The padding between the longest col value and the start of the next col. | ||
let columnWidths = columns.enumerated().map { i, _ in | ||
rows.reduce(0) { maxSoFar, row in | ||
max(row[i].count, maxSoFar) | ||
} + padding | ||
} | ||
|
||
var header = "" | ||
for (i, column) in columns.enumerated() { | ||
header += column.padding( | ||
toLength: columnWidths[i], | ||
withPad: " ", | ||
startingAt: 0 | ||
) | ||
} | ||
let rowStrings = rows.map { row in | ||
var rowString = "" | ||
for (i, value) in row.enumerated() { | ||
rowString += value.padding( | ||
toLength: columnWidths[i], | ||
withPad: " ", | ||
startingAt: 0 | ||
) | ||
} | ||
return rowString | ||
} | ||
|
||
print(header) | ||
print(String(repeating: "-", count: header.count)) | ||
for rowString in rowStrings { | ||
print(rowString) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import ArgumentParser | ||
|
||
struct Unit: ParsableCommand { | ||
static var configuration = CommandConfiguration( | ||
abstract: "A utility for performing unit conversions.", | ||
subcommands: [Convert.self, List.self] | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
ConvertUnit.main() | ||
Unit.main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
/// Serialize the dictionary of object-exponent pairs into a readable equation, populated | ||
/// with `*` for multiplication, `/` for division, and `^` for exponentiation. For example, | ||
/// given `["a": 1, "b": -2, "c": 3]`, it produces the equation string | ||
/// `a*c^3/b^2`. | ||
/// | ||
/// In the result, the input objects are sorted in the following way: | ||
/// - Positive exponents, from smallest to largest | ||
/// - Negative exponents, from smallest to largest | ||
/// - For equal exponents, objects are in alphabetical order by symbol | ||
/// | ||
/// - Parameters: | ||
/// - dict: The object-exponent pairs | ||
/// - symbolPath: The keypath used to produce String symbols for the objects | ||
/// - spaceAroundOperators: Whether to include space characters before and after multiplication and division characters. | ||
/// - Returns: A string that represents the equation of the object symbols and their respective exponentiation. | ||
func serializeSymbolicEquation<T>( | ||
of dict: [T: Int], | ||
symbolPath: KeyPath<T, String>, | ||
spaceAroundOperators: Bool = false | ||
) -> String { | ||
// Sort. The order is: | ||
// - Positive exponents, from smallest to largest | ||
// - Negative exponents, from smallest to largest | ||
// - For equal exponents, units are in alphabetical order by symbol | ||
var list = dict.map { object, exp in | ||
(object, exp) | ||
} | ||
list.sort { lhs, rhs in | ||
if lhs.1 > 0, rhs.1 > 0 { | ||
if lhs.1 == rhs.1 { | ||
return lhs.0[keyPath: symbolPath] < rhs.0[keyPath: symbolPath] | ||
} else { | ||
return lhs.1 < rhs.1 | ||
} | ||
} else if lhs.1 > 0, rhs.1 < 0 { | ||
return true | ||
} else if lhs.1 < 0, rhs.1 > 0 { | ||
return false | ||
} else { // lhs.1 < 0 && rhs.1 > 0 | ||
if lhs.1 == rhs.1 { | ||
return lhs.0[keyPath: symbolPath] < rhs.0[keyPath: symbolPath] | ||
} else { | ||
return lhs.1 > rhs.1 | ||
} | ||
} | ||
} | ||
|
||
// Build up equation, using correct operators | ||
let expSymbol = String(OperatorSymbols.exp.rawValue) | ||
var multSymbol = String(OperatorSymbols.mult.rawValue) | ||
var divSymbol = String(OperatorSymbols.div.rawValue) | ||
if spaceAroundOperators { | ||
multSymbol = " \(multSymbol) " | ||
divSymbol = " \(divSymbol) " | ||
} | ||
|
||
var equation = "" | ||
for (object, exp) in list { | ||
guard exp != 0 else { | ||
break | ||
} | ||
|
||
var prefix = "" | ||
if equation == "" { // first symbol | ||
if exp >= 0 { | ||
prefix = "" | ||
} else { | ||
prefix = "1\(divSymbol)" | ||
} | ||
} else { | ||
if exp >= 0 { | ||
prefix = multSymbol | ||
} else { | ||
prefix = divSymbol | ||
} | ||
} | ||
let symbol = object[keyPath: symbolPath] | ||
var expStr = "" | ||
if abs(exp) > 1 { | ||
expStr = "\(expSymbol)\(abs(exp))" | ||
} | ||
|
||
equation += "\(prefix)\(symbol)\(expStr)" | ||
} | ||
return equation | ||
} | ||
|
||
/// Deserialize the equation into a dictionary of String-exponent pairs. | ||
/// This is intended to be used on equations produced by | ||
/// `serializeSymbolicEquation` | ||
/// | ||
/// - Parameter equation: The equation to deserialize | ||
/// - Returns: A dictionary containing object symbols and exponents | ||
func deserializeSymbolicEquation( | ||
_ equation: String | ||
) throws -> [String: Int] { | ||
let expSymbol = OperatorSymbols.exp.rawValue | ||
let multSymbol = OperatorSymbols.mult.rawValue | ||
let divSymbol = OperatorSymbols.div.rawValue | ||
|
||
var result = [String: Int]() | ||
for multChunks in equation.split(separator: multSymbol, omittingEmptySubsequences: false) { | ||
for (index, divChunks) in multChunks.split(separator: divSymbol, omittingEmptySubsequences: false).enumerated() { | ||
let symbolChunks = divChunks.split(separator: expSymbol, omittingEmptySubsequences: false) | ||
let subSymbol = String(symbolChunks[0]).trimmingCharacters(in: .whitespaces) | ||
var exp = 1 | ||
if symbolChunks.count == 2 { | ||
guard let expInt = Int(String(symbolChunks[1])) else { | ||
throw UnitError.invalidSymbol(message: "Symbol '^' must be followed by an integer: \(equation)") | ||
} | ||
exp = expInt | ||
} | ||
if index > 0 { | ||
exp = -1 * exp | ||
} | ||
guard subSymbol != "1" else { | ||
continue | ||
} | ||
guard subSymbol != "" else { | ||
throw UnitError.unitNotFound(message: "Expected subsymbol missing") | ||
} | ||
|
||
if let existingExp = result[subSymbol] { | ||
result[subSymbol] = existingExp + exp | ||
} else { | ||
result[subSymbol] = exp | ||
} | ||
} | ||
} | ||
return result | ||
} | ||
|
||
/// String operator representations. Since composite units may be parsed from symbols, | ||
/// these must be disallowed in defined unit symbols. | ||
enum OperatorSymbols: Character, CaseIterable { | ||
case mult = "*" | ||
case div = "/" | ||
case exp = "^" | ||
} |
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.