feat: add calculator and unit conversion
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
import ConjureKit
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
@Test
|
||||
func calculatorHonorsPrecedenceUnaryAndRightAssociativePowers() {
|
||||
let examples: [(String, String)] = [
|
||||
("2 + 3 * 4", "14"), ("(2 + 3) * 4", "20"),
|
||||
("2^3^2", "512"), ("-2^2", "-4"), ("(-2)^2", "4"),
|
||||
("2^-2", "0.25"), ("--2 + +3", "5"),
|
||||
("8 ÷ 2 × 3", "12"), ("2x3", "6"), ("2 x (3 + 1)", "8"),
|
||||
("10 % 3", "1"), ("10 mod -3", "1"),
|
||||
]
|
||||
for (query, expected) in examples {
|
||||
let result = Calculator.evaluate(query)
|
||||
#expect(result?.displayValue == expected, "\(query)")
|
||||
#expect(result?.copyValue == expected, "\(query)")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func calculatorSupportsFunctionsPercentagesAndScientificNotation() {
|
||||
let examples: [(String, String)] = [
|
||||
("sqrt(81) + abs(-3)", "12"), ("round(2.5) + floor(2.9) + ceil(2.1)", "8"),
|
||||
("sin(pi/2) + cos(0) + tan(0)", "2"), ("log(100) + ln(e)", "3"),
|
||||
("50%", "0.5"), ("200 * 10%", "20"), ("50% + 2", "2.5"),
|
||||
("52% of 900", "468"), ("20% off 80", "64"), ("20% off (80 + 20)", "80"),
|
||||
(".5e2 + 1E-2", "50.01"), ("π - pi", "0"),
|
||||
("pi", "3.14159265359"), ("e", "2.71828182846"), ("1 / 3", "0.333333333333"),
|
||||
]
|
||||
for (query, expected) in examples {
|
||||
#expect(Calculator.evaluate(query)?.copyValue == expected, "\(query)")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func calculatorConvertsDimensionsAndTemperatureOffsets() throws {
|
||||
let examples: [(String, Double, String)] = [
|
||||
("10 km to mi", 6.21371192237, "mi"), ("10ft in m", 3.048, "m"),
|
||||
("29 inches to cm", 73.66, "cm"), ("2 * 5 cm to inches", 3.93700787402, "in"),
|
||||
("1 kilograms to pounds", 2.20462262185, "lb"), ("16oz to lb", 1, "lb"),
|
||||
("32 f to c", 0, "°C"), ("23C to F", 73.4, "°F"),
|
||||
("100°C to °F", 212, "°F"), ("0 c to kelvin", 273.15, "K"),
|
||||
("2 hours in minutes", 120, "min"), ("1 week as seconds", 604800, "s"),
|
||||
("1000 milliseconds to s", 1, "s"),
|
||||
("36 km/h to m/s", 10, "m/s"), ("60 mph to km/h", 96.56064, "km/h"),
|
||||
("1 square m to square cm", 10000, "cm²"), ("144in² to ft²", 1, "ft²"),
|
||||
("2 liters to ml", 2000, "mL"), ("1 US gallon to l", 3.785411784, "L"),
|
||||
("1 tbsp to tsp", 3, "tsp"), ("1 cup to fl oz", 8, "fl oz"),
|
||||
]
|
||||
for (query, expected, unit) in examples {
|
||||
let result = try #require(Calculator.evaluate(query), "\(query)")
|
||||
let copied = try #require(result.copyValue, "\(query): \(result.displayValue)")
|
||||
#expect(copied.hasSuffix(" \(unit)"), "\(query)")
|
||||
let value = try #require(Double(copied.dropLast(unit.count + 1)), "\(query)")
|
||||
// Foundation's built-in physical-unit coefficients have limited decimal precision.
|
||||
#expect(abs(value - expected) < max(1e-9, abs(expected) * 1e-5), "\(query): \(copied)")
|
||||
}
|
||||
#expect(Calculator.evaluate("32 f to c")?.copyValue == "0 °C")
|
||||
#expect(Calculator.evaluate("0 c to f")?.copyValue == "32 °F")
|
||||
}
|
||||
|
||||
@Test
|
||||
func calculatorDistinguishesDecimalBinaryAndBitsFromBytes() {
|
||||
let examples: [(String, String)] = [
|
||||
("1 GB to MB", "1000 MB"), ("1GB to B", "1000000000 B"),
|
||||
("1 GiB to MiB", "1024 MiB"), ("1 KiB to B", "1024 B"),
|
||||
("1 MB to KiB", "976.5625 KiB"), ("8b to B", "1 B"),
|
||||
("1 B to b", "8 b"), ("1 Mb to MB", "0.125 MB"), ("8 bits to bytes", "1 B"),
|
||||
("1 kilobyte to bytes", "1000 B"), ("1 mebibyte to KiB", "1024 KiB"),
|
||||
("8 megabits to MB", "1 MB"),
|
||||
]
|
||||
for (query, expected) in examples {
|
||||
#expect(Calculator.evaluate(query)?.copyValue == expected, "\(query)")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func calculatorLeavesApplicationQueriesAndIncompleteInputAlone() {
|
||||
for query in ["", "Safari", "Xcode", "1Password", "Calculator", "123", "-123", "1e3",
|
||||
"2 +", "sqrt(", "2^", "1e+", "(1+2", "2 3", "2 ** 3", "pi app", "sin 2",
|
||||
"alert(1)", "foo(2)", "1/0 Safari", "10 km to", "10 km to widgets", "2 of 3", "20 off 80"] {
|
||||
#expect(Calculator.evaluate(query) == nil, "\(query)")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func calculatorReportsRecognizedErrorsWithoutCopyableResults() throws {
|
||||
for query in ["1/0", "1 mod 0", "sqrt(-1)", "ln(0)", "log(-2)", "(-2)^0.5",
|
||||
"1e308 * 1e308", "1e999 + 1", "1e308 TB to B", "10 m to kg", "1 GB to seconds"] {
|
||||
let result = try #require(Calculator.evaluate(query), "\(query)")
|
||||
#expect(result.copyValue == nil, "\(query)")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func calculatorBoundsLiveSearchInputAndRecursiveGrammar() {
|
||||
#expect(Calculator.evaluate(String(repeating: "(", count: 70) + "1" + String(repeating: ")", count: 70)) == nil)
|
||||
#expect(Calculator.evaluate(String(repeating: "(", count: 16) + "1" + String(repeating: ")", count: 16)) == nil)
|
||||
#expect(Calculator.evaluate(String(repeating: "-", count: 70) + "1 + 2") == nil)
|
||||
#expect(Calculator.evaluate(String(repeating: "2^", count: 70) + "2") == nil)
|
||||
#expect(Calculator.evaluate(String(repeating: "1+", count: 300) + "1") == nil)
|
||||
#expect(Calculator.evaluate(String(repeating: "(", count: 15) + "1+2" + String(repeating: ")", count: 15))?.copyValue == "3")
|
||||
}
|
||||
Reference in New Issue
Block a user