use crate::backend::{TickInt, Ticks};
use crate::error::{Code, Result, TimeError};
pub const GROUP_BASE: u16 = 3125;
pub const DIGITS_PER_TIER: u32 = 5;
pub const BEAT_EXPONENT: u32 = 60;
pub const K_MIN: i8 = -12;
pub const K_MAX: i8 = 32;
pub const TIER_COUNT: usize = (K_MAX as isize - K_MIN as isize + 1) as usize;
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub struct Tier(i8);
impl Tier {
pub const BEAT: Tier = Tier(0);
pub const TICK: Tier = Tier(K_MIN);
pub const DEEP: Tier = Tier(5);
pub const DRIFT: Tier = Tier(4);
pub const SPAN: Tier = Tier(3);
pub const SWEEP: Tier = Tier(2);
pub const ARC: Tier = Tier(1);
pub const FLICKER: Tier = Tier(-1);
pub const GLINT: Tier = Tier(-2);
pub const SPARK: Tier = Tier(-3);
pub const fn new(k: i8) -> Result<Tier> {
if k < K_MIN || k > K_MAX {
Err(TimeError::with_context(
Code::E0080,
"tier index outside [-12, 32]",
))
} else {
Ok(Tier(k))
}
}
pub const fn from_exponent(exponent: u32) -> Result<Tier> {
if exponent % DIGITS_PER_TIER != 0 {
return Err(TimeError::with_context(
Code::E0080,
"exponent is not a multiple of 5, so it is not on the 5^(5k) grid",
));
}
let k = if exponent >= BEAT_EXPONENT {
((exponent - BEAT_EXPONENT) / DIGITS_PER_TIER) as i64
} else {
-(((BEAT_EXPONENT - exponent) / DIGITS_PER_TIER) as i64)
};
if k < K_MIN as i64 || k > K_MAX as i64 {
return Err(TimeError::with_context(
Code::E0080,
"exponent outside the profile grid",
));
}
Ok(Tier(k as i8))
}
pub const fn index(self) -> i8 {
self.0
}
pub const fn exponent(self) -> u32 {
(BEAT_EXPONENT as i64 + DIGITS_PER_TIER as i64 * self.0 as i64) as u32
}
pub const fn is_tick(self) -> bool {
self.0 == K_MIN
}
pub const fn coarser(self) -> Option<Tier> {
if self.0 >= K_MAX {
None
} else {
Some(Tier(self.0 + 1))
}
}
pub const fn finer(self) -> Option<Tier> {
if self.0 <= K_MIN {
None
} else {
Some(Tier(self.0 - 1))
}
}
pub fn ticks(self) -> Ticks {
<Ticks as TickInt>::pow5(self.exponent())
.expect("tier grid is bounded so that every tier fits the domain")
}
pub fn all_descending() -> impl Iterator<Item = Tier> {
(K_MIN..=K_MAX).rev().map(Tier)
}
pub fn all_ascending() -> impl Iterator<Item = Tier> {
(K_MIN..=K_MAX).map(Tier)
}
}
impl core::fmt::Display for Tier {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "T{}", self.0)
}
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
#[non_exhaustive]
pub enum TierName {
Deep,
Drift,
Span,
Sweep,
Arc,
Beat,
Flicker,
Glint,
Spark,
Tick,
}
impl TierName {
pub const fn key(self) -> &'static str {
match self {
TierName::Deep => "deep",
TierName::Drift => "drift",
TierName::Span => "span",
TierName::Sweep => "sweep",
TierName::Arc => "arc",
TierName::Beat => "beat",
TierName::Flicker => "flicker",
TierName::Glint => "glint",
TierName::Spark => "spark",
TierName::Tick => "tick",
}
}
}
pub const NAMED: &[(i8, TierName)] = &[
(5, TierName::Deep),
(4, TierName::Drift),
(3, TierName::Span),
(2, TierName::Sweep),
(1, TierName::Arc),
(0, TierName::Beat),
(-1, TierName::Flicker),
(-2, TierName::Glint),
(-3, TierName::Spark),
(-12, TierName::Tick),
];
pub fn name_of(tier: Tier) -> Option<TierName> {
let mut i = 0;
while i < NAMED.len() {
if NAMED[i].0 == tier.index() {
return Some(NAMED[i].1);
}
i += 1;
}
None
}
pub fn tier_of_key(key: &str) -> Option<Tier> {
NAMED
.iter()
.find(|(_, n)| n.key() == key)
.map(|(k, _)| Tier(*k))
}
pub struct TierTable {
ticks: [Option<Ticks>; TIER_COUNT],
}
impl TierTable {
pub fn build() -> TierTable {
let mut ticks: [Option<Ticks>; TIER_COUNT] = core::array::from_fn(|_| None);
for k in K_MIN..=K_MAX {
let idx = (k as isize - K_MIN as isize) as usize;
ticks[idx] = Some(Tier(k).ticks());
}
TierTable { ticks }
}
pub fn get(&self, tier: Tier) -> &Ticks {
let idx = (tier.index() as isize - K_MIN as isize) as usize;
self.ticks[idx]
.as_ref()
.expect("grid is fully populated by build()")
}
pub fn len(&self) -> usize {
TIER_COUNT
}
pub fn is_empty(&self) -> bool {
false
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn grid_has_45_tiers() {
assert_eq!(TIER_COUNT, 45);
assert_eq!(Tier::all_ascending().count(), 45);
}
#[test]
fn exponent_is_canonical_identity() {
for t in Tier::all_ascending() {
assert_eq!(Tier::from_exponent(t.exponent()).unwrap(), t);
}
assert_eq!(Tier::BEAT.exponent(), 60);
assert_eq!(Tier::TICK.exponent(), 0);
assert_eq!(Tier::DEEP.exponent(), 85);
assert_eq!(Tier::new(K_MAX).unwrap().exponent(), 220);
}
#[test]
fn off_grid_exponents_rejected() {
assert_eq!(Tier::from_exponent(61).unwrap_err().code, Code::E0080);
assert_eq!(Tier::from_exponent(225).unwrap_err().code, Code::E0080);
}
#[test]
fn each_tier_is_3125_of_the_one_below() {
let table = TierTable::build();
let gb = <Ticks as TickInt>::from_u64(GROUP_BASE as u64);
for k in (K_MIN + 1)..=K_MAX {
let hi = table.get(Tier::new(k).unwrap());
let lo = table.get(Tier::new(k - 1).unwrap());
assert_eq!(
hi,
&lo.try_mul(&gb).expect("within domain"),
"T{k} is not 3125 x T{}",
k - 1
);
}
}
#[test]
fn top_tier_fits_and_next_would_not() {
assert_eq!(<Ticks as TickInt>::pow5(220).unwrap().bit_len(), 511);
assert!(<Ticks as TickInt>::pow5(225).is_none());
}
#[test]
fn tick_tier_is_one_tick() {
assert_eq!(Tier::TICK.ticks(), <Ticks as TickInt>::one());
assert!(Tier::TICK.is_tick());
assert_eq!(Tier::TICK.finer(), None);
assert_eq!(Tier::new(K_MAX).unwrap().coarser(), None);
}
#[test]
fn names_are_display_only() {
assert_eq!(name_of(Tier::BEAT).unwrap().key(), "beat");
assert_eq!(tier_of_key("deep"), Some(Tier::DEEP));
assert_eq!(name_of(Tier::new(6).unwrap()), None);
assert_eq!(name_of(Tier::new(-4).unwrap()), None);
assert_eq!(tier_of_key("nonexistent"), None);
}
#[test]
fn no_duplicate_names() {
for (i, (_, a)) in NAMED.iter().enumerate() {
for (_, b) in NAMED.iter().skip(i + 1) {
assert_ne!(a.key(), b.key(), "duplicate tier name key");
}
}
}
#[test]
fn display_uses_index_notation() {
assert_eq!(Tier::BEAT.to_string(), "T0");
assert_eq!(Tier::DEEP.to_string(), "T5");
assert_eq!(Tier::TICK.to_string(), "T-12");
}
}