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") }