From 33b7f929ccd0212825267ab82a77ab52c35b90cb Mon Sep 17 00:00:00 2001 From: boris Date: Sun, 4 Oct 2026 12:58:55 +0300 Subject: [PATCH] Pick formatter precision from the rounded value, not the raw one --- Sources/PokeTokenBar/Core/TokenFormatter.swift | 11 ++++++++--- .../PokeTokenBarTests/LocalUsageCacheTests.swift | 16 ++++++++++++++++ docs/reference/defect-log.md | 13 +++++++++++++ 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/Sources/PokeTokenBar/Core/TokenFormatter.swift b/Sources/PokeTokenBar/Core/TokenFormatter.swift index a0115fa68..251d5ccf8 100644 --- a/Sources/PokeTokenBar/Core/TokenFormatter.swift +++ b/Sources/PokeTokenBar/Core/TokenFormatter.swift @@ -35,14 +35,19 @@ enum TokenFormatter { } /// 메뉴바용 짧은 비용 표기: $9.5 / $311 / $1.2K + /// 구간 판정은 원값이 아니라 *반올림된 문자열*로 한다 — 99.96 은 "$100.0" 이 아니라 "$100". static func costCompact(_ usd: Double) -> String { - if usd < 100 { return String(format: "$%.1f", usd) } - if usd < 10_000 { return String(format: "$%.0f", usd) } + let tenths = String(format: "%.1f", usd) + if let v = Double(tenths), v < 100 { return "$" + tenths } + let whole = String(format: "%.0f", usd) + if let v = Double(whole), v < 10_000 { return "$" + whole } return String(format: "$%.1fK", usd / 1_000) } + /// 79.96 → "80%" (not "80.0%"), 88.35 → "88.3%" static func percent(_ value: Double) -> String { - value == value.rounded() ? String(format: "%.0f%%", value) : String(format: "%.1f%%", value) + let tenths = String(format: "%.1f", value) + return (tenths.hasSuffix(".0") ? String(tenths.dropLast(2)) : tenths) + "%" } private static func trim(_ value: Double, decimals: Int) -> String { diff --git a/Tests/PokeTokenBarTests/LocalUsageCacheTests.swift b/Tests/PokeTokenBarTests/LocalUsageCacheTests.swift index c3e4bf42e..97dc3977e 100644 --- a/Tests/PokeTokenBarTests/LocalUsageCacheTests.swift +++ b/Tests/PokeTokenBarTests/LocalUsageCacheTests.swift @@ -824,6 +824,22 @@ final class LocalUsageCacheTests: XCTestCase { XCTAssertEqual(TokenFormatter.percent(88.35), "88.3%") } + /// Branching on the raw value and then rounding prints a value the branch said was below the + /// cutoff: "80.0%" instead of "80%", "$100.0" instead of "$100", "$10000" instead of "$10.0K". + /// Cursor's `usedPercent` is fractional and remaining mode prints `100 − x`, so these reach + /// the menu bar and the popover. + func testFormatterRoundingBoundaries() { + XCTAssertEqual(TokenFormatter.percent(79.96), "80%") + XCTAssertEqual(TokenFormatter.percent(99.97), "100%") + XCTAssertEqual(TokenFormatter.percent(0.04), "0%") + XCTAssertEqual(TokenFormatter.percent(79.94), "79.9%") + XCTAssertEqual(TokenFormatter.costCompact(99.96), "$100") + XCTAssertEqual(TokenFormatter.costCompact(99.94), "$99.9") + XCTAssertEqual(TokenFormatter.costCompact(9_999.6), "$10.0K") + XCTAssertEqual(TokenFormatter.costCompact(9_999.4), "$9999") + XCTAssertEqual(TokenFormatter.costCompact(100), "$100") + } + /// 날짜 유틸 — 주/월 경계와 monthKey (집계 윈도우 계산의 기반). func testDateHelpers() { var c = Calendar.current diff --git a/docs/reference/defect-log.md b/docs/reference/defect-log.md index 2cba9ec05..eb7c69638 100644 --- a/docs/reference/defect-log.md +++ b/docs/reference/defect-log.md @@ -775,6 +775,19 @@ read_when: 클로저 안에서 `.reservesScrollerLane()` 을 쓰는지 괄호 매칭으로 검사한다(주석·문자열 제외, 바깥에 붙인 패딩은 스크롤러까지 밀어 불인정). 다섯 곳 각각을 빼면 해당 `파일:줄` 로 실패하는 것을 확인했다. +- **숫자 표기 구간은 원값이 아니라 반올림된 문자열로 판정한다.** + `TokenFormatter.percent` 는 `value == value.rounded()` 로 정수 여부를 보고 아니면 `%.1f` 를 찍었다 — + 79.96 은 정수가 아니라 `%.1f` 로 가서 "80.0%" 가 됐다(88% 는 "88%" 인데 80% 만 소수점). `costCompact` 도 + `usd < 100` 을 원값으로 판정해 99.96 → "$100.0", 9 999.6 → "$10000"(K 구간 대신 5자리)이 나왔다. + Cursor `usedPercent`·잔여 모드 `100 − x`·리캡 증감 `abs(delta)*100`·토큰 단가 기반 비용은 전부 임의의 + 소수라 메뉴바·팝오버·알림에 실제로 나온다. + **왜 못 걸렀나:** `testFormatterEdges` 는 구간 한가운데 값(88, 88.35, 9.54, 311.4)만 넣었다. 반올림이 + 구간 경계를 넘는 값(x.95 이상)이 없으면 "판정 후 반올림" 순서 결함은 보이지 않는다. + **수정:** 먼저 그 구간의 정밀도로 포맷하고, 그 문자열(파싱값)로 구간을 고른다. + **회귀 가드:** `testFormatterRoundingBoundaries` — 경계 직전·직후 쌍(79.96/79.94, 99.96/99.94, + 9 999.6/9 999.4). 수정 전 코드로 5건 실패 확인. 스윕: `L.difficultyValue` 도 같은 모양이지만 + `snapDifficulty` 가 유효숫자 2자리로 스냅해 경계 넘는 입력이 존재하지 않아 손대지 않았다. + ## 에너지 (상시 표시 애니메이션) - **메뉴바 상태아이템 = idle CPU 저격수 (두 규칙 필수).** 실측: 라이브 앱 idle ~14% CPU → 수정 후 ~2%.