diff --git a/src/ordinal.rs b/src/ordinal.rs index 5e34b585d8..cb4caafc5b 100644 --- a/src/ordinal.rs +++ b/src/ordinal.rs @@ -24,6 +24,10 @@ impl Ordinal { Epoch::from(self).0 / CYCLE_EPOCHS } + pub(crate) fn percentile(self) -> String { + format!("{}%", (self.0 as f64 / Self::LAST.0 as f64) * 100.0) + } + pub(crate) fn epoch(self) -> Epoch { self.into() } @@ -79,8 +83,8 @@ impl Ordinal { Ok(Ordinal(Self::SUPPLY - x)) } - fn from_degree(s: &str) -> Result { - let (cycle_number, rest) = s + fn from_degree(degree: &str) -> Result { + let (cycle_number, rest) = degree .split_once('°') .ok_or_else(|| anyhow!("Missing degree symbol"))?; let cycle_number = cycle_number.parse::()?; @@ -133,8 +137,10 @@ impl Ordinal { Ok(height.starting_ordinal() + block_offset) } - fn from_decimal(s: &str) -> Result { - let (height, offset) = s.split_once('.').ok_or_else(|| anyhow!("Missing period"))?; + fn from_decimal(decimal: &str) -> Result { + let (height, offset) = decimal + .split_once('.') + .ok_or_else(|| anyhow!("Missing period"))?; let height = Height(height.parse()?); let offset = offset.parse::()?; @@ -144,6 +150,20 @@ impl Ordinal { Ok(height.starting_ordinal() + offset) } + + fn from_percentile(percentile: &str) -> Result { + if !percentile.ends_with('%') { + bail!("Invalid percentile: {}", percentile); + } + + let percentile = percentile[..percentile.len() - 1].parse::()?; + + let position = percentile / 100.0; + + let n = position * Ordinal::LAST.n() as f64; + + Ok(Ordinal(n.round() as u64)) + } } impl PartialEq for Ordinal { @@ -174,6 +194,8 @@ impl FromStr for Ordinal { Self::from_name(s) } else if s.contains('°') { Self::from_degree(s) + } else if s.contains('%') { + Self::from_percentile(s) } else if s.contains('.') { Self::from_decimal(s) } else { @@ -487,4 +509,30 @@ mod tests { assert_eq!(Ordinal(50 * 100_000_000).third(), 0); assert_eq!(Ordinal(50 * 100_000_000 + 1).third(), 1); } + + #[test] + fn percentile() { + assert_eq!(Ordinal(0).percentile(), "0%"); + assert_eq!( + Ordinal(Ordinal::LAST.n() / 2).percentile(), + "49.99999999999998%" + ); + assert_eq!(Ordinal::LAST.percentile(), "100%"); + } + + #[test] + fn percentile_round_trip() { + fn case(n: u64) { + let expected = Ordinal(n); + let actual = expected.percentile().parse::().unwrap(); + assert_eq!(expected, actual); + } + + for n in 0..1024 { + case(n); + case(Ordinal::LAST.n() / 2 + n); + case(Ordinal::LAST.n() - n); + case(Ordinal::LAST.n() / (n + 1)); + } + } } diff --git a/src/subcommand/server/templates/ordinal.rs b/src/subcommand/server/templates/ordinal.rs index fa8223c738..e8a2cd637d 100644 --- a/src/subcommand/server/templates/ordinal.rs +++ b/src/subcommand/server/templates/ordinal.rs @@ -37,6 +37,7 @@ mod tests {
offset
0
rarity
mythic
time
1970-01-01 00:00:00
+
percentile
0%
prev next @@ -66,6 +67,7 @@ mod tests {
offset
0
rarity
uncommon
time
1970-01-01 00:00:00
+
percentile
100%
prev next @@ -95,6 +97,7 @@ mod tests {
offset
1
rarity
common
time
1970-01-01 00:00:00
+
percentile
0.000000000000047619047671428595%
prev next diff --git a/templates/ordinal.html b/templates/ordinal.html index d0409f1fd5..9d0f010511 100644 --- a/templates/ordinal.html +++ b/templates/ordinal.html @@ -10,6 +10,7 @@

Ordinal {{ self.ordinal.n() }}

offset
{{ self.ordinal.third() }}
rarity
{{ self.ordinal.rarity() }}
time
{{ self.blocktime }}
+
percentile
{{ self.ordinal.percentile() }}
%% if self.ordinal.n() > 0 { prev