Skip to content

Commit

Permalink
Allow passing ordinals in degree and decimal notation (ordinals#261)
Browse files Browse the repository at this point in the history
  • Loading branch information
casey committed Jul 21, 2022
1 parent 594530d commit 6900370
Show file tree
Hide file tree
Showing 7 changed files with 240 additions and 44 deletions.
1 change: 1 addition & 0 deletions src/epoch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ mod tests {
(Epoch(0).subsidy() + Epoch(1).subsidy()) * Epoch::BLOCKS
);
assert_eq!(Epoch(33).starting_ordinal(), Ordinal(Ordinal::SUPPLY));
assert_eq!(Epoch(34).starting_ordinal(), Ordinal(Ordinal::SUPPLY));
}

#[test]
Expand Down
10 changes: 1 addition & 9 deletions src/height.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use super::*;

#[derive(Copy, Clone, Debug, Display)]
#[derive(Copy, Clone, Debug, Display, FromStr)]
pub(crate) struct Height(pub(crate) u64);

impl Height {
Expand Down Expand Up @@ -42,14 +42,6 @@ impl PartialEq<u64> for Height {
}
}

impl FromStr for Height {
type Err = Error;

fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self(s.parse::<u64>()?))
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
2 changes: 1 addition & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use {
arguments::Arguments, bytes::Bytes, epoch::Epoch, height::Height, index::Index, nft::Nft,
options::Options, ordinal::Ordinal, sat_point::SatPoint, subcommand::Subcommand,
},
anyhow::{anyhow, Context, Error},
anyhow::{anyhow, bail, Context, Error},
axum::{extract, http::StatusCode, response::IntoResponse, routing::get, Json, Router},
axum_server::Handle,
bdk::{
Expand Down
185 changes: 178 additions & 7 deletions src/ordinal.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
use super::*;

#[derive(
Copy, Clone, Eq, PartialEq, Debug, Display, Ord, PartialOrd, FromStr, Deserialize, Serialize,
)]
#[derive(Copy, Clone, Eq, PartialEq, Debug, Display, Ord, PartialOrd, Deserialize, Serialize)]
#[serde(transparent)]
pub(crate) struct Ordinal(pub(crate) u64);

Expand All @@ -27,7 +25,7 @@ impl Ordinal {
}

pub(crate) fn period(self) -> u64 {
self.0 / PERIOD_BLOCKS
self.height().n() / PERIOD_BLOCKS
}

pub(crate) fn subsidy_position(self) -> u64 {
Expand All @@ -52,6 +50,72 @@ impl Ordinal {
}
name.chars().rev().collect()
}

fn from_degree(s: &str) -> Result<Self> {
let (cycle_number, rest) = s
.split_once('°')
.ok_or_else(|| anyhow!("Missing degree symbol"))?;
let cycle_number = cycle_number.parse::<u64>()?;

let (epoch_offset, rest) = rest
.split_once('′')
.ok_or_else(|| anyhow!("Missing prime symbol"))?;
let epoch_offset = epoch_offset.parse::<u64>()?;
if epoch_offset >= Epoch::BLOCKS {
bail!("Invalid epoch offset");
}

let (period_offset, rest) = rest
.split_once('″')
.ok_or_else(|| anyhow!("Missing double prime symbol"))?;
let period_offset = period_offset.parse::<u64>()?;
if period_offset >= PERIOD_BLOCKS {
bail!("Invalid period offset");
}

let cycle_start_epoch = cycle_number * CYCLE_EPOCHS;

let cycle_progression = period_offset
.checked_sub(epoch_offset % PERIOD_BLOCKS)
.ok_or_else(|| anyhow!("Invalid relationship between epoch offset and period offset"))?;

if cycle_progression % (Epoch::BLOCKS % PERIOD_BLOCKS) != 0 {
bail!("Invalid relationship between epoch offset and period offset");
}

let epochs_since_cycle_start = cycle_progression / (Epoch::BLOCKS % PERIOD_BLOCKS);

let epoch = cycle_start_epoch + epochs_since_cycle_start;

let height = Height(epoch * Epoch::BLOCKS + epoch_offset);

let (block_offset, rest) = match rest.split_once('‴') {
Some((block_offset, rest)) => (block_offset.parse::<u64>()?, rest),
None => (0, rest),
};

if !rest.is_empty() {
bail!("Trailing characters");
}

if block_offset >= height.subsidy() {
bail!("Invalid block offset");
}

Ok(height.starting_ordinal() + block_offset)
}

fn from_decimal(s: &str) -> Result<Self> {
let (height, offset) = s.split_once('.').ok_or_else(|| anyhow!("Missing period"))?;
let height = Height(height.parse()?);
let offset = offset.parse::<u64>()?;

if offset >= height.subsidy() {
bail!("Invalid block offset");
}

Ok(height.starting_ordinal() + offset)
}
}

