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}