1use crate::backend::{TickInt, Ticks};
19use crate::error::{Code, Result, TimeError};
20
21pub const GROUP_BASE: u16 = 3125;
23
24pub const DIGITS_PER_TIER: u32 = 5;
26
27pub const BEAT_EXPONENT: u32 = 60;
29
30pub const K_MIN: i8 = -12;
32
33pub const K_MAX: i8 = 32;
37
38pub const TIER_COUNT: usize = (K_MAX as isize - K_MIN as isize + 1) as usize;
40
41#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
43pub struct Tier(i8);
44
45impl Tier {
46 pub const BEAT: Tier = Tier(0);
48 pub const TICK: Tier = Tier(K_MIN);
50 pub const DEEP: Tier = Tier(5);
52 pub const DRIFT: Tier = Tier(4);
54 pub const SPAN: Tier = Tier(3);
56 pub const SWEEP: Tier = Tier(2);
58 pub const ARC: Tier = Tier(1);
60 pub const FLICKER: Tier = Tier(-1);
62 pub const GLINT: Tier = Tier(-2);
64 pub const SPARK: Tier = Tier(-3);
66
67 pub const fn new(k: i8) -> Result<Tier> {
69 if k < K_MIN || k > K_MAX {
70 Err(TimeError::with_context(
71 Code::E0080,
72 "tier index outside [-12, 32]",
73 ))
74 } else {
75 Ok(Tier(k))
76 }
77 }
78
79 pub const fn from_exponent(exponent: u32) -> Result<Tier> {
81 if exponent % DIGITS_PER_TIER != 0 {
82 return Err(TimeError::with_context(
83 Code::E0080,
84 "exponent is not a multiple of 5, so it is not on the 5^(5k) grid",
85 ));
86 }
87 let k = if exponent >= BEAT_EXPONENT {
89 ((exponent - BEAT_EXPONENT) / DIGITS_PER_TIER) as i64
90 } else {
91 -(((BEAT_EXPONENT - exponent) / DIGITS_PER_TIER) as i64)
92 };
93 if k < K_MIN as i64 || k > K_MAX as i64 {
94 return Err(TimeError::with_context(
95 Code::E0080,
96 "exponent outside the profile grid",
97 ));
98 }
99 Ok(Tier(k as i8))
100 }
101
102 pub const fn index(self) -> i8 {
104 self.0
105 }
106
107 pub const fn exponent(self) -> u32 {
109 (BEAT_EXPONENT as i64 + DIGITS_PER_TIER as i64 * self.0 as i64) as u32
111 }
112
113 pub const fn is_tick(self) -> bool {
115 self.0 == K_MIN
116 }
117
118 pub const fn coarser(self) -> Option<Tier> {
120 if self.0 >= K_MAX {
121 None
122 } else {
123 Some(Tier(self.0 + 1))
124 }
125 }
126
127 pub const fn finer(self) -> Option<Tier> {
129 if self.0 <= K_MIN {
130 None
131 } else {
132 Some(Tier(self.0 - 1))
133 }
134 }
135
136 pub fn ticks(self) -> Ticks {
138 <Ticks as TickInt>::pow5(self.exponent())
139 .expect("tier grid is bounded so that every tier fits the domain")
140 }
141
142 pub fn all_descending() -> impl Iterator<Item = Tier> {
144 (K_MIN..=K_MAX).rev().map(Tier)
145 }
146
147 pub fn all_ascending() -> impl Iterator<Item = Tier> {
149 (K_MIN..=K_MAX).map(Tier)
150 }
151}
152
153impl core::fmt::Display for Tier {
154 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
157 write!(f, "T{}", self.0)
158 }
159}
160
161#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
164#[non_exhaustive]
165pub enum TierName {
166 Deep,
168 Drift,
170 Span,
172 Sweep,
174 Arc,
176 Beat,
178 Flicker,
180 Glint,
182 Spark,
184 Tick,
186}
187
188impl TierName {
189 pub const fn key(self) -> &'static str {
191 match self {
192 TierName::Deep => "deep",
193 TierName::Drift => "drift",
194 TierName::Span => "span",
195 TierName::Sweep => "sweep",
196 TierName::Arc => "arc",
197 TierName::Beat => "beat",
198 TierName::Flicker => "flicker",
199 TierName::Glint => "glint",
200 TierName::Spark => "spark",
201 TierName::Tick => "tick",
202 }
203 }
204}
205
206pub const NAMED: &[(i8, TierName)] = &[
212 (5, TierName::Deep),
213 (4, TierName::Drift),
214 (3, TierName::Span),
215 (2, TierName::Sweep),
216 (1, TierName::Arc),
217 (0, TierName::Beat),
218 (-1, TierName::Flicker),
219 (-2, TierName::Glint),
220 (-3, TierName::Spark),
221 (-12, TierName::Tick),
222];
223
224pub fn name_of(tier: Tier) -> Option<TierName> {
226 let mut i = 0;
227 while i < NAMED.len() {
228 if NAMED[i].0 == tier.index() {
229 return Some(NAMED[i].1);
230 }
231 i += 1;
232 }
233 None
234}
235
236pub fn tier_of_key(key: &str) -> Option<Tier> {
239 NAMED
240 .iter()
241 .find(|(_, n)| n.key() == key)
242 .map(|(k, _)| Tier(*k))
243}
244
245pub struct TierTable {
252 ticks: [Option<Ticks>; TIER_COUNT],
253}
254
255impl TierTable {
256 pub fn build() -> TierTable {
258 let mut ticks: [Option<Ticks>; TIER_COUNT] = core::array::from_fn(|_| None);
259 for k in K_MIN..=K_MAX {
260 let idx = (k as isize - K_MIN as isize) as usize;
261 ticks[idx] = Some(Tier(k).ticks());
262 }
263 TierTable { ticks }
264 }
265
266 pub fn get(&self, tier: Tier) -> &Ticks {
268 let idx = (tier.index() as isize - K_MIN as isize) as usize;
269 self.ticks[idx]
270 .as_ref()
271 .expect("grid is fully populated by build()")
272 }
273
274 pub fn len(&self) -> usize {
276 TIER_COUNT
277 }
278
279 pub fn is_empty(&self) -> bool {
281 false
282 }
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288
289 #[test]
290 fn grid_has_45_tiers() {
291 assert_eq!(TIER_COUNT, 45);
292 assert_eq!(Tier::all_ascending().count(), 45);
293 }
294
295 #[test]
296 fn exponent_is_canonical_identity() {
297 for t in Tier::all_ascending() {
299 assert_eq!(Tier::from_exponent(t.exponent()).unwrap(), t);
300 }
301 assert_eq!(Tier::BEAT.exponent(), 60);
302 assert_eq!(Tier::TICK.exponent(), 0);
303 assert_eq!(Tier::DEEP.exponent(), 85);
304 assert_eq!(Tier::new(K_MAX).unwrap().exponent(), 220);
305 }
306
307 #[test]
308 fn off_grid_exponents_rejected() {
309 assert_eq!(Tier::from_exponent(61).unwrap_err().code, Code::E0080);
311 assert_eq!(Tier::from_exponent(225).unwrap_err().code, Code::E0080);
313 }
314
315 #[test]
316 fn each_tier_is_3125_of_the_one_below() {
317 let table = TierTable::build();
318 let gb = <Ticks as TickInt>::from_u64(GROUP_BASE as u64);
319 for k in (K_MIN + 1)..=K_MAX {
320 let hi = table.get(Tier::new(k).unwrap());
321 let lo = table.get(Tier::new(k - 1).unwrap());
322 assert_eq!(
323 hi,
324 &lo.try_mul(&gb).expect("within domain"),
325 "T{k} is not 3125 x T{}",
326 k - 1
327 );
328 }
329 }
330
331 #[test]
332 fn top_tier_fits_and_next_would_not() {
333 assert_eq!(<Ticks as TickInt>::pow5(220).unwrap().bit_len(), 511);
336 assert!(<Ticks as TickInt>::pow5(225).is_none());
337 }
338
339 #[test]
340 fn tick_tier_is_one_tick() {
341 assert_eq!(Tier::TICK.ticks(), <Ticks as TickInt>::one());
342 assert!(Tier::TICK.is_tick());
343 assert_eq!(Tier::TICK.finer(), None);
344 assert_eq!(Tier::new(K_MAX).unwrap().coarser(), None);
345 }
346
347 #[test]
348 fn names_are_display_only() {
349 assert_eq!(name_of(Tier::BEAT).unwrap().key(), "beat");
350 assert_eq!(tier_of_key("deep"), Some(Tier::DEEP));
351 assert_eq!(name_of(Tier::new(6).unwrap()), None);
353 assert_eq!(name_of(Tier::new(-4).unwrap()), None);
354 assert_eq!(tier_of_key("nonexistent"), None);
355 }
356
357 #[test]
358 fn no_duplicate_names() {
359 for (i, (_, a)) in NAMED.iter().enumerate() {
361 for (_, b) in NAMED.iter().skip(i + 1) {
362 assert_ne!(a.key(), b.key(), "duplicate tier name key");
363 }
364 }
365 }
366
367 #[test]
368 fn display_uses_index_notation() {
369 assert_eq!(Tier::BEAT.to_string(), "T0");
370 assert_eq!(Tier::DEEP.to_string(), "T5");
371 assert_eq!(Tier::TICK.to_string(), "T-12");
372 }
373}