This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

more tests

Phillip Carter (May 22, 2026, 1:49 PM -0700) 99c63d5c b967cf09

+606 -76
+11 -1
Calc/CalculatorEngine.swift
··· 323 323 } 324 324 325 325 private mutating func parseIdentifierOrFunction() throws -> CalculatorNumber { 326 - let identifier = parseIdentifier().lowercased() 326 + var identifier = parseIdentifier().lowercased() 327 + 328 + if identifier == "log", matchesTrailingLogTenSuffix { 329 + index += 2 330 + identifier = "log10" 331 + } 327 332 328 333 switch identifier { 329 334 case "pi": ··· 499 504 private var currentCharacter: Character? { 500 505 guard !isAtEnd else { return nil } 501 506 return characters[index] 507 + } 508 + 509 + private var matchesTrailingLogTenSuffix: Bool { 510 + guard index + 1 < characters.count else { return false } 511 + return characters[index] == "1" && characters[index + 1] == "0" 502 512 } 503 513 }
+595 -75
CalcTests/CalculatorEngineTests.swift
··· 1 + import AppKit 1 2 import Foundation 2 3 import XCTest 3 4 @testable import Calc 4 5 5 6 final class CalculatorEngineTests: XCTestCase { 6 7 private let engine = CalculatorEngine() 8 + private let groupingSeparator = Locale.current.groupingSeparator ?? "," 7 9 8 10 private func assertEvaluates(_ expression: String, equals expected: String, file: StaticString = #filePath, line: UInt = #line) throws { 9 11 let result = try engine.evaluate(expression) ··· 19 21 XCTAssertEqual(doubleValue, expected, accuracy: accuracy, file: file, line: line) 20 22 } 21 23 24 + private func assertThrows(_ expression: String, equals expected: CalculatorError, file: StaticString = #filePath, line: UInt = #line) { 25 + XCTAssertThrowsError(try engine.evaluate(expression), file: file, line: line) { error in 26 + XCTAssertEqual(error as? CalculatorError, expected, file: file, line: line) 27 + } 28 + } 29 + 22 30 func testBasicPrecedence() throws { 23 31 try assertEvaluates("2+3*4", equals: "14") 24 32 } 25 33 26 - func testParentheses() throws { 27 - try assertEvaluates("2*(3+4)", equals: "14") 34 + func testNestedParenthesesWithWhitespace() throws { 35 + try assertEvaluates(" 2 * ( 3 + (4 - 1) ) ", equals: "12") 36 + } 37 + 38 + func testMultiplicationAndDivisionAreLeftAssociative() throws { 39 + try assertEvaluates("8/4/2", equals: "1") 28 40 } 29 41 30 42 func testExponentIsRightAssociative() throws { ··· 39 51 try assertEvaluates("(-2)^2", equals: "4") 40 52 } 41 53 54 + func testUnaryPlusReturnsSameValue() throws { 55 + try assertEvaluates("+(2+3)", equals: "5") 56 + } 57 + 58 + func testLeadingDecimalValueParses() throws { 59 + try assertEvaluates(".5+.25", equals: "0.75") 60 + } 61 + 62 + func testTrailingDecimalValueParses() throws { 63 + try assertEvaluates("5.+2", equals: "7") 64 + } 65 + 42 66 func testNegativeExponent() throws { 43 67 try assertEvaluates("2^-3", equals: "0.125") 44 68 } 45 69 46 - func testDivisionByZeroThrows() { 47 - XCTAssertThrowsError(try engine.evaluate("1/0")) { error in 48 - XCTAssertEqual(error as? CalculatorError, .divisionByZero) 49 - } 70 + func testScientificNotationWithPositiveExponentParses() throws { 71 + try assertEvaluates("1e3+2", equals: "1002") 72 + } 73 + 74 + func testScientificNotationWithNegativeExponentParses() throws { 75 + try assertEvaluates("1e-3*1000", equals: "1") 76 + } 77 + 78 + func testUnicodeOperatorsAndSquareRootAreNormalized() throws { 79 + try assertEvaluates("√(16)+2×3+8÷2", equals: "14") 80 + } 81 + 82 + func testPiConstantEvaluatesApproximately() throws { 83 + try assertEvaluates("pi", approximately: Double.pi) 50 84 } 51 85 52 86 func testNaturalLogKeyword() throws { 53 87 try assertEvaluates("ln(e)", approximately: 1) 54 88 } 55 89 56 - func testScientificFunctionsAndConstants() throws { 90 + func testPrimaryTrigFunctionsAndConstants() throws { 57 91 try assertEvaluates("sin(pi/2)+cos(0)+tan(0)", approximately: 2) 58 92 } 59 93 94 + func testInverseTrigFunctions() throws { 95 + try assertEvaluates("asin(1)+acos(1)+atan(1)", approximately: (.pi / 2) + 0 + (.pi / 4)) 96 + } 97 + 98 + func testAbsAndExpFunctions() throws { 99 + try assertEvaluates("abs(-5)+exp(0)", equals: "6") 100 + } 101 + 60 102 func testLogAndSquareRoot() throws { 61 - try assertEvaluates("sqrt(16)+log(1000)", approximately: 7) 103 + try assertEvaluates("sqrt(16)+log(1000)+log10(100)", approximately: 9) 62 104 } 63 105 64 106 func testFunctionWithoutParentheses() throws { 65 - try assertEvaluates("ln2", approximately: Foundation.log(2)) 107 + try assertEvaluates("sqrt16+abs-5", equals: "9") 66 108 } 67 109 68 110 func testGroupedNumberExpression() throws { 69 111 try assertEvaluates("330,000-260000", equals: "70000") 70 112 } 71 113 72 - func testLargeValueFormatsExponentially() { 73 - XCTAssertEqual(engine.format(CalculatorNumber("1250000000000")), "1.25e12") 74 - } 75 - 76 114 func testScientificNotationExpressionParses() throws { 77 115 try assertEvaluates("1.25e12-250000000000", equals: "1000000000000") 78 116 } ··· 85 123 try assertEvaluates("0.1+0.2", equals: "0.3") 86 124 } 87 125 126 + func testExpressionStringTrimsTrailingZeroes() { 127 + XCTAssertEqual(engine.expressionString(CalculatorNumber("123.4500")), "123.45") 128 + } 129 + 130 + func testThresholdValueFormatsWithoutScientificNotation() { 131 + let expected = "1\(groupingSeparator)000\(groupingSeparator)000\(groupingSeparator)000\(groupingSeparator)000" 132 + XCTAssertEqual(engine.format(CalculatorNumber("1000000000000")), expected) 133 + } 134 + 135 + func testLargeValueFormatsExponentially() { 136 + XCTAssertEqual(engine.format(CalculatorNumber("1250000000000")), "1.25e12") 137 + } 138 + 139 + func testNegativeLargeValueFormatsExponentially() { 140 + XCTAssertEqual(engine.format(CalculatorNumber("-1250000000000")), "-1.25e12") 141 + } 142 + 143 + func testSmallerDecimalValuesFormatWithGrouping() { 144 + XCTAssertEqual(engine.format(CalculatorNumber("1234567.89")), "1\(groupingSeparator)234\(groupingSeparator)567.89") 145 + } 146 + 147 + func testNumericStringDisplayFormattingPreservesTrailingDecimalPoint() { 148 + XCTAssertEqual(engine.formatNumericStringForDisplay("1234."), "1\(groupingSeparator)234.") 149 + } 150 + 151 + func testNumericStringDisplayFormattingNormalizesScientificNotation() { 152 + XCTAssertEqual(engine.formatNumericStringForDisplay("1.25E12"), "1.25e12") 153 + } 154 + 155 + func testDivisionByZeroThrows() { 156 + assertThrows("1/0", equals: .divisionByZero) 157 + } 158 + 159 + func testEmptyExpressionThrows() { 160 + assertThrows(" ", equals: .emptyExpression) 161 + } 162 + 163 + func testInvalidCharacterThrows() { 164 + assertThrows("2%3", equals: .invalidCharacter("%")) 165 + } 166 + 167 + func testMissingClosingParenthesisThrows() { 168 + assertThrows("(1+2", equals: .missingClosingParenthesis) 169 + } 170 + 171 + func testUnexpectedEndOfExpressionThrows() { 172 + assertThrows("1+", equals: .unexpectedEndOfExpression) 173 + } 174 + 175 + func testMalformedNumberThrows() { 176 + assertThrows(".", equals: .malformedNumber) 177 + } 178 + 88 179 func testUnknownFunctionThrows() { 89 - XCTAssertThrowsError(try engine.evaluate("foo(1)")) { error in 90 - XCTAssertEqual(error as? CalculatorError, .invalidFunction("foo")) 91 - } 180 + assertThrows("foo(1)", equals: .invalidFunction("foo")) 92 181 } 93 182 94 183 func testSquareRootDomainErrorThrows() { 95 - XCTAssertThrowsError(try engine.evaluate("sqrt(-1)")) { error in 96 - XCTAssertEqual(error as? CalculatorError, .domainError("sqrt")) 97 - } 184 + assertThrows("sqrt(-1)", equals: .domainError("sqrt")) 185 + } 186 + 187 + func testNaturalLogDomainErrorThrows() { 188 + assertThrows("ln(0)", equals: .domainError("ln")) 189 + } 190 + 191 + func testLogDomainErrorThrows() { 192 + assertThrows("log(0)", equals: .domainError("log")) 193 + } 194 + 195 + func testAsinDomainErrorThrows() { 196 + assertThrows("asin(2)", equals: .domainError("asin")) 197 + } 198 + 199 + func testAcosDomainErrorThrows() { 200 + assertThrows("acos(-2)", equals: .domainError("acos")) 98 201 } 99 202 } 100 203 101 204 @MainActor 102 205 final class CalculatorViewModelTests: XCTestCase { 206 + private let historyKey = "calc.history" 207 + private let groupingSeparator = Locale.current.groupingSeparator ?? "," 208 + 209 + override func setUp() { 210 + super.setUp() 211 + UserDefaults.standard.removeObject(forKey: historyKey) 212 + } 213 + 214 + override func tearDown() { 215 + UserDefaults.standard.removeObject(forKey: historyKey) 216 + super.tearDown() 217 + } 218 + 219 + private func makeViewModel() -> CalculatorViewModel { 220 + CalculatorViewModel() 221 + } 222 + 223 + private func input(_ tokens: [String], into viewModel: CalculatorViewModel) { 224 + tokens.forEach(viewModel.input) 225 + } 226 + 227 + private func makeKeyEvent( 228 + characters: String, 229 + keyCode: UInt16 = 0, 230 + modifiers: NSEvent.ModifierFlags = [] 231 + ) -> NSEvent { 232 + guard let event = NSEvent.keyEvent( 233 + with: .keyDown, 234 + location: .zero, 235 + modifierFlags: modifiers, 236 + timestamp: 0, 237 + windowNumber: 0, 238 + context: nil, 239 + characters: characters, 240 + charactersIgnoringModifiers: characters, 241 + isARepeat: false, 242 + keyCode: keyCode 243 + ) else { 244 + fatalError("Failed to create key event") 245 + } 246 + 247 + return event 248 + } 249 + 250 + func testInitialStateUsesPlaceholder() { 251 + let viewModel = makeViewModel() 252 + 253 + XCTAssertEqual(viewModel.expression, "0") 254 + XCTAssertEqual(viewModel.expressionDisplay, "0") 255 + XCTAssertTrue(viewModel.history.isEmpty) 256 + XCTAssertFalse(viewModel.canCopyDisplayValue) 257 + XCTAssertNil(viewModel.errorMessage) 258 + } 259 + 103 260 func testTypingScientificFunctionWithKeyboardCharacters() { 104 - let viewModel = CalculatorViewModel() 261 + let viewModel = makeViewModel() 105 262 106 - viewModel.input("l") 107 - viewModel.input("n") 108 - viewModel.input("(") 109 - viewModel.input("e") 110 - viewModel.input(")") 263 + input(["l", "n", "(", "e", ")"], into: viewModel) 111 264 112 265 XCTAssertEqual(viewModel.expression, "ln(e)") 113 266 } 114 267 268 + func testTypingUppercaseFunctionTokenLowercasesAndInsertsFunction() { 269 + let viewModel = makeViewModel() 270 + 271 + viewModel.input("SIN") 272 + 273 + XCTAssertEqual(viewModel.expression, "sin(") 274 + } 275 + 115 276 func testTypingConstantAfterNumberInsertsMultiplication() { 116 - let viewModel = CalculatorViewModel() 277 + let viewModel = makeViewModel() 278 + 279 + input(["2", "p", "i"], into: viewModel) 280 + 281 + XCTAssertEqual(viewModel.expression, "2*pi") 282 + } 283 + 284 + func testTypingDigitAfterConstantInsertsMultiplication() { 285 + let viewModel = makeViewModel() 117 286 287 + viewModel.input("pi") 118 288 viewModel.input("2") 119 - viewModel.input("p") 120 - viewModel.input("i") 121 289 122 - XCTAssertEqual(viewModel.expression, "2*pi") 290 + XCTAssertEqual(viewModel.expression, "pi*2") 123 291 } 124 292 125 293 func testScientificButtonTokenInsertsFunctionCall() { 126 - let viewModel = CalculatorViewModel() 294 + let viewModel = makeViewModel() 127 295 128 296 viewModel.input("sqrt") 129 297 130 298 XCTAssertEqual(viewModel.expression, "sqrt(") 131 299 } 132 300 133 - func testDisplayValueToCopyReflectsEvaluatedResult() { 134 - let viewModel = CalculatorViewModel() 301 + func testExpTokenInsertsFunctionCall() { 302 + let viewModel = makeViewModel() 135 303 136 - viewModel.input("5") 137 - viewModel.input("+") 138 - viewModel.input("5") 304 + viewModel.input("exp") 305 + 306 + XCTAssertEqual(viewModel.expression, "exp(") 307 + } 308 + 309 + func testScientificFunctionAfterEvaluationReplacesExpression() { 310 + let viewModel = makeViewModel() 311 + 312 + input(["5", "+", "5"], into: viewModel) 313 + viewModel.evaluate() 314 + viewModel.input("sqrt") 315 + 316 + XCTAssertEqual(viewModel.expression, "sqrt(") 317 + } 318 + 319 + func testConstantAfterEvaluationReplacesExpression() { 320 + let viewModel = makeViewModel() 321 + 322 + input(["5", "+", "5"], into: viewModel) 139 323 viewModel.evaluate() 324 + viewModel.input("pi") 325 + 326 + XCTAssertEqual(viewModel.expression, "pi") 327 + } 328 + 329 + func testMultiCharacterInputSplitsIntoExpressionTokens() { 330 + let viewModel = makeViewModel() 331 + 332 + viewModel.input("12+34") 140 333 141 - XCTAssertEqual(viewModel.displayValueToCopy, "10") 142 - XCTAssertTrue(viewModel.canCopyDisplayValue) 334 + XCTAssertEqual(viewModel.expression, "12+34") 143 335 } 144 336 145 337 func testPlaceholderZeroDoesNotEvaluate() { 146 - let viewModel = CalculatorViewModel() 147 - let initialHistoryCount = viewModel.history.count 338 + let viewModel = makeViewModel() 148 339 149 340 viewModel.evaluate() 150 341 151 342 XCTAssertEqual(viewModel.expression, "0") 152 - XCTAssertEqual(viewModel.history.count, initialHistoryCount) 343 + XCTAssertTrue(viewModel.history.isEmpty) 153 344 XCTAssertNil(viewModel.errorMessage) 154 345 } 155 346 156 347 func testEnteredZeroStillEvaluates() { 157 - let viewModel = CalculatorViewModel() 158 - let initialHistoryCount = viewModel.history.count 348 + let viewModel = makeViewModel() 159 349 160 350 viewModel.input("0") 161 351 viewModel.evaluate() 162 352 163 353 XCTAssertEqual(viewModel.expression, "0") 354 + XCTAssertEqual(viewModel.history.count, 1) 164 355 XCTAssertEqual(viewModel.history.first?.result, "0") 165 - XCTAssertEqual(viewModel.history.count, initialHistoryCount + 1) 356 + XCTAssertNil(viewModel.errorMessage) 357 + } 358 + 359 + func testClearExpressionResetsToPlaceholder() { 360 + let viewModel = makeViewModel() 361 + 362 + input(["1", "+", "2"], into: viewModel) 363 + viewModel.clearExpression() 364 + 365 + XCTAssertEqual(viewModel.expression, "0") 366 + XCTAssertEqual(viewModel.expressionDisplay, "0") 367 + XCTAssertFalse(viewModel.canCopyDisplayValue) 166 368 XCTAssertNil(viewModel.errorMessage) 167 369 } 168 370 169 - func testExpressionDisplayFormatsTypedLargeNumber() { 170 - let viewModel = CalculatorViewModel() 371 + func testDeleteBackwardOnPlaceholderDoesNothing() { 372 + let viewModel = makeViewModel() 373 + 374 + viewModel.deleteBackward() 375 + 376 + XCTAssertEqual(viewModel.expression, "0") 377 + } 378 + 379 + func testDeleteBackwardRestoresPlaceholderWhenLastCharacterRemoved() { 380 + let viewModel = makeViewModel() 381 + 382 + viewModel.input("5") 383 + viewModel.deleteBackward() 384 + 385 + XCTAssertEqual(viewModel.expression, "0") 386 + XCTAssertFalse(viewModel.canCopyDisplayValue) 387 + } 388 + 389 + func testDecimalAtStartCreatesZeroPrefix() { 390 + let viewModel = makeViewModel() 391 + 392 + viewModel.input(".") 393 + 394 + XCTAssertEqual(viewModel.expression, "0.") 395 + } 396 + 397 + func testDuplicateDecimalPointIsIgnored() { 398 + let viewModel = makeViewModel() 171 399 172 - for token in ["2", "6", "0", "0", "0", "0"] { 173 - viewModel.input(token) 400 + input(["1", ".", "2", "."], into: viewModel) 401 + 402 + XCTAssertEqual(viewModel.expression, "1.2") 403 + } 404 + 405 + func testDecimalAfterOperatorInsertsZeroPrefix() { 406 + let viewModel = makeViewModel() 407 + 408 + input(["1", "+", "."], into: viewModel) 409 + 410 + XCTAssertEqual(viewModel.expression, "1+0.") 411 + } 412 + 413 + func testDecimalAfterConstantInsertsMultiplication() { 414 + let viewModel = makeViewModel() 415 + 416 + viewModel.input("pi") 417 + viewModel.input(".") 418 + 419 + XCTAssertEqual(viewModel.expression, "pi*0.") 420 + } 421 + 422 + func testDecimalAfterEvaluationStartsNewExpression() { 423 + let viewModel = makeViewModel() 424 + 425 + input(["1", "+", "1"], into: viewModel) 426 + viewModel.evaluate() 427 + viewModel.input(".") 428 + 429 + XCTAssertEqual(viewModel.expression, "0.") 430 + } 431 + 432 + func testOperatorOnPlaceholderOnlyAllowsMinus() { 433 + let viewModel = makeViewModel() 434 + 435 + viewModel.input("+") 436 + XCTAssertEqual(viewModel.expression, "0") 437 + 438 + viewModel.input("-") 439 + XCTAssertEqual(viewModel.expression, "-") 440 + } 441 + 442 + func testOperatorReplacesPreviousOperator() { 443 + let viewModel = makeViewModel() 444 + 445 + input(["1", "+", "*"], into: viewModel) 446 + 447 + XCTAssertEqual(viewModel.expression, "1*") 448 + } 449 + 450 + func testOperatorAfterLeftParenthesisAllowsOnlyMinus() { 451 + let viewModel = makeViewModel() 452 + 453 + viewModel.input("(") 454 + viewModel.input("+") 455 + XCTAssertEqual(viewModel.expression, "(") 456 + 457 + viewModel.input("-") 458 + XCTAssertEqual(viewModel.expression, "(-") 459 + } 460 + 461 + func testOperatorAfterTrailingDecimalAppendsZero() { 462 + let viewModel = makeViewModel() 463 + 464 + input(["1", "."], into: viewModel) 465 + viewModel.input("+") 466 + 467 + XCTAssertEqual(viewModel.expression, "1.0+") 468 + } 469 + 470 + func testOperatorAfterIncompleteIdentifierIsIgnored() { 471 + let viewModel = makeViewModel() 472 + 473 + viewModel.input("l") 474 + viewModel.input("+") 475 + 476 + XCTAssertEqual(viewModel.expression, "l") 477 + } 478 + 479 + func testLeftParenthesisAfterNumberInsertsMultiplication() { 480 + let viewModel = makeViewModel() 481 + 482 + input(["2", "("], into: viewModel) 483 + 484 + XCTAssertEqual(viewModel.expression, "2*(") 485 + } 486 + 487 + func testLeftParenthesisAfterConstantInsertsMultiplication() { 488 + let viewModel = makeViewModel() 489 + 490 + viewModel.input("pi") 491 + viewModel.input("(") 492 + 493 + XCTAssertEqual(viewModel.expression, "pi*(") 494 + } 495 + 496 + func testLeftParenthesisAfterFunctionNameDoesNotInsertMultiplication() { 497 + let viewModel = makeViewModel() 498 + 499 + input(["l", "n", "("], into: viewModel) 500 + 501 + XCTAssertEqual(viewModel.expression, "ln(") 502 + } 503 + 504 + func testRightParenthesisIgnoredWithoutOpenParenthesis() { 505 + let viewModel = makeViewModel() 506 + 507 + viewModel.input(")") 508 + 509 + XCTAssertEqual(viewModel.expression, "0") 510 + } 511 + 512 + func testRightParenthesisIgnoredWhenExpressionEndsInOperator() { 513 + let viewModel = makeViewModel() 514 + 515 + input(["(", "1", "+"], into: viewModel) 516 + viewModel.input(")") 517 + 518 + XCTAssertEqual(viewModel.expression, "(1+") 519 + } 520 + 521 + func testRightParenthesisClosesValidExpression() { 522 + let viewModel = makeViewModel() 523 + 524 + input(["(", "1", ")"], into: viewModel) 525 + 526 + XCTAssertEqual(viewModel.expression, "(1)") 527 + } 528 + 529 + func testEvaluateStoresFormattedHistoryRecord() { 530 + let viewModel = makeViewModel() 531 + 532 + input(["3", "3", "0", "0", "0", "0", "-", "2", "6", "0", "0", "0", "0"], into: viewModel) 533 + viewModel.evaluate() 534 + 535 + XCTAssertEqual(viewModel.history.count, 1) 536 + XCTAssertEqual(viewModel.history.first?.expression, "330\(groupingSeparator)000-260\(groupingSeparator)000") 537 + XCTAssertEqual(viewModel.history.first?.result, "70\(groupingSeparator)000") 538 + } 539 + 540 + func testHistoryLoadsInNewViewModel() { 541 + let firstViewModel = makeViewModel() 542 + input(["5", "+", "5"], into: firstViewModel) 543 + firstViewModel.evaluate() 544 + 545 + let secondViewModel = makeViewModel() 546 + 547 + XCTAssertEqual(secondViewModel.history.count, 1) 548 + XCTAssertEqual(secondViewModel.history.first?.result, "10") 549 + } 550 + 551 + func testClearHistoryRemovesPersistedRecords() { 552 + let viewModel = makeViewModel() 553 + input(["5", "+", "5"], into: viewModel) 554 + viewModel.evaluate() 555 + 556 + viewModel.clearHistory() 557 + 558 + XCTAssertTrue(viewModel.history.isEmpty) 559 + XCTAssertTrue(makeViewModel().history.isEmpty) 560 + } 561 + 562 + func testHistoryIsCappedAtOneHundredRecords() { 563 + let viewModel = makeViewModel() 564 + 565 + for value in 1...105 { 566 + viewModel.clearExpression() 567 + viewModel.input(String(value)) 568 + viewModel.evaluate() 174 569 } 175 570 571 + XCTAssertEqual(viewModel.history.count, 100) 572 + XCTAssertEqual(viewModel.history.first?.result, "105") 573 + XCTAssertEqual(viewModel.history.last?.result, "6") 574 + } 575 + 576 + func testUseHistoryRestoresResultAndSupportsContinuedCalculation() { 577 + let viewModel = makeViewModel() 578 + 579 + input(["3", "3", "0", "0", "0", "0"], into: viewModel) 580 + viewModel.evaluate() 581 + 582 + let record = try! XCTUnwrap(viewModel.history.first) 583 + viewModel.clearExpression() 584 + viewModel.useHistory(record) 585 + input(["-", "2", "6", "0", "0", "0", "0"], into: viewModel) 586 + viewModel.evaluate() 587 + 588 + XCTAssertEqual(viewModel.expressionDisplay, "70\(groupingSeparator)000") 589 + } 590 + 591 + func testEvaluateErrorSetsMessageAndInputClearsIt() { 592 + let viewModel = makeViewModel() 593 + 594 + input(["1", "/", "0"], into: viewModel) 595 + viewModel.evaluate() 596 + XCTAssertEqual(viewModel.errorMessage, CalculatorError.divisionByZero.errorDescription) 597 + 598 + viewModel.input("2") 599 + 600 + XCTAssertNil(viewModel.errorMessage) 601 + } 602 + 603 + func testExpressionDisplayFormatsTypedLargeNumber() { 604 + let viewModel = makeViewModel() 605 + 606 + input(["2", "6", "0", "0", "0", "0"], into: viewModel) 607 + 176 608 XCTAssertEqual(viewModel.expression, "260000") 177 - XCTAssertEqual(viewModel.expressionDisplay, "260,000") 609 + XCTAssertEqual(viewModel.expressionDisplay, "260\(groupingSeparator)000") 178 610 } 179 611 180 612 func testExpressionDisplayFormatsLargeNumbersWithinExpression() { 181 - let viewModel = CalculatorViewModel() 613 + let viewModel = makeViewModel() 182 614 183 - for token in ["2", "6", "0", "0", "0", "0", "+", "3", "3", "0", "0", "0", "0"] { 184 - viewModel.input(token) 185 - } 615 + input(["2", "6", "0", "0", "0", "0", "+", "3", "3", "0", "0", "0", "0"], into: viewModel) 186 616 187 617 XCTAssertEqual(viewModel.expression, "260000+330000") 188 - XCTAssertEqual(viewModel.expressionDisplay, "260,000+330,000") 618 + XCTAssertEqual(viewModel.expressionDisplay, "260\(groupingSeparator)000+330\(groupingSeparator)000") 189 619 } 190 620 191 621 func testExpressionDisplayPreservesFractionalDigits() { 192 - let viewModel = CalculatorViewModel() 622 + let viewModel = makeViewModel() 193 623 194 - for token in ["1", "2", "3", "4", "5", ".", "6", "7"] { 195 - viewModel.input(token) 196 - } 624 + input(["1", "2", "3", "4", "5", ".", "6", "7"], into: viewModel) 197 625 198 - XCTAssertEqual(viewModel.expressionDisplay, "12,345.67") 626 + XCTAssertEqual(viewModel.expressionDisplay, "12\(groupingSeparator)345.67") 627 + } 628 + 629 + func testExpressionDisplayFormatsOperatorsAndConstants() { 630 + let viewModel = makeViewModel() 631 + 632 + input(["sqrt", "1", "6", ")", "/", "pi"], into: viewModel) 633 + 634 + XCTAssertEqual(viewModel.expressionDisplay, "√(16)÷π") 199 635 } 200 636 201 637 func testExpressionDisplayUsesExponentialForTypedTrillionScaleNumber() { 202 - let viewModel = CalculatorViewModel() 638 + let viewModel = makeViewModel() 203 639 204 - for token in ["1", "2", "5", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0"] { 205 - viewModel.input(token) 206 - } 640 + input(["1", "2", "5", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0"], into: viewModel) 207 641 208 642 XCTAssertEqual(viewModel.expression, "1250000000000") 209 643 XCTAssertEqual(viewModel.expressionDisplay, "1.25e12") 210 644 } 211 645 212 646 func testEvaluateLargeResultDisplaysExponentially() { 213 - let viewModel = CalculatorViewModel() 647 + let viewModel = makeViewModel() 214 648 215 - for token in ["1", "2", "5", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0"] { 216 - viewModel.input(token) 217 - } 649 + input(["1", "2", "5", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0"], into: viewModel) 218 650 viewModel.evaluate() 219 651 220 652 XCTAssertEqual(viewModel.expression, "1250000000000") ··· 223 655 } 224 656 225 657 func testContinuingFromGroupedResultWorks() { 226 - let viewModel = CalculatorViewModel() 658 + let viewModel = makeViewModel() 227 659 228 - for token in ["3", "3", "0", "0", "0", "0"] { 229 - viewModel.input(token) 230 - } 660 + input(["3", "3", "0", "0", "0", "0"], into: viewModel) 231 661 viewModel.evaluate() 232 662 233 - viewModel.input("-") 234 - for token in ["2", "6", "0", "0", "0", "0"] { 235 - viewModel.input(token) 236 - } 663 + input(["-", "2", "6", "0", "0", "0", "0"], into: viewModel) 237 664 viewModel.evaluate() 238 665 239 - XCTAssertEqual(viewModel.expressionDisplay, "70,000") 666 + XCTAssertEqual(viewModel.expressionDisplay, "70\(groupingSeparator)000") 240 667 XCTAssertNil(viewModel.errorMessage) 668 + } 669 + 670 + func testDisplayValueToCopyReflectsEvaluatedResult() { 671 + let viewModel = makeViewModel() 672 + 673 + input(["5", "+", "5"], into: viewModel) 674 + viewModel.evaluate() 675 + 676 + XCTAssertEqual(viewModel.displayValueToCopy, "10") 677 + XCTAssertTrue(viewModel.canCopyDisplayValue) 678 + } 679 + 680 + func testDisplayValueToCopyIsUnavailableForPlaceholder() { 681 + let viewModel = makeViewModel() 682 + 683 + XCTAssertEqual(viewModel.displayValueToCopy, "0") 684 + XCTAssertFalse(viewModel.canCopyDisplayValue) 685 + } 686 + 687 + func testHandleKeyEventBackspaceDeletes() { 688 + let viewModel = makeViewModel() 689 + input(["1", "2"], into: viewModel) 690 + 691 + let handled = viewModel.handleKeyEvent(makeKeyEvent(characters: "\u{8}", keyCode: 51)) 692 + 693 + XCTAssertTrue(handled) 694 + XCTAssertEqual(viewModel.expression, "1") 695 + } 696 + 697 + func testHandleKeyEventEscapeClears() { 698 + let viewModel = makeViewModel() 699 + input(["1", "2"], into: viewModel) 700 + 701 + let handled = viewModel.handleKeyEvent(makeKeyEvent(characters: "\u{1B}", keyCode: 53)) 702 + 703 + XCTAssertTrue(handled) 704 + XCTAssertEqual(viewModel.expression, "0") 705 + } 706 + 707 + func testHandleKeyEventReturnEvaluates() { 708 + let viewModel = makeViewModel() 709 + input(["5", "+", "5"], into: viewModel) 710 + 711 + let handled = viewModel.handleKeyEvent(makeKeyEvent(characters: "\r", keyCode: 36)) 712 + 713 + XCTAssertTrue(handled) 714 + XCTAssertEqual(viewModel.expression, "10") 715 + } 716 + 717 + func testHandleKeyEventEqualsCharacterEvaluates() { 718 + let viewModel = makeViewModel() 719 + input(["5", "+", "5"], into: viewModel) 720 + 721 + let handled = viewModel.handleKeyEvent(makeKeyEvent(characters: "=")) 722 + 723 + XCTAssertTrue(handled) 724 + XCTAssertEqual(viewModel.expression, "10") 725 + } 726 + 727 + func testHandleKeyEventIgnoresUnsupportedModifiers() { 728 + let viewModel = makeViewModel() 729 + 730 + let handled = viewModel.handleKeyEvent(makeKeyEvent(characters: "1", modifiers: [.command])) 731 + 732 + XCTAssertFalse(handled) 733 + XCTAssertEqual(viewModel.expression, "0") 734 + } 735 + 736 + func testHandleKeyEventAcceptsShiftedCharacters() { 737 + let viewModel = makeViewModel() 738 + 739 + let handled = viewModel.handleKeyEvent(makeKeyEvent(characters: "(", modifiers: [.shift])) 740 + 741 + XCTAssertTrue(handled) 742 + XCTAssertEqual(viewModel.expression, "(") 743 + } 744 + 745 + func testHandleKeyEventHandlesLetterInput() { 746 + let viewModel = makeViewModel() 747 + 748 + let handled = viewModel.handleKeyEvent(makeKeyEvent(characters: "L")) 749 + 750 + XCTAssertTrue(handled) 751 + XCTAssertEqual(viewModel.expression, "l") 752 + } 753 + 754 + func testHandleKeyEventHandlesUnicodeScientificTokens() { 755 + let viewModel = makeViewModel() 756 + 757 + let handled = viewModel.handleKeyEvent(makeKeyEvent(characters: "π√")) 758 + 759 + XCTAssertTrue(handled) 760 + XCTAssertEqual(viewModel.expression, "pi*sqrt(") 241 761 } 242 762 }