impl PartialEq<u64> for Ordinal {
Expand All @@ -74,6 +138,25 @@ impl AddAssign<u64> for Ordinal {
}
}

impl FromStr for Ordinal {
type Err = Error;

fn from_str(s: &str) -> Result<Self> {
if s.contains('°') {
Self::from_degree(s)
} else if s.contains('.') {
Self::from_decimal(s)
} else {
let ordinal = Self(s.parse()?);
if ordinal > Self::LAST {
Err(anyhow!("Invalid ordinal"))
} else {
Ok(ordinal)
}
}
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -102,6 +185,13 @@ mod tests {
assert_eq!(Ordinal(2099999997689999).name(), "a");
}

#[test]
fn period() {
assert_eq!(Ordinal(0).period(), 0);
assert_eq!(Ordinal(10080000000000).period(), 1);
assert_eq!(Ordinal(2099999997689999).period(), 3437);
}

#[test]
fn epoch() {
assert_eq!(Ordinal(0).epoch(), 0);
Expand Down Expand Up @@ -177,10 +267,91 @@ mod tests {
assert_eq!(ordinal, 101);
}

fn parse(s: &str) -> Result<Ordinal, String> {
s.parse::<Ordinal>().map_err(|e| e.to_string())
}

#[test]
fn from_str_decimal() {
assert_eq!(parse("0.0").unwrap(), 0);
assert_eq!(parse("0.1").unwrap(), 1);
assert_eq!(parse("1.0").unwrap(), 50 * 100_000_000);
assert_eq!(parse("6929999.0").unwrap(), 2099999997689999);
assert!(parse("0.5000000000").is_err());
assert!(parse("6930000.0").is_err());
}

#[test]
fn from_str_degree() {
assert_eq!(parse("0°0′0″0‴").unwrap(), 0);
assert_eq!(parse("0°0′0″").unwrap(), 0);
assert_eq!(parse("0°0′0″1‴").unwrap(), 1);
assert_eq!(parse("0°2015′2015″0‴").unwrap(), 10075000000000);
assert_eq!(parse("0°2016′0″0‴").unwrap(), 10080000000000);
assert_eq!(parse("0°2017′1″0‴").unwrap(), 10085000000000);
assert_eq!(parse("0°2016′0″1‴").unwrap(), 10080000000001);
assert_eq!(parse("0°2017′1″1‴").unwrap(), 10085000000001);
assert_eq!(parse("0°209999′335″0‴").unwrap(), 1049995000000000);
assert_eq!(parse("0°0′336″0‴").unwrap(), 1050000000000000);
assert_eq!(parse("0°0′672″0‴").unwrap(), 1575000000000000);
assert_eq!(parse("0°209999′1007″0‴").unwrap(), 1837498750000000);
assert_eq!(parse("0°0′1008″0‴").unwrap(), 1837500000000000);
assert_eq!(parse("1°0′0″0‴").unwrap(), 2067187500000000);
assert_eq!(parse("2°0′0″0‴").unwrap(), 2099487304530000);
assert_eq!(parse("3°0′0″0‴").unwrap(), 2099991988080000);
assert_eq!(parse("4°0′0″0‴").unwrap(), 2099999873370000);
assert_eq!(parse("5°0′0″0‴").unwrap(), 2099999996220000);
assert_eq!(parse("5°0′336″0‴").unwrap(), 2099999997060000);
assert_eq!(parse("5°0′672″0‴").unwrap(), 2099999997480000);
assert_eq!(parse("5°1′673″0‴").unwrap(), 2099999997480001);
assert_eq!(parse("5°209999′1007″0‴").unwrap(), 2099999997689999);
}

#[test]
fn from_str_number() {
assert_eq!(parse("0").unwrap(), 0);
assert!(parse("foo").is_err());
assert_eq!(parse("2099999997689999").unwrap(), 2099999997689999);
assert!(parse("2099999997690000").is_err());
}

#[test]
fn from_str_degree_invalid_cycle_number() {
assert!(parse("5°0′0″0‴").is_ok());
assert!(parse("6°0′0″0‴").is_err());
}

#[test]
fn from_str_degree_invalid_epoch_offset() {
assert!(parse("0°209999′335″0‴").is_ok());
assert!(parse("0°210000′336″0‴").is_err());
}

#[test]
fn from_str_degree_invalid_period_offset() {
assert!(parse("0°2015′2015″0‴").is_ok());
assert!(parse("0°2016′2016″0‴").is_err());
}

#[test]
fn from_str_degree_invalid_block_offset() {
assert!(parse("0°0′0″4999999999‴").is_ok());
assert!(parse("0°0′0″5000000000‴").is_err());
assert!(parse("0°209999′335″4999999999‴").is_ok());
assert!(parse("0°0′336″4999999999‴").is_err());
}

#[test]
fn from_str_degree_invalid_period_block_relationship() {
assert!(parse("0°2015′2015″0‴").is_ok());
assert!(parse("0°2016′0″0‴").is_ok());
assert!(parse("0°2016′1″0‴").is_err());
}

#[test]
fn from_str() {
assert_eq!("0".parse::<Ordinal>().unwrap(), 0);
assert!("foo".parse::<Ordinal>().is_err());
fn from_str_degree_post_distribution() {
assert!(parse("5°209999′1007″0‴").is_ok());
assert!(parse("5°0′1008″0‴").is_err());
}

#[test]
Expand Down
24 changes: 13 additions & 11 deletions src/subcommand/traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,12 @@ impl Traits {
return Err(anyhow!("Invalid ordinal"));
}

println!("name: {}", self.ordinal.name());

println!("cycle: {}", self.ordinal.cycle());

println!("epoch: {}", self.ordinal.epoch());

println!("height: {}", self.ordinal.height());

println!("period: {}", self.ordinal.period());

println!("offset: {}", self.ordinal.subsidy_position());
println!("number: {}", self.ordinal.n());
println!(
"decimal: {}.{}",
self.ordinal.height(),
self.ordinal.subsidy_position()
);

let height = self.ordinal.height().n();
let c = height / (CYCLE_EPOCHS * Epoch::BLOCKS);
Expand All @@ -30,6 +25,13 @@ impl Traits {
let o = self.ordinal.subsidy_position();
println!("degree: {c}°{e}′{p}″{o}‴");

println!("name: {}", self.ordinal.name());
println!("height: {}", self.ordinal.height());
println!("cycle: {}", self.ordinal.cycle());
println!("epoch: {}", self.ordinal.epoch());
println!("period: {}", self.ordinal.period());
println!("offset: {}", self.ordinal.subsidy_position());

println!(
"rarity: {}",
if c == 0 && o == 0 && p == 0 && e == 0 {
Expand Down
39 changes: 28 additions & 11 deletions tests/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ struct Test {
envs: Vec<(OsString, OsString)>,
events: Vec<Event>,
expected_status: i32,
expected_stderr: Option<String>,
expected_stderr: Expected,
expected_stdout: Expected,
tempdir: TempDir,
}
Expand All @@ -105,7 +105,7 @@ impl Test {
envs: Vec::new(),
events: Vec::new(),
expected_status: 0,
expected_stderr: None,
expected_stderr: Expected::Ignore,
expected_stdout: Expected::String(String::new()),
tempdir,
}
Expand Down Expand Up @@ -139,7 +139,7 @@ impl Test {
fn stdout_regex(self, expected_stdout: impl AsRef<str>) -> Self {
Self {
expected_stdout: Expected::Regex(
Regex::new(&format!("^{}$", expected_stdout.as_ref())).unwrap(),
Regex::new(&format!("(?s)^{}$", expected_stdout.as_ref())).unwrap(),
),
..self
}
Expand All @@ -155,7 +155,16 @@ impl Test {

fn expected_stderr(self, expected_stderr: &str) -> Self {
Self {
expected_stderr: Some(expected_stderr.to_owned()),
expected_stderr: Expected::String(expected_stderr.to_owned()),
..self
}
}

fn stderr_regex(self, expected_stderr: impl AsRef<str>) -> Self {
Self {
expected_stderr: Expected::Regex(
Regex::new(&format!("(?s)^{}$", expected_stderr.as_ref())).unwrap(),
),
..self
}
}
Expand Down Expand Up @@ -219,7 +228,7 @@ impl Test {
.envs(self.envs)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(if self.expected_stderr.is_some() {
.stderr(if !matches!(self.expected_stderr, Expected::Ignore) {
Stdio::piped()
} else {
Stdio::inherit()
Expand Down Expand Up @@ -291,21 +300,29 @@ impl Test {
);
}

let re = Regex::new(r"(?m)^\[.*\n")?;
let log_line_re = Regex::new(r"(?m)^\[.*\n")?;

for m in re.find_iter(stderr) {
print!("{}", m.as_str())
for log_line in log_line_re.find_iter(stderr) {
print!("{}", log_line.as_str())
}

if let Some(expected_stderr) = self.expected_stderr {
assert_eq!(re.replace_all(stderr, ""), expected_stderr);
let stripped_stderr = log_line_re.replace_all(stderr, "");

match self.expected_stderr {
Expected::String(expected_stderr) => assert_eq!(stripped_stderr, expected_stderr),
Expected::Regex(expected_stderr) => assert!(
expected_stderr.is_match(&stripped_stderr),
"stderr did not match regex: {:?}",
stripped_stderr
),
Expected::Ignore => {}
}

match self.expected_stdout {
Expected::String(expected_stdout) => assert_eq!(stdout, expected_stdout),
Expected::Regex(expected_stdout) => assert!(
expected_stdout.is_match(stdout),
"stdout did not match regex: {}",
"stdout did not match regex: {:?}",
stdout
),
Expected::Ignore => {}
Expand Down
Loading

0 comments on commit 6900370

Please sign in to comment.