Skip to content

Commit

Permalink
Add from_formatted_string
Browse files Browse the repository at this point in the history
  • Loading branch information
Sajjon committed Feb 18, 2024
1 parent ac0e057 commit 3dc5907
Show file tree
Hide file tree
Showing 7 changed files with 323 additions and 77 deletions.
17 changes: 17 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ bip39 = { version = "2.0.0", features = ["serde"] }
time-util = { version = "0.3.4", features = ["chrono"] }
assert-json-diff = "2.0.2"
url = { version = "2.5.0", features = ["serde"] }
num-format = "0.4.4"

[build-dependencies]
uniffi = { version = "0.26.0", features = ["build"] }
Expand Down
3 changes: 3 additions & 0 deletions src/core/error/common_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,9 @@ pub enum CommonError {

#[error("Invalid UUID (v4), got: {bad_value}")]
InvalidUUIDv4 { bad_value: String } = 10086,

#[error("Unrecognized Locale Identifier: {bad_value}")]
UnrecognizedLocaleIdentifier { bad_value: String } = 10087,
}

/*
Expand Down
183 changes: 106 additions & 77 deletions src/core/types/decimal192.rs
Original file line number Diff line number Diff line change
Expand Up @@ -281,80 +281,48 @@ impl Decimal {
}
}

/// Defines the rounding strategy used when you round e.g. `Decimal192`.
///
/// Following the same naming convention as https://docs.rs/rust_decimal/latest/rust_decimal/enum.RoundingStrategy.html.
#[derive(
Clone, Copy, Debug, PartialEq, Eq, enum_iterator::Sequence, uniffi::Enum,
)]
pub enum RoundingMode {
/// The number is always rounded toward positive infinity, e.g. `3.1 -> 4`, `-3.1 -> -3`.
ToPositiveInfinity,

/// The number is always rounded toward negative infinity, e.g. `3.1 -> 3`, `-3.1 -> -4`.
ToNegativeInfinity,

/// The number is always rounded toward zero, e.g. `3.1 -> 3`, `-3.1 -> -3`.
ToZero,

/// The number is always rounded away from zero, e.g. `3.1 -> 4`, `-3.1 -> -4`.
AwayFromZero,
impl Decimal192 {
pub const MACHINE_READABLE_DECIMAL_SEPARATOR: &'static str = ".";

/// The number is rounded to the nearest, and when it is halfway between two others, it's rounded toward zero, e.g. `3.5 -> 3`, `-3.5 -> -3`.
ToNearestMidpointTowardZero,

/// The number is rounded to the nearest, and when it is halfway between two others, it's rounded away from zero, e.g. `3.5 -> 4`, `-3.5 -> -4`.
ToNearestMidpointAwayFromZero,

/// The number is rounded to the nearest, and when it is halfway between two others, it's rounded toward the nearest even number. Also known as "Bankers Rounding".
ToNearestMidpointToEven,
}
/// Parse a local respecting string
pub fn new_with_formatted_string(
formatted_string: impl AsRef<str>,
locale: LocaleConfig,
) -> Result<Self> {
let formatted_string = formatted_string.as_ref().to_owned();
// Pad with a leading zero, to make numbers with leading decimal separator parsable
let mut string = format!("0{}", formatted_string);

impl From<RoundingMode> for ScryptoRoundingMode {
fn from(value: RoundingMode) -> Self {
match value {
RoundingMode::ToPositiveInfinity => {
ScryptoRoundingMode::ToPositiveInfinity
}
RoundingMode::ToNegativeInfinity => {
ScryptoRoundingMode::ToNegativeInfinity
}
RoundingMode::ToZero => ScryptoRoundingMode::ToZero,
RoundingMode::AwayFromZero => ScryptoRoundingMode::AwayFromZero,
RoundingMode::ToNearestMidpointTowardZero => {
ScryptoRoundingMode::ToNearestMidpointTowardZero
}
RoundingMode::ToNearestMidpointAwayFromZero => {
ScryptoRoundingMode::ToNearestMidpointAwayFromZero
}
RoundingMode::ToNearestMidpointToEven => {
ScryptoRoundingMode::ToNearestMidpointToEven
}
// If the locale recognizes a grouping separator, we strip that from the string
if let Some(grouping_separator) = locale.grouping_separator {
string = string.replace(&grouping_separator, "");
}
}
}

impl From<ScryptoRoundingMode> for RoundingMode {
fn from(value: ScryptoRoundingMode) -> Self {
match value {
ScryptoRoundingMode::ToPositiveInfinity => {
RoundingMode::ToPositiveInfinity
}
ScryptoRoundingMode::ToNegativeInfinity => {
RoundingMode::ToNegativeInfinity
}
ScryptoRoundingMode::ToZero => RoundingMode::ToZero,
ScryptoRoundingMode::AwayFromZero => RoundingMode::AwayFromZero,
ScryptoRoundingMode::ToNearestMidpointTowardZero => {
RoundingMode::ToNearestMidpointTowardZero
}
ScryptoRoundingMode::ToNearestMidpointAwayFromZero => {
RoundingMode::ToNearestMidpointAwayFromZero
}
ScryptoRoundingMode::ToNearestMidpointToEven => {
RoundingMode::ToNearestMidpointToEven
// `num` crate defines some pretty specific grouping separators: `"\u{a0}"` and `"\u{202f}"` for
// for some locales, but in unit tests we might use _normal_ space (`"U+0020"`), so we remove
// those (being a bit lenient...).
string = string.replace(' ', "");

// If the locale recognizes a decimal separator that is different from the machine readable one, we replace it with that
if let Some(decimal_separator) = locale.decimal_separator {
if decimal_separator != Self::MACHINE_READABLE_DECIMAL_SEPARATOR {
// If `decimal_separator != Self::MACHINE_READABLE_DECIMAL_SEPARATOR`,
// but if the string contains it, it might have been used incorrectly as
// a grouping separator. i.e. often "." is used in Swedish as a grouping
// separator, even though a space is the canonical one. So BEFORE
// we replace occurrences of decimal separator with "."
// (`Self::MACHINE_READABLE_DECIMAL_SEPARATOR`), we replace
// occurrences of `Self::MACHINE_READABLE_DECIMAL_SEPARATOR` with "".
string = string
.replace(Self::MACHINE_READABLE_DECIMAL_SEPARATOR, "");

Check warning on line 316 in src/core/types/decimal192.rs

View check run for this annotation

Codecov / codecov/patch

src/core/types/decimal192.rs#L316

Added line #L316 was not covered by tests

string = string.replace(
&decimal_separator,
Self::MACHINE_READABLE_DECIMAL_SEPARATOR,
);
}
}

string.parse::<Self>()
}
}

Expand Down Expand Up @@ -383,6 +351,16 @@ pub fn new_decimal_from_string(string: String) -> Result<Decimal192> {
Decimal192::new(string)
}

/// Tries to creates a new `Decimal192` from a formatted String for
/// a specific locale.
#[uniffi::export]
pub fn new_decimal_from_formatted_string(
formatted_string: String,
locale: LocaleConfig,
) -> Result<Decimal192> {
Decimal192::new_with_formatted_string(formatted_string, locale)
}

/// Creates a new `Decimal192` from a u32 integer.
#[uniffi::export]
pub fn new_decimal_from_u32(value: u32) -> Decimal192 {
Expand Down Expand Up @@ -559,7 +537,6 @@ mod test_inner {

#[cfg(test)]
mod test_decimal {
use enum_iterator::all;

use super::*;
use crate::prelude::*;
Expand Down Expand Up @@ -754,16 +731,44 @@ mod test_decimal {
}

#[test]
fn rounding_mode_conversion() {
let test = |m: RoundingMode| {
fn test_parse_formatted_decimal() {
let test = |s: &str, l: &LocaleConfig, exp: &str| {
assert_eq!(
Into::<RoundingMode>::into(Into::<ScryptoRoundingMode>::into(
m
)),
m
Decimal192::new_with_formatted_string(s, l.clone()).unwrap(),
exp.parse::<Decimal192>().unwrap()
)
};
all::<RoundingMode>().for_each(test);
let fail = |s: &str, l: &LocaleConfig| {
assert!(Decimal192::new_with_formatted_string(s, l.clone()).is_err())
};
let swedish = LocaleConfig::swedish();
let us = LocaleConfig::us();
test(",005", &swedish, "0.005");
test(".005", &us, "0.005");
test("1,001", &swedish, "1.001");
test("1,001", &us, "1001");
test("1\u{a0}001,45", &swedish, "1001.45");
test("1 001,45", &swedish, "1001.45");
test("1.001,45", &swedish, "1001.45");
test("1.001,45", &us, "1.00145");

fail("1.000.000", &us);
test("1.000.000", &swedish, "1000000");

fail("1.000.000,23", &us);
test("1.000.000,23", &swedish, "1000000.23");

test("1 000 000,23", &us, "100000023");
test("1 000 000,23", &swedish, "1000000.23");

test("1 000 000.23", &us, "1000000.23");
test("1 000 000.23", &swedish, "100000023");

fail("1,000,000", &swedish);
test("1,000,000", &us, "1000000");

fail("1,000,000.23", &swedish);
test("1,000,000.23", &us, "1000000.23");
}
}

Expand Down Expand Up @@ -1116,4 +1121,28 @@ mod uniffi_tests {
assert_eq!(decimal_clamped_to_zero(&-SUT::one()), SUT::zero());
assert_eq!(decimal_clamped_to_zero(&SUT::one()), SUT::one());
}

#[test]
fn from_formatted_string() {
let test = |s: &str, l: &LocaleConfig, exp: &str| {
assert_eq!(
new_decimal_from_formatted_string(s.to_owned(), l.clone())
.unwrap(),
exp.parse::<Decimal192>().unwrap()
)
};
let fail = |s: &str, l: &LocaleConfig| {
assert!(new_decimal_from_formatted_string(s.to_owned(), l.clone())
.is_err())
};
let swedish = LocaleConfig::swedish();
let us = LocaleConfig::us();
test(",005", &swedish, "0.005");
test(".005", &us, "0.005");
test("1,001", &swedish, "1.001");
test("1,001", &us, "1001");

fail("1,000,000.23", &swedish);
test("1,000,000.23", &us, "1000000.23");
}
}
92 changes: 92 additions & 0 deletions src/core/types/locale_config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
use crate::prelude::*;

#[derive(Clone, Debug, PartialEq, Eq, Hash, uniffi::Record)]
pub struct LocaleConfig {
pub decimal_separator: Option<String>,
pub grouping_separator: Option<String>,
}

impl LocaleConfig {
pub fn new(
decimal_separator: impl Into<Option<String>>,
grouping_separator: impl Into<Option<String>>,
) -> Self {
Self {
decimal_separator: decimal_separator.into(),
grouping_separator: grouping_separator.into(),
}
}
}

impl Default for LocaleConfig {
fn default() -> Self {
Self::new(".".to_owned(), " ".to_owned())
}
}

impl From<num_format::Locale> for LocaleConfig {
fn from(value: num_format::Locale) -> Self {
Self::new(
Some(value.decimal().to_owned()),
Some(value.separator().to_owned()),
)
}
}

impl LocaleConfig {
/// Tries to create a
/// A BCP-47 language identifier such as `"en_US_POSIX"`, `"sv_FI"` or `"zh_Hant_MO"`,
/// see: [list][list]
///
/// [list]: https://docs.rs/num-format/0.4.4/src/num_format/locale.rs.html#5565-6444
pub fn from_identifier(identifier: impl AsRef<str>) -> Result<Self> {
let identifier = identifier.as_ref().to_owned();
num_format::Locale::from_name(identifier.clone())
.map_err(|_| CommonError::UnrecognizedLocaleIdentifier {
bad_value: identifier,

Check warning on line 46 in src/core/types/locale_config.rs

View check run for this annotation

Codecov / codecov/patch

src/core/types/locale_config.rs#L46

Added line #L46 was not covered by tests
})
.map(Into::<Self>::into)
}
}

#[cfg(test)]
impl LocaleConfig {
pub fn swedish() -> Self {
Self::from_identifier("sv").expect("Sweden exists")
}
pub fn us() -> Self {
Self::from_identifier("en_US_POSIX").expect("US exists")
}
}

#[cfg(test)]
mod tests {
use crate::prelude::*;

#[test]
fn swedish() {
assert_eq!(LocaleConfig::swedish().decimal_separator.unwrap(), ",");
assert_eq!(
LocaleConfig::swedish().grouping_separator.unwrap(),
"\u{a0}"
);
}

#[test]
fn english_us() {
assert_eq!(LocaleConfig::us().decimal_separator.unwrap(), ".");
assert_eq!(LocaleConfig::us().grouping_separator.unwrap(), ",");
}

#[test]
fn default_uses_spaces_as_grouping_separator() {
let sut = LocaleConfig::default();
assert_eq!(&sut.grouping_separator.unwrap(), " ");
}

#[test]
fn default_uses_dot_as_decimal_separator() {
let sut = LocaleConfig::default();
assert_eq!(&sut.decimal_separator.unwrap(), ".");
}
}
4 changes: 4 additions & 0 deletions src/core/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ mod entity_kind;
mod hex_32bytes;
mod identified_vec_via;
mod keys;
mod locale_config;
mod logged_result;
mod rounding_mode;
mod safe_to_log;

pub use bag_of_bytes::*;
Expand All @@ -13,5 +15,7 @@ pub use entity_kind::*;
pub use hex_32bytes::*;
pub use identified_vec_via::*;
pub use keys::*;
pub use locale_config::*;
pub use logged_result::*;
pub use rounding_mode::*;
pub use safe_to_log::*;
Loading

0 comments on commit 3dc5907

Please sign in to comment.