Skip to main content

cuqueclicker_lib/
format.rs

1/// Short suffixes for the first 12 engineering steps (10^0 … 10^33).
2/// After Decillion the formatter switches to an *infinite* alphabetic
3/// scheme (see `alpha_suffix`).
4const KNOWN_SUFFIXES: &[&str] = &[
5    "", "k", "M", "B", "T", "Qa", "Qi", "Sx", "Sp", "Oc", "No", "Dc",
6];
7
8/// Format a `Mag` (log-magnitude) the same way `big` formats an `f64`,
9/// but without an upper bound on representable values. Cuques, FPS,
10/// and costs all flow through here once they live in `Mag` form.
11pub fn big_mag(m: crate::bignum::Mag) -> String {
12    if m.is_zero() {
13        return "0".into();
14    }
15    if m.log10 < 3.0 {
16        // Below 1000 — defer to the regular `big()` path for parity
17        // with how the game has always rendered small early-game
18        // numbers (plain integer, no suffix).
19        return big(m.to_f64());
20    }
21    // Engineering-group: every 3 OOM gets a suffix.
22    // `log10 / 3.0` for normal play never exceeds a few thousand, so
23    // the as-i64 cast is safe; the upper-bound clamp guards a runaway
24    // future bug from indexing past the alpha-suffix horizon.
25    let raw_group = (m.log10 / 3.0).floor();
26    if !raw_group.is_finite() || raw_group > 1e15 {
27        // Past `10^(3e15)` we've left any reasonable gameplay regime —
28        // emit a compact log-space label so the HUD still says something
29        // useful instead of stalling on an infinite suffix lookup.
30        return format!("10^{:.0}", m.log10);
31    }
32    let group = raw_group as usize;
33    let suffix = if group < KNOWN_SUFFIXES.len() {
34        KNOWN_SUFFIXES[group].to_string()
35    } else {
36        alpha_suffix(group - KNOWN_SUFFIXES.len())
37    };
38    let scaled = 10f64.powf(m.log10 - (group as f64) * 3.0);
39    format!("{scaled:.2}{suffix}")
40}
41
42pub fn big(n: f64) -> String {
43    if n.is_nan() || n.is_infinite() {
44        // Bug sentinel: the gameplay math should never reach Inf/NaN
45        // with the rebalanced tree magnitudes. If we ever see `?` in
46        // game again, something has overflowed and needs investigating.
47        return "?".into();
48    }
49    if n.abs() < 1000.0 {
50        return format!("{}", n.floor() as i64);
51    }
52    let mag = (n.abs().log10() / 3.0).floor() as i32;
53    let mag = mag.max(0) as usize;
54    let suffix = if mag < KNOWN_SUFFIXES.len() {
55        KNOWN_SUFFIXES[mag].to_string()
56    } else {
57        alpha_suffix(mag - KNOWN_SUFFIXES.len())
58    };
59    let scaled = n / 10f64.powi((mag * 3) as i32);
60    format!("{scaled:.2}{suffix}")
61}
62
63/// Infinite alphabetic suffix sequence. `n=0` yields the first suffix
64/// past Decillion. The sequence is grouped into *phases*; phase `L≥0`
65/// produces all `(L+2)`-letter strings, first all-lowercase, then
66/// capital-first. Each phase has `2 × 26^(L+2)` entries.
67///
68/// Concretely:
69///
70/// ```text
71/// n = 0..676            → aa, ab, …, az, ba, …, zz       (676 = 26²  lowercase pairs)
72/// n = 676..1352         → Aa, Ab, …, Az, Ba, …, Zz        (676  uppercase-first pairs)
73/// n = 1352..1352+17576  → aaa, aab, …, zzz               (17576 = 26³  lowercase triples)
74/// n = …  + 17576        → Aaa, Aab, …, Zzz                (uppercase-first triples)
75/// …
76/// ```
77///
78/// Each suffix step represents three more orders of magnitude on top of
79/// `10^33 ≈ Dc`, so phase 0 alone covers `10^36 … 10^(33 + 676·3)` =
80/// `10^36 … 10^2061` — already past `f64::MAX ≈ 1.8e308`. The higher
81/// phases exist so the helper remains correct under any future
82/// arbitrary-precision number type.
83pub fn alpha_suffix(mut n: usize) -> String {
84    let mut len: u32 = 2;
85    loop {
86        let phase_entries = 26usize.pow(len);
87        // Phase L, sub-phase 0: lowercase only.
88        if n < phase_entries {
89            return digits_to_letters(n, len as usize, false);
90        }
91        n -= phase_entries;
92        // Phase L, sub-phase 1: capital-first.
93        if n < phase_entries {
94            return digits_to_letters(n, len as usize, true);
95        }
96        n -= phase_entries;
97        len += 1;
98    }
99}
100
101/// Encode `idx` as a `len`-digit base-26 numeral with leading zeros,
102/// emitting `a..z` for each digit. If `cap_first` is true, capitalize
103/// the leading letter (e.g. `Aa`, `Aaa`).
104fn digits_to_letters(mut idx: usize, len: usize, cap_first: bool) -> String {
105    let mut digits = vec![0u8; len];
106    for i in (0..len).rev() {
107        digits[i] = (idx % 26) as u8;
108        idx /= 26;
109    }
110    digits
111        .iter()
112        .enumerate()
113        .map(|(i, &d)| {
114            let base = if i == 0 && cap_first { b'A' } else { b'a' };
115            (base + d) as char
116        })
117        .collect()
118}
119
120/// Render a multiplicative magnitude (e.g. a `MulFactor`/`EffectMul` value)
121/// with enough decimal places to show ~3 significant figures of its
122/// distance from `1.0`. Lets per-tree-node values like `1.00004` actually
123/// read as `×1.00004` in the info pane instead of getting rounded to the
124/// uninformative `×1.00`.
125pub fn mul_magnitude(v: f64) -> String {
126    let delta = (v - 1.0).abs();
127    if delta == 0.0 || !v.is_finite() {
128        return format!("×{v:.2}");
129    }
130    // Sig-figs of the delta determine precision: `delta = 1e-4` → 6
131    // decimals (so the user sees `×1.00010`), `delta = 0.5` → 2 decimals
132    // (`×1.50`). Clamped to [2, 8] so we never write e.g. `×1.5` (too
133    // few) or `×1.00000123456` (visual noise).
134    let need = (-delta.log10()).ceil() as i32 + 2;
135    let prec = need.clamp(2, 8) as usize;
136    format!("×{v:.*}", prec)
137}
138
139/// Like `mul_magnitude`, but for an `AddPercent` magnitude (raw `f64`
140/// where `0.01 == 1%`). Picks decimal precision so the user can see
141/// sub-percent values like `+0.005%` that would otherwise round to
142/// `+0.0%`.
143pub fn percent_magnitude(v: f64) -> String {
144    let p = v * 100.0;
145    let mag = p.abs();
146    let prec = if mag >= 1.0 {
147        1
148    } else if mag >= 0.1 {
149        2
150    } else if mag >= 0.01 {
151        3
152    } else {
153        4
154    };
155    format!("{p:+.*}%", prec)
156}
157
158/// Like `mul_magnitude`, but for a `FlatAdd` magnitude. Picks decimal
159/// precision so a 0.002 FlatAdd doesn't collapse to `+0.0`.
160pub fn flat_magnitude(v: f64) -> String {
161    let m = v.abs();
162    let prec = if m >= 10.0 {
163        1
164    } else if m >= 1.0 {
165        2
166    } else if m >= 0.1 {
167        3
168    } else {
169        4
170    };
171    format!("{v:+.*}", prec)
172}
173
174pub fn rate(n: f64) -> String {
175    if n < 10.0 {
176        format!("{n:.2}")
177    } else if n < 100.0 {
178        format!("{n:.1}")
179    } else {
180        big(n)
181    }
182}
183
184pub fn duration(secs: u64) -> String {
185    let h = secs / 3600;
186    let m = (secs % 3600) / 60;
187    let s = secs % 60;
188    if h > 0 {
189        format!("{h}h {m:02}m {s:02}s")
190    } else if m > 0 {
191        format!("{m}m {s:02}s")
192    } else {
193        format!("{s}s")
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    #[test]
202    fn small_numbers_are_plain_integers() {
203        assert_eq!(big(0.0), "0");
204        assert_eq!(big(42.7), "42");
205        assert_eq!(big(999.0), "999");
206    }
207
208    #[test]
209    fn known_suffix_range_uses_short_names() {
210        assert_eq!(big(1_500.0), "1.50k");
211        assert_eq!(big(2.5e6), "2.50M");
212        assert_eq!(big(7.0e33), "7.00Dc");
213    }
214
215    #[test]
216    fn beyond_decillion_uses_alpha_suffix() {
217        // 10^36 is the first step past Decillion.
218        assert_eq!(big(1.0e36), "1.00aa");
219        assert_eq!(big(3.5e36), "3.50aa");
220        assert_eq!(big(1.0e39), "1.00ab");
221        assert_eq!(big(1.0e42), "1.00ac");
222    }
223
224    #[test]
225    fn alpha_suffix_phase_transitions() {
226        // First lowercase pair (n=0) is "aa".
227        assert_eq!(alpha_suffix(0), "aa");
228        // Last lowercase pair (n=675) is "zz".
229        assert_eq!(alpha_suffix(675), "zz");
230        // Then uppercase-first pairs start at n=676 with "Aa".
231        assert_eq!(alpha_suffix(676), "Aa");
232        // Last uppercase-first pair (n=1351) is "Zz".
233        assert_eq!(alpha_suffix(1351), "Zz");
234        // Then lowercase triples begin at n=1352 with "aaa".
235        assert_eq!(alpha_suffix(1352), "aaa");
236    }
237
238    #[test]
239    fn alpha_suffix_within_phase_is_base26() {
240        // n=1 → "ab", n=25 → "az", n=26 → "ba".
241        assert_eq!(alpha_suffix(1), "ab");
242        assert_eq!(alpha_suffix(25), "az");
243        assert_eq!(alpha_suffix(26), "ba");
244    }
245
246    #[test]
247    fn infinity_and_nan_render_as_question_mark() {
248        assert_eq!(big(f64::INFINITY), "?");
249        assert_eq!(big(f64::NEG_INFINITY), "?");
250        assert_eq!(big(f64::NAN), "?");
251    }
252
253    #[test]
254    fn big_mag_handles_unbounded_values() {
255        use crate::bignum::Mag;
256        // 10^36 → first alpha-suffix entry.
257        assert_eq!(big_mag(Mag::from_f64(1.0e36)), "1.00aa");
258        // 10^999 — past f64::MAX, but Mag handles it cleanly. Pick
259        // a power that's an exact multiple of 3 so the mantissa is "1".
260        let huge = Mag { log10: 999.0 };
261        let s = big_mag(huge);
262        assert!(!s.contains('?'), "got {s}");
263        assert!(s.starts_with("1.00"), "got {s}");
264    }
265
266    #[test]
267    fn big_mag_handles_zero_and_small() {
268        use crate::bignum::Mag;
269        assert_eq!(big_mag(Mag::ZERO), "0");
270        assert_eq!(big_mag(Mag::from_f64(42.0)), "42");
271        assert_eq!(big_mag(Mag::from_f64(1500.0)), "1.50k");
272    }
273
274    #[test]
275    fn huge_but_finite_number_does_not_print_question_mark() {
276        // 1e200 would have overflowed the old fixed-suffix table and
277        // produced a stretch of trailing zeros in front of "Dc". With
278        // the alpha tail it formats compactly with a real suffix.
279        let s = big(1.0e200);
280        assert!(!s.contains('?'), "got {s}");
281        assert!(s.ends_with(char::is_alphabetic), "got {s}");
282    }
283}