Skip to content

Commit 49c3399

Browse files
feat: Move humanization from ic-admin to nervous_system.
1 parent 2762cd0 commit 49c3399

File tree

10 files changed

+320
-249
lines changed

10 files changed

+320
-249
lines changed

Cargo.lock

Lines changed: 11 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ members = [
145145
"rs/nervous_system/common/test_canister",
146146
"rs/nervous_system/common/test_keys",
147147
"rs/nervous_system/common/test_utils",
148+
"rs/nervous_system/humanize",
148149
"rs/nervous_system/proto",
149150
"rs/nervous_system/proto/protobuf_generator",
150151
"rs/nervous_system/root",
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test")
2+
3+
package(default_visibility = ["//visibility:public"])
4+
5+
DEPENDENCIES = [
6+
"//rs/nervous_system/proto",
7+
"@crate_index//:humantime",
8+
"@crate_index//:lazy_static",
9+
"@crate_index//:regex",
10+
]
11+
12+
DEV_DEPENDENCIES = DEPENDENCIES + [
13+
]
14+
15+
rust_library(
16+
name = "humanize",
17+
srcs = glob(
18+
["src/**"],
19+
exclude = ["src/**/*tests.rs"],
20+
),
21+
crate_name = "ic_nervous_system_humanize",
22+
version = "0.0.1",
23+
deps = DEPENDENCIES,
24+
)
25+
26+
rust_test(
27+
name = "humanize_test",
28+
srcs = glob(["src/**"]),
29+
crate = ":humanize",
30+
deps = DEV_DEPENDENCIES,
31+
)

rs/nervous_system/humanize/Cargo.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[package]
2+
name = "ic-nervous-system-humanize"
3+
version = "0.0.1"
4+
edition = "2021"
5+
6+
[dependencies]
7+
humantime = "2.1.0"
8+
lazy_static = "1.4.0"
9+
regex = "1.3.0"
10+
ic-nervous-system-proto = { path = "../proto" }

rs/nervous_system/humanize/src/lib.rs

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
use core::fmt::Display;
2+
use ic_nervous_system_proto::pb::v1 as nervous_system_pb;
3+
use lazy_static::lazy_static;
4+
use regex::Regex;
5+
use std::str::FromStr;
6+
7+
#[cfg(test)]
8+
mod tests;
9+
10+
/// Parses decimal strings ending in "tokens" (plural), decimal strings end in
11+
/// "token" (singular) , or integer strings (again, base 10) ending in "e8s". In
12+
/// the case of "tokens" strings, the maximum number of digits after the
13+
/// (optional) decimal point is 8.
14+
///
15+
/// As with parse_fixed_point_decimal, "_" may be sprinkled throughout.
16+
///
17+
/// Whitespace around number is insignificant. E.g. " 42 tokens" is equivalent
18+
/// to "42tokens".
19+
pub fn parse_tokens(s: &str) -> Result<nervous_system_pb::Tokens, String> {
20+
let e8s = if let Some(s) = s.strip_suffix("tokens").map(|s| s.trim()) {
21+
parse_fixed_point_decimal(s, /* decimal_places = */ 8)?
22+
} else if let Some(s) = s.strip_suffix("token").map(|s| s.trim()) {
23+
parse_fixed_point_decimal(s, /* decimal_places = */ 8)?
24+
} else if let Some(s) = s.strip_suffix("e8s").map(|s| s.trim()) {
25+
u64::from_str(&s.replace('_', "")).map_err(|err| err.to_string())?
26+
} else {
27+
return Err(format!("Invalid tokens input string: {}", s));
28+
};
29+
let e8s = Some(e8s);
30+
31+
Ok(nervous_system_pb::Tokens { e8s })
32+
}
33+
34+
/// A wrapper around humantime::parse_duration that does some additional
35+
/// mechanical conversions.
36+
///
37+
/// To recapitulate the docs for humantime, "1w 2d 3h" gets parsed as
38+
///
39+
/// 1 week + 2 days + 3 hours
40+
/// =
41+
/// (1 * (7 * 24 * 60 * 60) + 2 * 24 * 60 * 60 + 3 * (60 * 60)) seconds
42+
pub fn parse_duration(s: &str) -> Result<nervous_system_pb::Duration, String> {
43+
humantime::parse_duration(s)
44+
.map(|d| nervous_system_pb::Duration {
45+
seconds: Some(d.as_secs()),
46+
})
47+
.map_err(|err| err.to_string())
48+
}
49+
50+
/// Similar to parse_fixed_point_decimal(s, 2), except a trailing percent sign
51+
/// is REQUIRED (and not fed into parse_fixed_point_decimal).
52+
pub fn parse_percentage(s: &str) -> Result<nervous_system_pb::Percentage, String> {
53+
let number = s
54+
.strip_suffix('%')
55+
.ok_or_else(|| format!("Input string must end with a percent sign: {}", s))?;
56+
57+
let basis_points = Some(parse_fixed_point_decimal(
58+
number, /* decimal_places = */ 2,
59+
)?);
60+
Ok(nervous_system_pb::Percentage { basis_points })
61+
}
62+
63+
/// Parses strings like "123_456.789" into 123456789. Notice that in this
64+
/// example, the decimal point in the result has been shifted to the right by 3
65+
/// places. The amount of such shifting is specified using the decimal_places
66+
/// parameter.
67+
///
68+
/// Also, notice that "_" (underscore) can be sprinkled as you wish in the input
69+
/// for readability. No need to use groups of size 3, although it is
70+
/// recommended, since that's what people are used to.
71+
///
72+
/// s is considered invalid if the number of digits after the decimal point >
73+
/// decimal_places.
74+
///
75+
/// The decimal point is optional, but if it is included, it must have at least
76+
/// one digit on each side (of course, the digit can be "0").
77+
///
78+
/// Prefixes (such as "0") do not change the base; base 10 is always
79+
/// used. Therefore, s = "0xDEAD_BEEF" is invalid, for example.
80+
fn parse_fixed_point_decimal(s: &str, decimal_places: usize) -> Result<u64, String> {
81+
lazy_static! {
82+
static ref REGEX: Regex = Regex::new(
83+
r"(?x) # Verbose (ignore white space, and comments, like this).
84+
^ # begin
85+
(?P<whole>[\d_]+) # Digit or underscores (for grouping digits).
86+
( # The dot + fractional part...
87+
[.] # dot
88+
(?P<fractional>[\d_]+)
89+
)? # ... is optional.
90+
$ # end
91+
"
92+
)
93+
.unwrap();
94+
}
95+
96+
let found = REGEX
97+
.captures(s)
98+
.ok_or_else(|| format!("Not a number: {}", s))?;
99+
100+
let whole = u64::from_str(
101+
&found
102+
.name("whole")
103+
.expect("Missing capture group?!")
104+
.as_str()
105+
.replace('_', ""),
106+
)
107+
.map_err(|err| err.to_string())?;
108+
109+
let fractional = format!(
110+
// Pad so that fractional ends up being of length (at least) decimal_places.
111+
"{:0<decimal_places$}",
112+
found
113+
.name("fractional")
114+
.map(|m| m.as_str())
115+
.unwrap_or("0")
116+
.replace('_', ""),
117+
);
118+
if fractional.len() > decimal_places {
119+
return Err(format!("Too many digits after the decimal place: {}", s));
120+
}
121+
let fractional = u64::from_str(&fractional).map_err(|err| err.to_string())?;
122+
123+
Ok(shift_decimal_right(whole, decimal_places)? + fractional)
124+
}
125+
126+
/// Multiplies n by 10^count.
127+
fn shift_decimal_right<I>(n: u64, count: I) -> Result<u64, String>
128+
where
129+
u32: TryFrom<I>,
130+
<u32 as TryFrom<I>>::Error: Display,
131+
I: Display + Copy,
132+
{
133+
let count = u32::try_from(count)
134+
.map_err(|err| format!("Unable to convert {} to u32. Reason: {}", count, err))?;
135+
136+
let boost = 10_u64
137+
.checked_pow(count)
138+
.ok_or_else(|| format!("Too large of an exponent: {}", count))?;
139+
140+
n.checked_mul(boost)
141+
.ok_or_else(|| format!("Too large of a decimal shift: {} >> {}", n, count))
142+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
use super::*;
2+
3+
#[test]
4+
fn test_parse_tokens() {
5+
assert_eq!(
6+
parse_tokens("1e8s"),
7+
Ok(nervous_system_pb::Tokens { e8s: Some(1) }),
8+
);
9+
assert_eq!(
10+
parse_tokens("1 token"),
11+
Ok(nervous_system_pb::Tokens {
12+
e8s: Some(100_000_000)
13+
}),
14+
);
15+
assert_eq!(
16+
parse_tokens("1_.23_4_tokens"),
17+
Ok(nervous_system_pb::Tokens {
18+
e8s: Some(123_400_000)
19+
}),
20+
);
21+
assert_eq!(
22+
parse_tokens("_123_456_789_e8s"),
23+
Ok(nervous_system_pb::Tokens {
24+
e8s: Some(123456789)
25+
}),
26+
);
27+
}
28+
29+
#[test]
30+
fn test_parse_percentage() {
31+
assert_eq!(
32+
parse_percentage("0%"),
33+
Ok(nervous_system_pb::Percentage {
34+
basis_points: Some(0)
35+
}),
36+
);
37+
assert_eq!(
38+
parse_percentage("1%"),
39+
Ok(nervous_system_pb::Percentage {
40+
basis_points: Some(100)
41+
}),
42+
);
43+
assert_eq!(
44+
parse_percentage("1.0%"),
45+
Ok(nervous_system_pb::Percentage {
46+
basis_points: Some(100)
47+
}),
48+
);
49+
assert_eq!(
50+
parse_percentage("1.00%"),
51+
Ok(nervous_system_pb::Percentage {
52+
basis_points: Some(100)
53+
}),
54+
);
55+
assert_eq!(
56+
parse_percentage("1.2%"),
57+
Ok(nervous_system_pb::Percentage {
58+
basis_points: Some(120)
59+
}),
60+
);
61+
assert_eq!(
62+
parse_percentage("1.23%"),
63+
Ok(nervous_system_pb::Percentage {
64+
basis_points: Some(123)
65+
}),
66+
);
67+
assert_eq!(
68+
parse_percentage("0.1%"),
69+
Ok(nervous_system_pb::Percentage {
70+
basis_points: Some(10)
71+
}),
72+
);
73+
assert_eq!(
74+
parse_percentage("0.12%"),
75+
Ok(nervous_system_pb::Percentage {
76+
basis_points: Some(12)
77+
}),
78+
);
79+
assert_eq!(
80+
parse_percentage("0.07%"),
81+
Ok(nervous_system_pb::Percentage {
82+
basis_points: Some(7)
83+
}),
84+
);
85+
86+
// Dot must be surrounded.
87+
let result = parse_percentage("0.%");
88+
assert!(result.is_err(), "{:?}", result);
89+
90+
let result = parse_percentage(".1%");
91+
assert!(result.is_err(), "{:?}", result);
92+
93+
// Too many decimal places.
94+
let result = parse_percentage("0.009%");
95+
assert!(result.is_err(), "{:?}", result);
96+
97+
// Percent sign required.
98+
let result = parse_percentage("1.0");
99+
assert!(result.is_err(), "{:?}", result);
100+
}
101+
102+
#[test]
103+
fn test_shift_decimal_right() {
104+
assert_eq!(shift_decimal_right(0, 0).unwrap(), 0,);
105+
assert_eq!(shift_decimal_right(0, 5).unwrap(), 0,);
106+
107+
assert_eq!(shift_decimal_right(1, 0).unwrap(), 1,);
108+
assert_eq!(shift_decimal_right(1, 1).unwrap(), 10,);
109+
assert_eq!(shift_decimal_right(1, 2).unwrap(), 100,);
110+
111+
assert_eq!(shift_decimal_right(23, 2).unwrap(), 2300,);
112+
}

rs/registry/admin/BUILD.bazel

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ DEPENDENCIES = [
1313
"//rs/interfaces/registry",
1414
"//rs/nervous_system/common",
1515
"//rs/nervous_system/common/test_keys",
16+
"//rs/nervous_system/humanize",
1617
"//rs/nervous_system/root",
1718
"//rs/nervous_system/proto",
1819
"//rs/nns/cmc",
@@ -54,11 +55,9 @@ DEPENDENCIES = [
5455
"@crate_index//:humantime",
5556
"@crate_index//:ic-btc-interface",
5657
"@crate_index//:itertools",
57-
"@crate_index//:lazy_static",
5858
"@crate_index//:maplit",
5959
"@crate_index//:pretty_assertions",
6060
"@crate_index//:prost",
61-
"@crate_index//:regex",
6261
"@crate_index//:reqwest",
6362
"@crate_index//:serde",
6463
"@crate_index//:serde-bytes-repr",

rs/registry/admin/Cargo.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,12 @@ edition = "2021"
77
anyhow = "1.0.31"
88
ic-prep = { path = "../../prep" }
99
async-trait = "0.1.51"
10-
regex = "1.3.9"
1110
bytes = "1.0.1"
1211
base64 = "0.13.0"
1312
clap = { version = "3.1.6", features = ["derive"] }
1413
futures = "0.3.8"
1514
hex = "0.4"
1615
humantime = "2.1.0"
17-
lazy_static = "1.4.0"
1816
cycles-minting-canister = { path ="../../nns/cmc" }
1917
ic-admin-derive = { path = "../admin-derive"}
2018
ic-btc-interface = { workspace = true }
@@ -42,6 +40,7 @@ ic-registry-subnet-type = { path = "../subnet_type" }
4240
ic-registry-transport = { path = "../transport" }
4341
ic-types = { path = "../../types/types" }
4442
ic-nervous-system-common = { path = "../../nervous_system/common" }
43+
ic-nervous-system-humanize = { path = "../../nervous_system/humanize" }
4544
ic-nervous-system-proto = { path = "../../nervous_system/proto" }
4645
ic-nervous-system-common-test-keys = { path = "../../nervous_system/common/test_keys" }
4746
ic-nervous-system-root = { path = "../../nervous_system/root" }

0 commit comments

Comments
 (0)