更新文件:TWD 改為整數四捨五入的說明與 Kotlin 陷阱紀錄
This commit is contained in:
@@ -50,7 +50,8 @@ cd .. && zip -9 bot_rates.zip bot_rates.exe
|
|||||||
- **輸入狀態**:`App.amounts: HashMap<String, String>`,存字串不是 `f64`,這樣才能保留「`100.`」「空字串」「`1,000`」這類輸入中間態。`parse_amount` 才做清洗與 parse。
|
- **輸入狀態**:`App.amounts: HashMap<String, String>`,存字串不是 `f64`,這樣才能保留「`100.`」「空字串」「`1,000`」這類輸入中間態。`parse_amount` 才做清洗與 parse。
|
||||||
- **字型**:egui 預設字型沒中文,會變豆腐。`setup_fonts` 在執行期讀 OS 字型(Windows `msjh.ttc` / macOS `PingFang.ttc`,有 fallback 鏈),**刻意不 bundle 字型**以免 binary 胖 5–15 MB。如果在沒有這些字型的環境跑就會回到豆腐。
|
- **字型**:egui 預設字型沒中文,會變豆腐。`setup_fonts` 在執行期讀 OS 字型(Windows `msjh.ttc` / macOS `PingFang.ttc`,有 fallback 鏈),**刻意不 bundle 字型**以免 binary 胖 5–15 MB。如果在沒有這些字型的環境跑就會回到豆腐。
|
||||||
- **時間**:`now_string` / `civil_from_days` 自己實作 UTC→台北時間換算與 Gregorian 日期計算,避免引入 `chrono` / `time` 把 binary 撐大。
|
- **時間**:`now_string` / `civil_from_days` 自己實作 UTC→台北時間換算與 Gregorian 日期計算,避免引入 `chrono` / `time` 把 binary 撐大。
|
||||||
- **複製**:「複製」按鈕用 `ui.output_mut(|o| o.copied_text = raw)` 寫純數字(`3162.00`,不含 `NT$` / 逗號)到剪貼簿,設計是為了貼到 Excel 會被當數字。TWD label 另外有 `.selectable(true)` 讓使用者自行選取。
|
- **複製**:「複製」按鈕用 `ui.output_mut(|o| o.copied_text = raw)` 寫純整數(`3163`,四捨五入到個位,不含 `NT$` / 逗號 / 小數)到剪貼簿,設計是為了貼到 Excel 會被當數字。TWD label 另外有 `.selectable(true)` 讓使用者自行選取。
|
||||||
|
- **四捨五入**:TWD 一律顯示整數;Rust 用 `f64::round() as i64`(語言內建就是 ties away from zero),Kotlin 改用 `BigDecimal.setScale(0, HALF_UP)`(因為 `kotlin.math.round` 是銀行家捨入、行為不一致)。兩側要同步。
|
||||||
|
|
||||||
## 擴充時的常見改點
|
## 擴充時的常見改點
|
||||||
|
|
||||||
@@ -58,6 +59,7 @@ cd .. && zip -9 bot_rates.zip bot_rates.exe
|
|||||||
|------|------|
|
|------|------|
|
||||||
| 新增幣別 | `TARGETS` 常數,加一行 `(代碼, 中文名)` 即可,其他地方會自動處理 |
|
| 新增幣別 | `TARGETS` 常數,加一行 `(代碼, 中文名)` 即可,其他地方會自動處理 |
|
||||||
| 改成即期買入/現金匯率 | `fetch_rates` 裡的 `cols[13]` 換成對應 index(見上方資料抓取段) |
|
| 改成即期買入/現金匯率 | `fetch_rates` 裡的 `cols[13]` 換成對應 index(見上方資料抓取段) |
|
||||||
|
| 顯示到小數點 | 改 `format_twd` / `format_twd_raw` 的 `v.round() as i64`(Rust)與 `setScale(0, HALF_UP)`(Kotlin),兩邊都要改 |
|
||||||
| 視窗尺寸/不可 resize | `main()` 的 `ViewportBuilder` |
|
| 視窗尺寸/不可 resize | `main()` 的 `ViewportBuilder` |
|
||||||
| 預設輸入金額 | `App::new` 裡的 `"100".to_string()` |
|
| 預設輸入金額 | `App::new` 裡的 `"100".to_string()` |
|
||||||
| 配色 | `update` 開頭的 `bg` / `card` / `rate_color` / `twd_color` 等常數 |
|
| 配色 | `update` 開頭的 `bg` / `card` / `rate_color` / `twd_color` 等常數 |
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ xattr -cr /path/to/BotRates.app
|
|||||||
|------|------|
|
|------|------|
|
||||||
| 左側 | 幣別、代碼、即期賣出匯率(例如 `美元 USD 31.6200`) |
|
| 左側 | 幣別、代碼、即期賣出匯率(例如 `美元 USD 31.6200`) |
|
||||||
| 中間 | 輸入外幣金額(預設 100,可自由修改) |
|
| 中間 | 輸入外幣金額(預設 100,可自由修改) |
|
||||||
| 右側 | 台幣換算結果(例如 `NT$ 3,162.00`) |
|
| 右側 | 台幣換算結果,四捨五入到個位(例如 `NT$ 3,163`) |
|
||||||
| 複製 | 按下複製純數字(`3162.00`)到剪貼簿,可直接貼 Excel |
|
| 複製 | 按下複製純整數(`3163`)到剪貼簿,可直接貼 Excel |
|
||||||
| 重新整理 | 右下角按鈕,重新抓最新匯率 |
|
| 重新整理 | 右下角按鈕,重新抓最新匯率 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
- **即時匯率**:從台銀官網 CSV(`rate.bot.com.tw/xrt/flcsv/0/day`)抓取,免 API key
|
- **即時匯率**:從台銀官網 CSV(`rate.bot.com.tw/xrt/flcsv/0/day`)抓取,免 API key
|
||||||
- **雙向換算**:每個幣別獨立輸入框,輸入外幣金額立即換算出台幣
|
- **雙向換算**:每個幣別獨立輸入框,輸入外幣金額立即換算出台幣
|
||||||
- **一鍵複製**:按「複製」即把純數字(如 `3162.00`)寫入剪貼簿,可直接貼到 Excel
|
- **一鍵複製**:按「複製」即把整數(如 `3163`,四捨五入到個位)寫入剪貼簿,可直接貼到 Excel
|
||||||
- **零依賴**:單一執行檔,使用者不需要安裝 Python、Rust 或任何 runtime
|
- **零依賴**:單一執行檔,使用者不需要安裝 Python、Rust 或任何 runtime
|
||||||
- **跨平台**:Windows 與 macOS 共用同一份 Rust 原始碼
|
- **跨平台**:Windows 與 macOS 共用同一份 Rust 原始碼
|
||||||
|
|
||||||
|
|||||||
@@ -96,13 +96,13 @@ BotRates.app/
|
|||||||
|
|
||||||
輸入解析用 `parse_amount`,忽略逗號和空白再 parse。
|
輸入解析用 `parse_amount`,忽略逗號和空白再 parse。
|
||||||
|
|
||||||
TWD 顯示用 `format_twd`,手刻千分位 separator(沒引入 `num-format` crate 以減少 binary size)。
|
TWD 顯示用 `format_twd`,四捨五入到個位(`v.round() as i64`,Rust 的 `f64::round` 本身就是 ties away from zero,符合中文四捨五入),手刻千分位 separator(沒引入 `num-format` crate 以減少 binary size)。
|
||||||
|
|
||||||
## 階段 7:複製功能
|
## 階段 7:複製功能
|
||||||
|
|
||||||
兩種複製方式並存:
|
兩種複製方式並存:
|
||||||
|
|
||||||
1. **按鈕複製**(主要 UX):按「複製」→ `ui.output_mut(|o| o.copied_text = raw)`,複製純數字 `3162.00`(不含 `NT$`、不含逗號)。原因:這樣貼到 Excel 會被識別成數字而不是文字。
|
1. **按鈕複製**(主要 UX):按「複製」→ `ui.output_mut(|o| o.copied_text = raw)`,複製純整數 `3163`(不含 `NT$`、不含逗號、不含小數)。原因:這樣貼到 Excel 會被識別成數字而不是文字。
|
||||||
2. **選取複製**:`egui::Label::new(...).selectable(true)`,使用者可以用滑鼠拉選 TWD 文字後 `Cmd/Ctrl+C`。
|
2. **選取複製**:`egui::Label::new(...).selectable(true)`,使用者可以用滑鼠拉選 TWD 文字後 `Cmd/Ctrl+C`。
|
||||||
|
|
||||||
按下按鈕後短暫顯示「已複製」(1.2 秒),用 `Option<(String, Instant)>` 追蹤狀態 + `ctx.request_repaint_after` 觸發回復重繪。
|
按下按鈕後短暫顯示「已複製」(1.2 秒),用 `Option<(String, Instant)>` 追蹤狀態 + `ctx.request_repaint_after` 觸發回復重繪。
|
||||||
|
|||||||
@@ -272,6 +272,60 @@ curl -X POST "http://.../api/v1/repos/timmy/bot-rates/releases/$ID/assets?name=B
|
|||||||
| 抓匯率耗時 | < 500ms |
|
| 抓匯率耗時 | < 500ms |
|
||||||
| Gitea release | v1.0 已上傳 APK |
|
| Gitea release | v1.0 已上傳 APK |
|
||||||
|
|
||||||
|
## 階段 7:調整顯示為整數(v1.0.1)
|
||||||
|
|
||||||
|
需求:TWD 顯示不要小數,直接四捨五入到個位(例如 `3,162.50` → `3,163`)。
|
||||||
|
|
||||||
|
### Rust 端(macOS / Windows)
|
||||||
|
|
||||||
|
Rust 的 `f64::round()` 本身就是 ties away from zero(正數 `.5` 往上、負數 `-.5` 往下),符合中文「四捨五入」定義。改動只是把「×100 round ÷100 + `.xx` 格式化」簡化成 `v.round() as i64`:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn format_twd(v: f64) -> String {
|
||||||
|
let rounded = v.round() as i64;
|
||||||
|
// 手刻千分位逗號
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_twd_raw(v: f64) -> String {
|
||||||
|
format!("{}", v.round() as i64)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Kotlin 端的陷阱
|
||||||
|
|
||||||
|
`kotlin.math.round(Double)` 是**銀行家捨入**(ties to even),例如 `316.5 → 316`、`317.5 → 318`。這跟 Rust 端行為不一致,而且不是台灣語境下「四捨五入」的常識定義。
|
||||||
|
|
||||||
|
**解法**:改用 `BigDecimal.setScale(0, RoundingMode.HALF_UP)`:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
private fun roundHalfUp(v: Double): BigDecimal =
|
||||||
|
BigDecimal.valueOf(v).setScale(0, RoundingMode.HALF_UP)
|
||||||
|
|
||||||
|
private fun formatTwd(v: Double): String =
|
||||||
|
DecimalFormat("#,##0").format(roundHalfUp(v))
|
||||||
|
|
||||||
|
private fun formatTwdRaw(v: Double): String =
|
||||||
|
roundHalfUp(v).toPlainString()
|
||||||
|
```
|
||||||
|
|
||||||
|
**為什麼 `BigDecimal.valueOf(v)` 而不是 `BigDecimal(v)`?**
|
||||||
|
`BigDecimal(double)` 吃 IEEE 754 的尾數誤差,例如 `BigDecimal(0.1)` 會變 `0.1000000000000000055511151...`。`valueOf(double)` 走 `Double.toString` 路線,給你看起來「應該」的值,對使用者輸入的金額比較合理。
|
||||||
|
|
||||||
|
### 版號策略
|
||||||
|
|
||||||
|
行為變了(而且使用者肉眼可見),所以 bump `versionCode = 2 / versionName = "1.0.1"`,tag `v1.0.1`,在 Gitea 另開一個 release `BotRates-v1.0.1.apk`,不覆蓋 v1.0 的 asset。這樣歷史版本可追溯。
|
||||||
|
|
||||||
|
### 實機 install 踩到 INSTALL_FAILED_UPDATE_INCOMPATIBLE
|
||||||
|
|
||||||
|
先前裝過 debug 版(debug keystore 簽),現在要裝 release 版(自己的 keystore 簽),Android 拒絕覆蓋:
|
||||||
|
|
||||||
|
```
|
||||||
|
Failure [INSTALL_FAILED_UPDATE_INCOMPATIBLE: Package tw.local.botrates
|
||||||
|
signatures do not match previously installed version; ignoring!]
|
||||||
|
```
|
||||||
|
|
||||||
|
**解法**:`adb uninstall tw.local.botrates` 先移除,再 `adb install`。之後都用同一把 release keystore 簽,就能持續覆蓋更新。
|
||||||
|
|
||||||
## 如果要擴充
|
## 如果要擴充
|
||||||
|
|
||||||
- **相容 minSdk 21(Android 5.0)**:`compileOptions` 加 `isCoreLibraryDesugaringEnabled = true`,dependencies 加 `coreLibraryDesugaring`,讓 `java.time` 等新 API 降級。
|
- **相容 minSdk 21(Android 5.0)**:`compileOptions` 加 `isCoreLibraryDesugaringEnabled = true`,dependencies 加 `coreLibraryDesugaring`,讓 `java.time` 等新 API 降級。
|
||||||
|
|||||||
Reference in New Issue
Block a user