From d199db0cc7c298a118c87b89ede3e44877ddd1c6 Mon Sep 17 00:00:00 2001 From: Joshix Date: Sat, 18 Jul 2026 20:00:00 +0000 Subject: [PATCH 01/31] POC: scientific notation --- core/src/ast.rs | 5 ++ core/src/error.rs | 9 ++++ core/src/num/bigrat.rs | 85 +++++++++++++++++++++++++++++--- core/src/num/formatting_style.rs | 9 ++++ core/src/value.rs | 23 ++++++++- 5 files changed, 123 insertions(+), 8 deletions(-) diff --git a/core/src/ast.rs b/core/src/ast.rs index 091ae145..c3f51674 100644 --- a/core/src/ast.rs +++ b/core/src/ast.rs @@ -706,6 +706,9 @@ fn evaluate_as( Value::Sf => { return Err(FendError::SpecifyNumSf); } + Value::Sn => { + return Err(FendError::SpecifyNumSn); + } Value::Base(base) => Value::Num(Box::new( evaluate(a, scope, attrs, spans, context, int)? .expect_num()? @@ -765,6 +768,7 @@ pub(crate) fn resolve_identifier( lowercase_builtin_result.or(unit_result) } +#[allow(clippy::too_many_lines)] fn resolve_builtin_identifier( ident: &Ident, scope: Option>, @@ -847,6 +851,7 @@ fn resolve_builtin_identifier( "float" => Value::Format(FormattingStyle::ExactFloat), "dp" => Value::Dp, "sf" => Value::Sf, + "sn" => Value::Sn, "base" => Value::BuiltInFunction(BuiltInFunction::Base), "dec" | "decimal" => Value::Base(Base::from_plain_base(10)?), "hex" | "hexadecimal" => Value::Base(Base::from_plain_base(16)?), diff --git a/core/src/error.rs b/core/src/error.rs index 007971fb..efbb05f0 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -38,12 +38,14 @@ pub(crate) enum FendError { InvalidDiceSyntax, SpecifyNumDp, SpecifyNumSf, + SpecifyNumSn, UnableToInvertFunction(&'static str), InvalidOperandsForSubtraction, InversesOfLambdasUnsupported, CouldNotFindKeyInObject, CouldNotFindKey(String), CannotFormatWithZeroSf, + CannotFormatWithZeroSn, UnableToGetCurrentDate, IsNotAFunction(String), IsNotAFunctionOrNumber(String), @@ -150,6 +152,10 @@ impl fmt::Display for FendError { f, "you need to specify what number of significant figures to use, e.g. '10 sf'" ), + Self::SpecifyNumSn => write!( + f, + "you need to specify what precision to use, e.g. '10 sn'" + ), Self::ExpectedAUnitlessNumber => write!(f, "expected a unitless number"), Self::ExpectedARealNumber => write!(f, "expected a real number"), Self::StringCannotBeLonger => write!(f, "string cannot be longer than one codepoint"), @@ -189,6 +195,9 @@ impl fmt::Display for FendError { Self::CannotFormatWithZeroSf => { write!(f, "cannot format a number with zero significant figures") } + Self::CannotFormatWithZeroSn => { + write!(f, "cannot format a number with zero precision in scientific notation") + } Self::IsNotAFunction(s) => write!(f, "'{s}' is not a function"), Self::IsNotAFunctionOrNumber(s) => write!(f, "'{s}' is not a function or number"), Self::IdentifierNotFound(s) => write!(f, "unknown identifier '{s}'"), diff --git a/core/src/num/bigrat.rs b/core/src/num/bigrat.rs index 04a6161b..ff9013c1 100644 --- a/core/src/num/bigrat.rs +++ b/core/src/num/bigrat.rs @@ -636,7 +636,7 @@ impl BigRat { )) } - #[allow(clippy::too_many_arguments)] + #[allow(clippy::too_many_arguments, clippy::too_many_lines)] fn format_as_decimal( &self, style: FormattingStyle, @@ -647,12 +647,13 @@ impl BigRat { decimal_separator: DecimalSeparatorStyle, int: &I, ) -> FResult> { - let integer_part = self.clone().num.div(&self.den, int)?; - let sf_limit = if let FormattingStyle::SignificantFigures(sf) = style { - Some(sf) - } else { - None + let integer_part = self.num.clone().div(&self.den, int)?; + + let sf_limit = match style { + FormattingStyle::SignificantFigures(sf) | FormattingStyle::ScientificNotation(sf) => Some(sf), + _ => None, }; + let formatted_integer_part = integer_part.format( &biguint::FormatOptions { base, @@ -662,6 +663,68 @@ impl BigRat { int, )?; + if let FormattingStyle::ScientificNotation(sf) = style { + let num_digits_of_int_part = formatted_integer_part.value.num_digits(); + + let positive_exponent = !integer_part.is_definitely_zero(); + + let mut exact: bool = formatted_integer_part.exact; + + let (value, exponent): (Box, usize) = if positive_exponent { + let mut string = formatted_integer_part.value.to_string(); + + if string.len() > sf { + string.truncate(sf); + } + + string.insert(1, '.'); + + (string.into(), (num_digits_of_int_part - 1)) + } else { + let decimal = self.format_as_decimal(FormattingStyle::SignificantFigures(sf), base, sign, term, terminating, decimal_separator, int)?; + + if !decimal.exact { + exact = false; + } + + let string = decimal.value.to_string(); + + let first_non_zero_digit = string.as_bytes().iter().enumerate().find(|(_, b)| { + assert!(b.is_ascii_digit() || **b == b'.'); + + **b != b'.' && **b != b'0' + }).unwrap().0; + + + let mut string = string.as_str()[first_non_zero_digit..].to_string(); + + if string.len() > sf { + string.truncate(sf); + } else { + while string.len() < sf { + string.push('0'); + } + } + + string.insert(1, '.'); + + (string.into(), first_non_zero_digit - 1) + }; + + return Ok(Exact::new( + FormattedBigRat { + sign, + ty: FormattedBigRatType::ScientificNotation( + value, + " × 10^", + if positive_exponent { "" } else { "-" }, + exponent, + ), + }, + exact, + )); + } + let num_trailing_digits_to_print = if style == FormattingStyle::ExactFloat || (style == FormattingStyle::Auto && terminating()?) || style == FormattingStyle::Exact @@ -1236,7 +1299,7 @@ impl Format for BigRat { x.sign = Sign::Positive; // try as integer if possible - if x.den == 1.into() { + if x.den == 1.into() && !matches!(style, FormattingStyle::ScientificNotation(_)) { let sf_limit = if let FormattingStyle::SignificantFigures(sf) = style { Some(sf) } else { @@ -1311,6 +1374,11 @@ enum FormattedBigRatType { // space // string (empty, "i", "pi", etc.) Decimal(String, bool, &'static str), + // string representation of decimal number (may not contain recurring digits) + // separator ("E", " × 10^") + // sign of exponent ("", "-", "+") + // exponent + ScientificNotation(Box, &'static str, &'static str, usize), } #[must_use] @@ -1371,6 +1439,9 @@ impl fmt::Display for FormattedBigRat { } write!(f, "{term}")?; } + FormattedBigRatType::ScientificNotation(m, separator, sign, exponent) => { + write!(f, "{m}{separator}{sign}{exponent}")?; + }, } Ok(()) } diff --git a/core/src/num/formatting_style.rs b/core/src/num/formatting_style.rs index 5950b5c5..6bd75ecf 100644 --- a/core/src/num/formatting_style.rs +++ b/core/src/num/formatting_style.rs @@ -20,6 +20,8 @@ pub(crate) enum FormattingStyle { DecimalPlaces(usize), /// Print with the given number of significant figures (not including any leading zeroes) SignificantFigures(usize), + /// Print with the given number of significant figures in scientific notation. + ScientificNotation(usize), /// If exact and no recurring digits: `ExactFloat`, if complex/imag: `MixedFraction`, /// otherwise: DecimalPlaces(10) #[default] @@ -38,6 +40,7 @@ impl fmt::Display for FormattingStyle { Self::Exact => write!(f, "exact"), Self::DecimalPlaces(d) => write!(f, "{d} dp"), Self::SignificantFigures(s) => write!(f, "{s} sf"), + Self::ScientificNotation(s) => write!(f, "{s} sn"), Self::Auto => write!(f, "auto"), } } @@ -52,6 +55,7 @@ impl fmt::Debug for FormattingStyle { Self::Exact => write!(f, "exact"), Self::DecimalPlaces(d) => write!(f, "{d} dp"), Self::SignificantFigures(s) => write!(f, "{s} sf"), + Self::ScientificNotation(s) => write!(f, "{s} sn"), Self::Auto => write!(f, "auto"), } } @@ -73,6 +77,10 @@ impl FormattingStyle { s.serialize(write)?; } Self::Auto => 7u8.serialize(write)?, + Self::ScientificNotation(s) => { + 8u8.serialize(write)?; + s.serialize(write)?; + } } Ok(()) } @@ -86,6 +94,7 @@ impl FormattingStyle { 5 => Self::DecimalPlaces(usize::deserialize(read)?), 6 => Self::SignificantFigures(usize::deserialize(read)?), 7 => Self::Auto, + 8 => Self::ScientificNotation(usize::deserialize(read)?), _ => { return Err(FendError::DeserializationError( "formatting style is out of range", diff --git a/core/src/value.rs b/core/src/value.rs index 34ddae72..14fed31d 100644 --- a/core/src/value.rs +++ b/core/src/value.rs @@ -25,6 +25,7 @@ pub(crate) enum Value { Format(FormattingStyle), Dp, Sf, + Sn, Base(Base), // user-defined function with a named parameter Fn(Ident, Box, Option>), @@ -61,7 +62,7 @@ impl Value { (Self::Num(a), Self::Num(b)) => a.compare(b, ctx.decimal_separator, int)?, (Self::BuiltInFunction(a), Self::BuiltInFunction(b)) => c(a == b), (Self::Format(a), Self::Format(b)) => c(a == b), - (Self::Dp, Self::Dp) | (Self::Sf, Self::Sf) | (Self::Unit, Self::Unit) => c(true), + (Self::Dp, Self::Dp) | (Self::Sf, Self::Sf) | (Self::Sn, Self::Sn) | (Self::Unit, Self::Unit) => c(true), (Self::Base(a), Self::Base(b)) => c(a == b), (Self::Fn(a1, a2, a3), Self::Fn(b1, b2, b3)) => c(a1 == b1 && a2.compare(b2, ctx, int)? @@ -151,6 +152,7 @@ impl Value { 13u8.serialize(write)?; d.serialize(write)?; } + Self::Sn => 14u8.serialize(write)?, } Ok(()) } @@ -189,6 +191,7 @@ impl Value { 11 => Self::Month(Month::deserialize(read)?), 12 => Self::DayOfWeek(DayOfWeek::deserialize(read)?), 13 => Self::Date(Date::deserialize(read)?), + 14 => Self::Sn, _ => { return Err(FendError::DeserializationError( "fend value type is out of range", @@ -204,6 +207,7 @@ impl Value { Self::Format(_) => "formatting style", Self::Dp => "decimal places", Self::Sf => "significant figures", + Self::Sn => "scientific notation", Self::Base(_) => "base", Self::Object(_) => "object", Self::String(_) => "string", @@ -295,6 +299,15 @@ impl Value { } return Ok(Self::Format(FormattingStyle::SignificantFigures(num))); } + if matches!(other, Self::Sn) { + let num = Self::Num(n) + .expect_num()? + .try_as_usize(context.decimal_separator, int)?; + if num == 0 { + return Err(FendError::CannotFormatWithZeroSn); + } + return Ok(Self::Format(FormattingStyle::ScientificNotation(num))); + } if apply_mul_handling == ApplyMulHandling::OnlyApply { let self_ = Self::Num(n); return Err(FendError::IsNotAFunction( @@ -410,6 +423,7 @@ impl Value { Ok(res) } + #[allow(clippy::too_many_lines)] pub(crate) fn format( &self, indent: usize, @@ -450,6 +464,12 @@ impl Value { kind: SpanKind::Keyword, }); } + Self::Sn => { + spans.push(Span { + string: "sn".to_string(), + kind: SpanKind::Keyword, + }); + } Self::Base(b) => { spans.push(Span { string: "base ".to_string(), @@ -545,6 +565,7 @@ impl fmt::Debug for Value { Self::Format(fmt) => write!(f, "format: {fmt:?}"), Self::Dp => write!(f, "dp"), Self::Sf => write!(f, "sf"), + Self::Sn => write!(f, "sn"), Self::Base(b) => write!(f, "base: {b:?}"), Self::Fn(name, expr, scope) => { write!(f, "fn: {name} => {expr:?} (scope: {scope:?})") From 8770e6d2a101f3fb37b203d02ec761b9926d3efc Mon Sep 17 00:00:00 2001 From: Joshix Date: Sat, 18 Jul 2026 20:00:00 +0000 Subject: [PATCH 02/31] only insert . if precision > 1 --- core/src/num/bigrat.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/core/src/num/bigrat.rs b/core/src/num/bigrat.rs index ff9013c1..e58f58a5 100644 --- a/core/src/num/bigrat.rs +++ b/core/src/num/bigrat.rs @@ -677,7 +677,9 @@ impl BigRat { string.truncate(sf); } - string.insert(1, '.'); + if sf > 1 { + string.insert(1, '.'); + } (string.into(), (num_digits_of_int_part - 1)) } else { @@ -706,7 +708,9 @@ impl BigRat { } } - string.insert(1, '.'); + if sf > 1 { + string.insert(1, '.'); + } (string.into(), first_non_zero_digit - 1) }; From 8c053e376737feca3342dd37157e96d58737a970 Mon Sep 17 00:00:00 2001 From: Joshix Date: Sat, 18 Jul 2026 20:00:00 +0000 Subject: [PATCH 03/31] cargo fmt --- core/src/error.rs | 12 +-- core/src/num/bigrat.rs | 125 +++++++++++++++++-------------- core/src/num/formatting_style.rs | 4 +- core/src/value.rs | 5 +- 4 files changed, 83 insertions(+), 63 deletions(-) diff --git a/core/src/error.rs b/core/src/error.rs index efbb05f0..27a9d460 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -152,10 +152,9 @@ impl fmt::Display for FendError { f, "you need to specify what number of significant figures to use, e.g. '10 sf'" ), - Self::SpecifyNumSn => write!( - f, - "you need to specify what precision to use, e.g. '10 sn'" - ), + Self::SpecifyNumSn => { + write!(f, "you need to specify what precision to use, e.g. '10 sn'") + } Self::ExpectedAUnitlessNumber => write!(f, "expected a unitless number"), Self::ExpectedARealNumber => write!(f, "expected a real number"), Self::StringCannotBeLonger => write!(f, "string cannot be longer than one codepoint"), @@ -196,7 +195,10 @@ impl fmt::Display for FendError { write!(f, "cannot format a number with zero significant figures") } Self::CannotFormatWithZeroSn => { - write!(f, "cannot format a number with zero precision in scientific notation") + write!( + f, + "cannot format a number with zero precision in scientific notation" + ) } Self::IsNotAFunction(s) => write!(f, "'{s}' is not a function"), Self::IsNotAFunctionOrNumber(s) => write!(f, "'{s}' is not a function or number"), diff --git a/core/src/num/bigrat.rs b/core/src/num/bigrat.rs index e58f58a5..ecebedfa 100644 --- a/core/src/num/bigrat.rs +++ b/core/src/num/bigrat.rs @@ -647,10 +647,12 @@ impl BigRat { decimal_separator: DecimalSeparatorStyle, int: &I, ) -> FResult> { - let integer_part = self.num.clone().div(&self.den, int)?; + let integer_part = self.num.clone().div(&self.den, int)?; let sf_limit = match style { - FormattingStyle::SignificantFigures(sf) | FormattingStyle::ScientificNotation(sf) => Some(sf), + FormattingStyle::SignificantFigures(sf) | FormattingStyle::ScientificNotation(sf) => { + Some(sf) + } _ => None, }; @@ -663,71 +665,84 @@ impl BigRat { int, )?; - if let FormattingStyle::ScientificNotation(sf) = style { - let num_digits_of_int_part = formatted_integer_part.value.num_digits(); - - let positive_exponent = !integer_part.is_definitely_zero(); + if let FormattingStyle::ScientificNotation(sf) = style { + let num_digits_of_int_part = formatted_integer_part.value.num_digits(); - let mut exact: bool = formatted_integer_part.exact; + let positive_exponent = !integer_part.is_definitely_zero(); - let (value, exponent): (Box, usize) = if positive_exponent { - let mut string = formatted_integer_part.value.to_string(); + let mut exact: bool = formatted_integer_part.exact; - if string.len() > sf { - string.truncate(sf); - } + let (value, exponent): (Box, usize) = if positive_exponent { + let mut string = formatted_integer_part.value.to_string(); - if sf > 1 { - string.insert(1, '.'); - } + if string.len() > sf { + string.truncate(sf); + } - (string.into(), (num_digits_of_int_part - 1)) - } else { - let decimal = self.format_as_decimal(FormattingStyle::SignificantFigures(sf), base, sign, term, terminating, decimal_separator, int)?; + if sf > 1 { + string.insert(1, '.'); + } - if !decimal.exact { - exact = false; - } + (string.into(), (num_digits_of_int_part - 1)) + } else { + let decimal = self.format_as_decimal( + FormattingStyle::SignificantFigures(sf), + base, + sign, + term, + terminating, + decimal_separator, + int, + )?; - let string = decimal.value.to_string(); + if !decimal.exact { + exact = false; + } - let first_non_zero_digit = string.as_bytes().iter().enumerate().find(|(_, b)| { - assert!(b.is_ascii_digit() || **b == b'.'); + let string = decimal.value.to_string(); - **b != b'.' && **b != b'0' - }).unwrap().0; + let first_non_zero_digit = string + .as_bytes() + .iter() + .enumerate() + .find(|(_, b)| { + assert!(b.is_ascii_digit() || **b == b'.'); + **b != b'.' && **b != b'0' + }) + .unwrap() + .0; - let mut string = string.as_str()[first_non_zero_digit..].to_string(); + let mut string = string.as_str()[first_non_zero_digit..].to_string(); - if string.len() > sf { - string.truncate(sf); - } else { - while string.len() < sf { - string.push('0'); - } - } + if string.len() > sf { + string.truncate(sf); + } else { + while string.len() < sf { + string.push('0'); + } + } - if sf > 1 { - string.insert(1, '.'); - } + if sf > 1 { + string.insert(1, '.'); + } - (string.into(), first_non_zero_digit - 1) - }; + (string.into(), first_non_zero_digit - 1) + }; - return Ok(Exact::new( - FormattedBigRat { - sign, - ty: FormattedBigRatType::ScientificNotation( - value, - " × 10^", - if positive_exponent { "" } else { "-" }, - exponent, - ), - }, - exact, - )); - } + return Ok(Exact::new( + FormattedBigRat { + sign, + ty: FormattedBigRatType::ScientificNotation( + value, + " × 10^", + if positive_exponent { "" } else { "-" }, + exponent, + ), + }, + exact, + )); + } let num_trailing_digits_to_print = if style == FormattingStyle::ExactFloat || (style == FormattingStyle::Auto && terminating()?) @@ -1443,9 +1458,9 @@ impl fmt::Display for FormattedBigRat { } write!(f, "{term}")?; } - FormattedBigRatType::ScientificNotation(m, separator, sign, exponent) => { - write!(f, "{m}{separator}{sign}{exponent}")?; - }, + FormattedBigRatType::ScientificNotation(m, separator, sign, exponent) => { + write!(f, "{m}{separator}{sign}{exponent}")?; + } } Ok(()) } diff --git a/core/src/num/formatting_style.rs b/core/src/num/formatting_style.rs index 6bd75ecf..5db4fa83 100644 --- a/core/src/num/formatting_style.rs +++ b/core/src/num/formatting_style.rs @@ -40,7 +40,7 @@ impl fmt::Display for FormattingStyle { Self::Exact => write!(f, "exact"), Self::DecimalPlaces(d) => write!(f, "{d} dp"), Self::SignificantFigures(s) => write!(f, "{s} sf"), - Self::ScientificNotation(s) => write!(f, "{s} sn"), + Self::ScientificNotation(s) => write!(f, "{s} sn"), Self::Auto => write!(f, "auto"), } } @@ -55,7 +55,7 @@ impl fmt::Debug for FormattingStyle { Self::Exact => write!(f, "exact"), Self::DecimalPlaces(d) => write!(f, "{d} dp"), Self::SignificantFigures(s) => write!(f, "{s} sf"), - Self::ScientificNotation(s) => write!(f, "{s} sn"), + Self::ScientificNotation(s) => write!(f, "{s} sn"), Self::Auto => write!(f, "auto"), } } diff --git a/core/src/value.rs b/core/src/value.rs index 14fed31d..7ce2d8ab 100644 --- a/core/src/value.rs +++ b/core/src/value.rs @@ -62,7 +62,10 @@ impl Value { (Self::Num(a), Self::Num(b)) => a.compare(b, ctx.decimal_separator, int)?, (Self::BuiltInFunction(a), Self::BuiltInFunction(b)) => c(a == b), (Self::Format(a), Self::Format(b)) => c(a == b), - (Self::Dp, Self::Dp) | (Self::Sf, Self::Sf) | (Self::Sn, Self::Sn) | (Self::Unit, Self::Unit) => c(true), + (Self::Dp, Self::Dp) + | (Self::Sf, Self::Sf) + | (Self::Sn, Self::Sn) + | (Self::Unit, Self::Unit) => c(true), (Self::Base(a), Self::Base(b)) => c(a == b), (Self::Fn(a1, a2, a3), Self::Fn(b1, b2, b3)) => c(a1 == b1 && a2.compare(b2, ctx, int)? From 20036492ff047a8ff4bfdb8525c3cd3f85d27b86 Mon Sep 17 00:00:00 2001 From: Joshix Date: Sat, 18 Jul 2026 20:00:00 +0000 Subject: [PATCH 04/31] remove the redundant `&` --- core/tests/integration_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/tests/integration_tests.rs b/core/tests/integration_tests.rs index 1fc46546..4e34ca5f 100644 --- a/core/tests/integration_tests.rs +++ b/core/tests/integration_tests.rs @@ -8,7 +8,7 @@ fn test_serialization_roundtrip(context: &mut Context) { match context.deserialize_variables(&mut v.as_slice()) { Ok(()) => (), Err(s) => { - eprintln!("Data: {:?}", &v); + eprintln!("Data: {:?}", v); eprintln!("Context: {ctx_debug_repr}"); panic!("Failed to deserialize: {s}"); } From 8d2934ff4172995dda72a8e58d8bcb02dded166d Mon Sep 17 00:00:00 2001 From: Joshix Date: Sat, 18 Jul 2026 21:00:00 +0000 Subject: [PATCH 05/31] bug fixes - respect decimal separator - fix some bugs - do not format 0 in scientific notation - add some tests --- core/src/num/bigrat.rs | 70 ++++++++++++------------ core/tests/integration_tests.rs | 95 ++++++++++++++++++++++++++++++++- 2 files changed, 130 insertions(+), 35 deletions(-) diff --git a/core/src/num/bigrat.rs b/core/src/num/bigrat.rs index ecebedfa..b7a8af0d 100644 --- a/core/src/num/bigrat.rs +++ b/core/src/num/bigrat.rs @@ -665,25 +665,19 @@ impl BigRat { int, )?; - if let FormattingStyle::ScientificNotation(sf) = style { + if !self.is_definitely_zero() + && let FormattingStyle::ScientificNotation(sf) = style + { let num_digits_of_int_part = formatted_integer_part.value.num_digits(); let positive_exponent = !integer_part.is_definitely_zero(); let mut exact: bool = formatted_integer_part.exact; - let (value, exponent): (Box, usize) = if positive_exponent { - let mut string = formatted_integer_part.value.to_string(); - - if string.len() > sf { - string.truncate(sf); - } - - if sf > 1 { - string.insert(1, '.'); - } + let (mut value, exponent): (String, usize) = if positive_exponent { + let string = formatted_integer_part.value.to_string(); - (string.into(), (num_digits_of_int_part - 1)) + (string, num_digits_of_int_part - 1) } else { let decimal = self.format_as_decimal( FormattingStyle::SignificantFigures(sf), @@ -699,42 +693,50 @@ impl BigRat { exact = false; } + let is_negative = decimal.value.sign == Sign::Negative; + let string = decimal.value.to_string(); - let first_non_zero_digit = string - .as_bytes() - .iter() - .enumerate() - .find(|(_, b)| { - assert!(b.is_ascii_digit() || **b == b'.'); + let trimmed_string = if is_negative { + let minus = string.chars().next().unwrap(); - **b != b'.' && **b != b'0' + &string.as_str()[minus.len_utf8()..] + } else { + string.as_str() + }; + let trimmed_string: String = trimmed_string + .trim_start_matches(|ch| { + ch == decimal_separator.decimal_separator() || ch == '0' }) - .unwrap() - .0; + .into(); - let mut string = string.as_str()[first_non_zero_digit..].to_string(); + let zeros = string + .chars() + .count() + .saturating_sub(trimmed_string.chars().count()) + .saturating_sub(1) + .saturating_sub(is_negative.into()); - if string.len() > sf { - string.truncate(sf); - } else { - while string.len() < sf { - string.push('0'); - } - } + (trimmed_string, zeros) + }; - if sf > 1 { - string.insert(1, '.'); + if value.len() > sf { + value.truncate(sf); + } else { + while value.len() < sf { + value.push('0'); } + } - (string.into(), first_non_zero_digit - 1) - }; + if sf > 1 { + value.insert(1, decimal_separator.decimal_separator()); + } return Ok(Exact::new( FormattedBigRat { sign, ty: FormattedBigRatType::ScientificNotation( - value, + value.into(), " × 10^", if positive_exponent { "" } else { "-" }, exponent, diff --git a/core/tests/integration_tests.rs b/core/tests/integration_tests.rs index 4e34ca5f..8cb686c3 100644 --- a/core/tests/integration_tests.rs +++ b/core/tests/integration_tests.rs @@ -8,7 +8,7 @@ fn test_serialization_roundtrip(context: &mut Context) { match context.deserialize_variables(&mut v.as_slice()) { Ok(()) => (), Err(s) => { - eprintln!("Data: {:?}", v); + eprintln!("Data: {v:?}"); eprintln!("Context: {ctx_debug_repr}"); panic!("Failed to deserialize: {s}"); } @@ -6167,3 +6167,96 @@ fn decimal_separator_comma() { "1,69 AUD" ); } + +#[test] +fn test_scientific_notation() { + let mut context = Context::new(); + assert_eq!( + evaluate("10^67 to 4 sn", &mut context) + .unwrap() + .get_main_result(), + "1.000 × 10^67" + ); + + let mut context = Context::new(); + assert_eq!( + evaluate("10^-67 to 4 sn", &mut context) + .unwrap() + .get_main_result(), + "1.000 × 10^-67" + ); + + let mut context = Context::new(); + assert_eq!( + evaluate("-10^67 to 5 sn", &mut context) + .unwrap() + .get_main_result(), + "-1.0000 × 10^67" + ); + + let mut context = Context::new(); + assert_eq!( + evaluate("-10^-67 to 5 sn", &mut context) + .unwrap() + .get_main_result(), + "-1.0000 × 10^-67" + ); +} + +#[test] +fn test_scientific_notation_with_comma() { + let mut context = Context::new(); + context.set_decimal_separator_style(fend_core::DecimalSeparatorStyle::Comma); + assert_eq!( + evaluate("10^67 to 4 sn", &mut context) + .unwrap() + .get_main_result(), + "1,000 × 10^67" + ); + + let mut context = Context::new(); + context.set_decimal_separator_style(fend_core::DecimalSeparatorStyle::Comma); + assert_eq!( + evaluate("10^-67 to 4 sn", &mut context) + .unwrap() + .get_main_result(), + "1,000 × 10^-67" + ); + + let mut context = Context::new(); + context.set_decimal_separator_style(fend_core::DecimalSeparatorStyle::Comma); + assert_eq!( + evaluate("-10^67 to 5 sn", &mut context) + .unwrap() + .get_main_result(), + "-1,0000 × 10^67" + ); + + let mut context = Context::new(); + context.set_decimal_separator_style(fend_core::DecimalSeparatorStyle::Comma); + assert_eq!( + evaluate("-10^-67 to 5 sn", &mut context) + .unwrap() + .get_main_result(), + "-1,0000 × 10^-67" + ); +} + +#[test] +fn test_scientific_notation_approx() { + let mut context = Context::new(); + assert_eq!( + evaluate("1 + 10^67 to 2 sn", &mut context) + .unwrap() + .get_main_result(), + "approx. 1.0 × 10^67" + ); + + let mut context = Context::new(); + assert_eq!( + evaluate("1 + 10^67 to 1 sn", &mut context) + .unwrap() + .get_main_result(), + "approx. 1 × 10^67" + ); +} From 160604b9b610c3114eb9974fe1e354b88155da1f Mon Sep 17 00:00:00 2001 From: Joshix Date: Sun, 19 Jul 2026 00:34:43 +0200 Subject: [PATCH 06/31] fix "pi to 4 sn" --- core/src/num/bigrat.rs | 54 +++++++++++++++------------------ core/tests/integration_tests.rs | 37 ++++++++++++++++++++++ 2 files changed, 62 insertions(+), 29 deletions(-) diff --git a/core/src/num/bigrat.rs b/core/src/num/bigrat.rs index b7a8af0d..c92b5ede 100644 --- a/core/src/num/bigrat.rs +++ b/core/src/num/bigrat.rs @@ -7,6 +7,7 @@ use crate::num::{Base, Exact, FormattingStyle, Range, RangeBound}; use crate::result::FResult; use crate::serialize::CborValue; use core::f64; +use std::fmt::Debug; use std::{cmp, fmt, hash, ops}; pub(crate) mod sign { @@ -672,39 +673,35 @@ impl BigRat { let positive_exponent = !integer_part.is_definitely_zero(); - let mut exact: bool = formatted_integer_part.exact; + let decimal = self.format_as_decimal( + FormattingStyle::SignificantFigures(sf), + base, + sign, + term, + terminating, + decimal_separator, + int, + )?; - let (mut value, exponent): (String, usize) = if positive_exponent { - let string = formatted_integer_part.value.to_string(); + let exact: bool = formatted_integer_part.exact && decimal.exact; - (string, num_digits_of_int_part - 1) - } else { - let decimal = self.format_as_decimal( - FormattingStyle::SignificantFigures(sf), - base, - sign, - term, - terminating, - decimal_separator, - int, - )?; - - if !decimal.exact { - exact = false; - } + let decimal = FormattedBigRat { + sign: Sign::Positive, + ty: decimal.value.ty, + }; - let is_negative = decimal.value.sign == Sign::Negative; + let (mut value, exponent): (String, usize) = if positive_exponent { + let mut string = decimal.to_string(); - let string = decimal.value.to_string(); + if let Some(idx) = string.find(decimal_separator.decimal_separator()) { + string.remove(idx); + } - let trimmed_string = if is_negative { - let minus = string.chars().next().unwrap(); + (string, num_digits_of_int_part - 1) + } else { + let string = decimal.to_string(); - &string.as_str()[minus.len_utf8()..] - } else { - string.as_str() - }; - let trimmed_string: String = trimmed_string + let trimmed_string: String = string .trim_start_matches(|ch| { ch == decimal_separator.decimal_separator() || ch == '0' }) @@ -714,8 +711,7 @@ impl BigRat { .chars() .count() .saturating_sub(trimmed_string.chars().count()) - .saturating_sub(1) - .saturating_sub(is_negative.into()); + .saturating_sub(1); (trimmed_string, zeros) }; diff --git a/core/tests/integration_tests.rs b/core/tests/integration_tests.rs index 8cb686c3..0558c7ff 100644 --- a/core/tests/integration_tests.rs +++ b/core/tests/integration_tests.rs @@ -6260,3 +6260,40 @@ fn test_scientific_notation_approx() { "approx. 1 × 10^67" ); } + +#[test] +fn test_scientific_notation_edge_cases() { + let mut context = Context::new(); + assert_eq!( + evaluate("pi to 4 sn", &mut context) + .unwrap() + .get_main_result(), + "approx. 3.142 × 10^0" + ); + + return; + + let mut context = Context::new(); + assert_eq!( + evaluate("5i to 3 sn", &mut context) + .unwrap() + .get_main_result(), + "5.00 × 10^0i" + ); + + let mut context = Context::new(); + assert_eq!( + evaluate("12389 to 3 sn", &mut context) + .unwrap() + .get_main_result(), + "1.24 × 10^4" + ); + + let mut context = Context::new(); + assert_eq!( + evaluate("123.456 to 5 sn", &mut context) + .unwrap() + .get_main_result(), + "1.2346 × 10^2" + ); +} From c35a190ecd4427f2e5fe66e522fd398ef2ded058 Mon Sep 17 00:00:00 2001 From: Joshix Date: Sat, 18 Jul 2026 22:00:00 +0000 Subject: [PATCH 07/31] try fix imaginary scientific notation --- core/src/num/bigrat.rs | 18 +++++++++++++++--- core/tests/integration_tests.rs | 4 ++-- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/core/src/num/bigrat.rs b/core/src/num/bigrat.rs index c92b5ede..ea275a21 100644 --- a/core/src/num/bigrat.rs +++ b/core/src/num/bigrat.rs @@ -690,6 +690,12 @@ impl BigRat { ty: decimal.value.ty, }; + let (FormattedBigRatType::Integer(_, _, is_imag, _) + | FormattedBigRatType::Decimal(_, _, is_imag)) = decimal.ty + else { + unreachable!() + }; + let (mut value, exponent): (String, usize) = if positive_exponent { let mut string = decimal.to_string(); @@ -697,6 +703,10 @@ impl BigRat { string.remove(idx); } + while string.ends_with('i') { + string.remove(string.len() - 1); + } + (string, num_digits_of_int_part - 1) } else { let string = decimal.to_string(); @@ -736,6 +746,7 @@ impl BigRat { " × 10^", if positive_exponent { "" } else { "-" }, exponent, + is_imag, ), }, exact, @@ -1395,7 +1406,8 @@ enum FormattedBigRatType { // separator ("E", " × 10^") // sign of exponent ("", "-", "+") // exponent - ScientificNotation(Box, &'static str, &'static str, usize), + // string (empty, "i", "pi", etc.) + ScientificNotation(Box, &'static str, &'static str, usize, &'static str), } #[must_use] @@ -1456,8 +1468,8 @@ impl fmt::Display for FormattedBigRat { } write!(f, "{term}")?; } - FormattedBigRatType::ScientificNotation(m, separator, sign, exponent) => { - write!(f, "{m}{separator}{sign}{exponent}")?; + FormattedBigRatType::ScientificNotation(m, separator, sign, exponent, imag) => { + write!(f, "{m}{separator}{sign}{exponent}{imag}")?; } } Ok(()) diff --git a/core/tests/integration_tests.rs b/core/tests/integration_tests.rs index 0558c7ff..b910e6c8 100644 --- a/core/tests/integration_tests.rs +++ b/core/tests/integration_tests.rs @@ -6271,8 +6271,6 @@ fn test_scientific_notation_edge_cases() { "approx. 3.142 × 10^0" ); - return; - let mut context = Context::new(); assert_eq!( evaluate("5i to 3 sn", &mut context) @@ -6281,6 +6279,8 @@ fn test_scientific_notation_edge_cases() { "5.00 × 10^0i" ); + return; + let mut context = Context::new(); assert_eq!( evaluate("12389 to 3 sn", &mut context) From c38e127c96f2b7c5839161722314490be9ce2dd9 Mon Sep 17 00:00:00 2001 From: Joshix Date: Sat, 18 Jul 2026 22:00:00 +0000 Subject: [PATCH 08/31] rounding test, comment out failing test --- core/tests/integration_tests.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/core/tests/integration_tests.rs b/core/tests/integration_tests.rs index b910e6c8..51b52120 100644 --- a/core/tests/integration_tests.rs +++ b/core/tests/integration_tests.rs @@ -6279,21 +6279,21 @@ fn test_scientific_notation_edge_cases() { "5.00 × 10^0i" ); - return; - let mut context = Context::new(); assert_eq!( - evaluate("12389 to 3 sn", &mut context) + evaluate("123.456 to 5 sn", &mut context) .unwrap() .get_main_result(), - "1.24 × 10^4" + "approx. 1.2346 × 10^2" ); + /* let mut context = Context::new(); assert_eq!( - evaluate("123.456 to 5 sn", &mut context) + evaluate("12389 to 3 sn", &mut context) .unwrap() .get_main_result(), - "1.2346 × 10^2" + "approx. 1.24 × 10^4" ); + */ } From 7810334529c6f639c9aa263edb8aae381a1d2a9a Mon Sep 17 00:00:00 2001 From: Joshix Date: Sun, 19 Jul 2026 09:00:00 +0000 Subject: [PATCH 09/31] fix rounding of integers with sf --- core/src/num/biguint.rs | 37 +++++++++++++++++++++++++++++---- core/tests/integration_tests.rs | 18 ++++++++-------- 2 files changed, 42 insertions(+), 13 deletions(-) diff --git a/core/src/num/biguint.rs b/core/src/num/biguint.rs index f64cbb28..30ec0a3d 100644 --- a/core/src/num/biguint.rs +++ b/core/src/num/biguint.rs @@ -1487,11 +1487,40 @@ impl fmt::Display for FormattedBigUint { FormattedBigUintType::Zero => write!(f, "0")?, FormattedBigUintType::Simple(i) => write!(f, "{i}")?, FormattedBigUintType::Complex(s, sf_limit) => { - for (i, ch) in s.chars().rev().enumerate() { - if sf_limit.is_some() && &Some(i) >= sf_limit { + let mut chars = s.chars().rev(); + + if let Some(sf_limit) = sf_limit { + for char in (&mut chars).take(sf_limit - 1) { + write!(f, "{char}")?; + } + + let mut chars = chars.peekable(); + + if let Some(last_non_zero_char) = chars.next() { + debug_assert!( + last_non_zero_char.is_ascii_digit(), + "{last_non_zero_char} is not an ascii digit" + ); + + let mut last_digit: u8 = last_non_zero_char as u8 - b'0'; + let after_digit = chars.peek().map_or(0u8, |ch| { + debug_assert!(ch.is_ascii_digit(), "{ch} is not an ascii digit"); + *ch as u8 - b'0' + }); + + if after_digit >= 5 { + last_digit += 1; + } + + write!(f, "{last_digit}")?; + } + + for _ in chars { write!(f, "0")?; - } else { - write!(f, "{ch}")?; + } + } else { + for char in chars { + write!(f, "{char}")?; } } } diff --git a/core/tests/integration_tests.rs b/core/tests/integration_tests.rs index 1fc46546..3db79d4a 100644 --- a/core/tests/integration_tests.rs +++ b/core/tests/integration_tests.rs @@ -4240,17 +4240,17 @@ fn sf_3() { #[test] fn sf_4() { - test_eval("1234567.55645 to 4 sf", "approx. 1234000"); + test_eval("1234567.55645 to 4 sf", "approx. 1235000"); } #[test] fn sf_5() { - test_eval("1234567.55645 to 5 sf", "approx. 1234500"); + test_eval("1234567.55645 to 5 sf", "approx. 1234600"); } #[test] fn sf_6() { - test_eval("1234567.55645 to 6 sf", "approx. 1234560"); + test_eval("1234567.55645 to 6 sf", "approx. 1234570"); } #[test] @@ -4380,12 +4380,12 @@ fn million_pi_3_sf() { #[test] fn million_pi_4_sf() { - test_eval("1e6 pi to 4 sf", "approx. 3141000"); + test_eval("1e6 pi to 4 sf", "approx. 3142000"); } #[test] fn million_pi_5_sf() { - test_eval("1e6 pi to 5 sf", "approx. 3141500"); + test_eval("1e6 pi to 5 sf", "approx. 3141600"); } #[test] @@ -4430,17 +4430,17 @@ fn large_integer_to_3_sf() { #[test] fn large_integer_to_4_sf() { - test_eval("1234567 to 4 sf", "approx. 1234000"); + test_eval("1234567 to 4 sf", "approx. 1235000"); } #[test] fn large_integer_to_5_sf() { - test_eval("1234567 to 5 sf", "approx. 1234500"); + test_eval("1234567 to 5 sf", "approx. 1234600"); } #[test] fn large_integer_to_6_sf() { - test_eval("1234567 to 6 sf", "approx. 1234560"); + test_eval("1234567 to 6 sf", "approx. 1234570"); } #[test] @@ -4465,7 +4465,7 @@ fn large_integer_to_10_sf() { #[test] fn trailing_zeroes_sf_1() { - test_eval("1234560 to 5sf", "approx. 1234500"); + test_eval("1234560 to 5sf", "approx. 1234600"); } #[test] From f8b5d4623ac25167563cddc9edd0389cff13dec9 Mon Sep 17 00:00:00 2001 From: Joshix Date: Sun, 19 Jul 2026 09:00:00 +0000 Subject: [PATCH 10/31] fix ci --- core/tests/integration_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/tests/integration_tests.rs b/core/tests/integration_tests.rs index 3db79d4a..53f02765 100644 --- a/core/tests/integration_tests.rs +++ b/core/tests/integration_tests.rs @@ -8,7 +8,7 @@ fn test_serialization_roundtrip(context: &mut Context) { match context.deserialize_variables(&mut v.as_slice()) { Ok(()) => (), Err(s) => { - eprintln!("Data: {:?}", &v); + eprintln!("Data: {v:?}"); eprintln!("Context: {ctx_debug_repr}"); panic!("Failed to deserialize: {s}"); } From 81914b73eab359c6e2fd96ee81f3a295dd51ea64 Mon Sep 17 00:00:00 2001 From: Joshix Date: Sun, 19 Jul 2026 09:00:00 +0000 Subject: [PATCH 11/31] uncommend rounding test and add another --- core/tests/integration_tests.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/core/tests/integration_tests.rs b/core/tests/integration_tests.rs index e9060e6c..6460c45d 100644 --- a/core/tests/integration_tests.rs +++ b/core/tests/integration_tests.rs @@ -6287,7 +6287,6 @@ fn test_scientific_notation_edge_cases() { "approx. 1.2346 × 10^2" ); - /* let mut context = Context::new(); assert_eq!( evaluate("12389 to 3 sn", &mut context) @@ -6295,5 +6294,12 @@ fn test_scientific_notation_edge_cases() { .get_main_result(), "approx. 1.24 × 10^4" ); - */ + + let mut context = Context::new(); + assert_eq!( + evaluate("666 to 2 sn", &mut context) + .unwrap() + .get_main_result(), + "approx. 6.7 × 10^2" + ); } From b2cfe40dbf604b171891a6daae834e7a82458b23 Mon Sep 17 00:00:00 2001 From: Joshix Date: Sun, 19 Jul 2026 10:00:00 +0000 Subject: [PATCH 12/31] always store base in FormattedBigUint --- core/src/num/biguint.rs | 67 +++++++++++++++++++++------------ core/tests/integration_tests.rs | 11 ++++++ 2 files changed, 53 insertions(+), 25 deletions(-) diff --git a/core/src/num/biguint.rs b/core/src/num/biguint.rs index 30ec0a3d..2b9d59a1 100644 --- a/core/src/num/biguint.rs +++ b/core/src/num/biguint.rs @@ -1379,16 +1379,11 @@ impl Format for BigUint { type Out = FormattedBigUint; fn format(&self, params: &Self::Params, int: &I) -> FResult> { - let base_prefix = if params.write_base_prefix { - Some(params.base) - } else { - None - }; - if self.is_zero() { return Ok(Exact::new( FormattedBigUint { - base: base_prefix, + write_base_prefix: params.write_base_prefix, + base: params.base, ty: FormattedBigUintType::Zero, }, true, @@ -1400,7 +1395,8 @@ impl Format for BigUint { if num.value_len() == 1 && params.base.base_as_u8() == 10 && params.sf_limit.is_none() { Exact::new( FormattedBigUint { - base: base_prefix, + write_base_prefix: params.write_base_prefix, + base: params.base, ty: FormattedBigUintType::Simple(num.get(0)), }, true, @@ -1454,7 +1450,8 @@ impl Format for BigUint { .is_none_or(|sf| sf >= output.len() - num_leading_zeroes); Exact::new( FormattedBigUint { - base: base_prefix, + write_base_prefix: params.write_base_prefix, + base: params.base, ty: FormattedBigUintType::Complex(output, params.sf_limit), }, exact, @@ -1474,14 +1471,27 @@ enum FormattedBigUintType { #[must_use] #[derive(Debug)] pub(crate) struct FormattedBigUint { - base: Option, + write_base_prefix: bool, + base: Base, ty: FormattedBigUintType, } +fn parse_char(ch: char) -> u8 { + if ch.is_ascii_digit() { + ch as u8 - b'0' + } else if ch.is_ascii_lowercase() { + 10 + ch as u8 - b'a' + } else if ch.is_ascii_uppercase() { + 10 + ch as u8 - b'A' + } else { + unreachable!("{ch} needs to be a digit"); + } +} + impl fmt::Display for FormattedBigUint { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { - if let Some(base) = self.base { - base.write_prefix(f)?; + if self.write_base_prefix { + self.base.write_prefix(f)?; } match &self.ty { FormattedBigUintType::Zero => write!(f, "0")?, @@ -1497,22 +1507,29 @@ impl fmt::Display for FormattedBigUint { let mut chars = chars.peekable(); if let Some(last_non_zero_char) = chars.next() { - debug_assert!( - last_non_zero_char.is_ascii_digit(), - "{last_non_zero_char} is not an ascii digit" - ); - - let mut last_digit: u8 = last_non_zero_char as u8 - b'0'; - let after_digit = chars.peek().map_or(0u8, |ch| { - debug_assert!(ch.is_ascii_digit(), "{ch} is not an ascii digit"); - *ch as u8 - b'0' - }); - - if after_digit >= 5 { + let mut last_digit: u8 = parse_char(last_non_zero_char); + let after_digit = chars.peek().map_or(0u8, |ch| parse_char(*ch)); + + let base = self.base.base_as_u8(); + + debug_assert!(last_digit < base); + debug_assert!(after_digit < base); + + if after_digit >= base.div_ceil(2) { last_digit += 1; } - write!(f, "{last_digit}")?; + if last_digit == base { + write!(f, "10")?; + } else { + debug_assert!(last_digit < base); + write!( + f, + "{}", + Base::digit_as_char(last_digit.into()) + .expect("needs to be valid char") + )?; + } } for _ in chars { diff --git a/core/tests/integration_tests.rs b/core/tests/integration_tests.rs index 53f02765..62b477ce 100644 --- a/core/tests/integration_tests.rs +++ b/core/tests/integration_tests.rs @@ -4226,6 +4226,7 @@ fn zero_sf() { #[test] fn sf_1() { test_eval("1234567.55645 to 1 sf", "approx. 1000000"); + test_eval("99 to 1 sf", "approx. 100"); } #[test] @@ -4288,6 +4289,16 @@ fn sf_13() { test_eval("1234567.55645 to 13 sf", "1234567.55645"); } +#[test] +fn sf_hex_1() { + test_eval("0xff to 1 sf", "approx. 0x100"); +} + +#[test] +fn sf_hex_2() { + test_eval("0xff to 2 sf", "0xff"); +} + #[test] fn sf_small_1() { test_eval("pi / 1000000 to 1 sf", "approx. 0.000003"); From 81b953a5b7ed7bdd6b6b7838ab096c294f4a903e Mon Sep 17 00:00:00 2001 From: Joshix Date: Sun, 19 Jul 2026 10:00:00 +0000 Subject: [PATCH 13/31] remove redundant bool option --- core/src/num/biguint.rs | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/core/src/num/biguint.rs b/core/src/num/biguint.rs index 2b9d59a1..933a16c8 100644 --- a/core/src/num/biguint.rs +++ b/core/src/num/biguint.rs @@ -1379,11 +1379,15 @@ impl Format for BigUint { type Out = FormattedBigUint; fn format(&self, params: &Self::Params, int: &I) -> FResult> { + let base = if params.write_base_prefix { + params.base + } else { + Base::from_plain_base(params.base.base_as_u8()).expect("is valid base") + }; if self.is_zero() { return Ok(Exact::new( FormattedBigUint { - write_base_prefix: params.write_base_prefix, - base: params.base, + base, ty: FormattedBigUintType::Zero, }, true, @@ -1395,8 +1399,8 @@ impl Format for BigUint { if num.value_len() == 1 && params.base.base_as_u8() == 10 && params.sf_limit.is_none() { Exact::new( FormattedBigUint { - write_base_prefix: params.write_base_prefix, - base: params.base, + base, + ty: FormattedBigUintType::Simple(num.get(0)), }, true, @@ -1450,8 +1454,8 @@ impl Format for BigUint { .is_none_or(|sf| sf >= output.len() - num_leading_zeroes); Exact::new( FormattedBigUint { - write_base_prefix: params.write_base_prefix, - base: params.base, + base, + ty: FormattedBigUintType::Complex(output, params.sf_limit), }, exact, @@ -1471,7 +1475,6 @@ enum FormattedBigUintType { #[must_use] #[derive(Debug)] pub(crate) struct FormattedBigUint { - write_base_prefix: bool, base: Base, ty: FormattedBigUintType, } @@ -1490,9 +1493,8 @@ fn parse_char(ch: char) -> u8 { impl fmt::Display for FormattedBigUint { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { - if self.write_base_prefix { - self.base.write_prefix(f)?; - } + self.base.write_prefix(f)?; + match &self.ty { FormattedBigUintType::Zero => write!(f, "0")?, FormattedBigUintType::Simple(i) => write!(f, "{i}")?, From 3c07135aad608bf5b9bd812db5b2c430c6360436 Mon Sep 17 00:00:00 2001 From: Joshix Date: Sun, 19 Jul 2026 10:00:00 +0000 Subject: [PATCH 14/31] add test_format_big_uint_hex --- core/src/num/biguint.rs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/core/src/num/biguint.rs b/core/src/num/biguint.rs index 933a16c8..ea1c982b 100644 --- a/core/src/num/biguint.rs +++ b/core/src/num/biguint.rs @@ -1,6 +1,6 @@ use crate::error::{FendError, Interrupt}; use crate::format::Format; -use crate::interrupt::test_int; +use crate::interrupt::{test_int}; use crate::num::bigrat::sign::Sign; use crate::num::{Base, Exact, Range, RangeBound, out_of_range}; use crate::result::FResult; @@ -1465,6 +1465,24 @@ impl Format for BigUint { } } +#[test] +fn test_format_big_uint_hex() { + let ff = BigUint::Small(255); + let opts = FormatOptions { + base: Base::HEX, + write_base_prefix: false, + sf_limit: Some(1), + }; + + assert_eq!( + ff.format(&opts, &crate::interrupt::Never) + .expect("formatting should work") + .value + .to_string(), + "100", + ); +} + #[derive(Debug)] enum FormattedBigUintType { Zero, From cf659a5812be41d219c9fcb7d4e6dd3da44cd76a Mon Sep 17 00:00:00 2001 From: Joshix Date: Sun, 19 Jul 2026 10:00:00 +0000 Subject: [PATCH 15/31] fix ci --- core/src/num/biguint.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/num/biguint.rs b/core/src/num/biguint.rs index ea1c982b..13232663 100644 --- a/core/src/num/biguint.rs +++ b/core/src/num/biguint.rs @@ -1,6 +1,6 @@ use crate::error::{FendError, Interrupt}; use crate::format::Format; -use crate::interrupt::{test_int}; +use crate::interrupt::test_int; use crate::num::bigrat::sign::Sign; use crate::num::{Base, Exact, Range, RangeBound, out_of_range}; use crate::result::FResult; From 50433501745877858190d55323280c98a4952383 Mon Sep 17 00:00:00 2001 From: Joshix Date: Sun, 19 Jul 2026 11:00:00 +0000 Subject: [PATCH 16/31] test rounding base 9 and hex better --- core/src/num/biguint.rs | 79 +++++++++++++++++++++++++++++++---------- 1 file changed, 61 insertions(+), 18 deletions(-) diff --git a/core/src/num/biguint.rs b/core/src/num/biguint.rs index 13232663..d68e9c89 100644 --- a/core/src/num/biguint.rs +++ b/core/src/num/biguint.rs @@ -1465,24 +1465,6 @@ impl Format for BigUint { } } -#[test] -fn test_format_big_uint_hex() { - let ff = BigUint::Small(255); - let opts = FormatOptions { - base: Base::HEX, - write_base_prefix: false, - sf_limit: Some(1), - }; - - assert_eq!( - ff.format(&opts, &crate::interrupt::Never) - .expect("formatting should work") - .value - .to_string(), - "100", - ); -} - #[derive(Debug)] enum FormattedBigUintType { Zero, @@ -1596,6 +1578,67 @@ mod tests { use super::BigUint; type Res = Result<(), crate::error::FendError>; + #[test] + fn test_format_big_uint_hex() { + let opts = FormatOptions { + base: super::Base::HEX, + write_base_prefix: false, + sf_limit: Some(1), + }; + + let ff = BigUint::Small(0xff); + assert_eq!( + ff.format(&opts, &crate::interrupt::Never) + .expect("formatting should work") + .value + .to_string(), + "100", + ); + let f8 = BigUint::Small(0xf8); + assert_eq!( + f8.format(&opts, &crate::interrupt::Never) + .expect("formatting should work") + .value + .to_string(), + "100", + ); + let f7 = BigUint::Small(0xf7); + assert_eq!( + f7.format(&opts, &crate::interrupt::Never) + .expect("formatting should work") + .value + .to_string(), + "f0", + ); + } + + #[test] + fn test_format_big_uint_base9() { + let opts = FormatOptions { + base: super::Base::from_custom_base(9).unwrap(), + write_base_prefix: false, + sf_limit: Some(1), + }; + + let u44 = BigUint::Small(4 * 9 + 4); + assert_eq!( + u44.format(&opts, &crate::interrupt::Never) + .expect("formatting should work") + .value + .to_string(), + "40", + ); + + let u45 = BigUint::Small(4 * 9 + 5); + assert_eq!( + u45.format(&opts, &crate::interrupt::Never) + .expect("formatting should work") + .value + .to_string(), + "50", + ); + } + #[test] fn test_sqrt() -> Res { let two = &BigUint::from(2); From 80e79a1a683ebb28bee8c0ccc0a48f85831836bf Mon Sep 17 00:00:00 2001 From: Joshix Date: Sun, 19 Jul 2026 12:00:00 +0000 Subject: [PATCH 17/31] try to fix scientific notation with different bases --- core/src/num/base.rs | 6 ++++- core/src/num/bigrat.rs | 56 ++++++++++++++++++++++++++++++++++++++---- 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/core/src/num/base.rs b/core/src/num/base.rs index d6a2aa89..7e7a6bfd 100644 --- a/core/src/num/base.rs +++ b/core/src/num/base.rs @@ -26,6 +26,10 @@ enum BaseEnum { impl Base { pub(crate) const HEX: Self = Self(BaseEnum::Hex); + pub(crate) const fn is_plain(self) -> bool { + matches!(self.0, BaseEnum::Plain(_)) + } + pub(crate) const fn base_as_u8(self) -> u8 { match self.0 { BaseEnum::Binary => 2, @@ -62,7 +66,7 @@ impl Base { Ok(Self(BaseEnum::Custom(base))) } - pub(crate) fn write_prefix(self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + pub(crate) fn write_prefix(self, f: &mut impl fmt::Write) -> Result<(), fmt::Error> { match self.0 { BaseEnum::Binary => write!(f, "0b")?, BaseEnum::Octal => write!(f, "0o")?, diff --git a/core/src/num/bigrat.rs b/core/src/num/bigrat.rs index ea275a21..e6309113 100644 --- a/core/src/num/bigrat.rs +++ b/core/src/num/bigrat.rs @@ -7,7 +7,7 @@ use crate::num::{Base, Exact, FormattingStyle, Range, RangeBound}; use crate::result::FResult; use crate::serialize::CborValue; use core::f64; -use std::fmt::Debug; +use std::fmt::{Debug, Display, Write}; use std::{cmp, fmt, hash, ops}; pub(crate) mod sign { @@ -675,7 +675,7 @@ impl BigRat { let decimal = self.format_as_decimal( FormattingStyle::SignificantFigures(sf), - base, + Base::from_plain_base(base.base_as_u8()).expect("is valid base"), sign, term, terminating, @@ -738,14 +738,33 @@ impl BigRat { value.insert(1, decimal_separator.decimal_separator()); } + let separator = if base.is_plain() { + ScientificNotationSeparator::StaticStr(" × 10^") + } else { + let mut base_str = String::with_capacity(4); + base.write_prefix(&mut base_str)?; + base_str.write_str("10")?; + + ScientificNotationSeparator::DynamicBase(" × ", base_str.into(), "^") + }; + return Ok(Exact::new( FormattedBigRat { sign, ty: FormattedBigRatType::ScientificNotation( value.into(), - " × 10^", + separator, if positive_exponent { "" } else { "-" }, - exponent, + BigUint::Small(exponent as u64) + .format( + &biguint::FormatOptions { + base, + write_base_prefix: true, + sf_limit: None, + }, + int, + )? + .value, is_imag, ), }, @@ -1407,7 +1426,34 @@ enum FormattedBigRatType { // sign of exponent ("", "-", "+") // exponent // string (empty, "i", "pi", etc.) - ScientificNotation(Box, &'static str, &'static str, usize, &'static str), + ScientificNotation( + Box, + ScientificNotationSeparator, + &'static str, + FormattedBigUint, + &'static str, + ), +} + +#[derive(Debug)] +pub(crate) enum ScientificNotationSeparator { + /// e.g. "E" + StaticStr(&'static str), + // mult " × " + // base + // pow "^" + DynamicBase(&'static str, Box, &'static str), +} + +impl Display for ScientificNotationSeparator { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::StaticStr(value) => f.write_str(value), + Self::DynamicBase(mult, base, pow) => { + write!(f, "{mult}{base}{pow}") + } + } + } } #[must_use] From 8aba4da489f76e1615c9546b439b42230e713f68 Mon Sep 17 00:00:00 2001 From: Joshix Date: Sun, 19 Jul 2026 16:00:00 +0000 Subject: [PATCH 18/31] remove manual logic from parse_char function --- core/src/num/biguint.rs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/core/src/num/biguint.rs b/core/src/num/biguint.rs index d68e9c89..4cb68a52 100644 --- a/core/src/num/biguint.rs +++ b/core/src/num/biguint.rs @@ -1479,13 +1479,14 @@ pub(crate) struct FormattedBigUint { ty: FormattedBigUintType, } -fn parse_char(ch: char) -> u8 { - if ch.is_ascii_digit() { - ch as u8 - b'0' - } else if ch.is_ascii_lowercase() { - 10 + ch as u8 - b'a' - } else if ch.is_ascii_uppercase() { - 10 + ch as u8 - b'A' +#[allow(clippy::cast_possible_truncation)] +fn parse_char(ch: char, base: Base) -> u8 { + if let Some(digit) = ch.to_digit(base.base_as_u8().into()) { + let byte = digit as u8; + + debug_assert_eq!(u32::from(byte), digit); + + byte } else { unreachable!("{ch} needs to be a digit"); } @@ -1509,8 +1510,8 @@ impl fmt::Display for FormattedBigUint { let mut chars = chars.peekable(); if let Some(last_non_zero_char) = chars.next() { - let mut last_digit: u8 = parse_char(last_non_zero_char); - let after_digit = chars.peek().map_or(0u8, |ch| parse_char(*ch)); + let mut last_digit: u8 = parse_char(last_non_zero_char, self.base); + let after_digit = chars.peek().map_or(0u8, |ch| parse_char(*ch, self.base)); let base = self.base.base_as_u8(); From be7a6d9947e9e5611dacf9b0e320cee42493865c Mon Sep 17 00:00:00 2001 From: Joshix Date: Sun, 19 Jul 2026 19:00:00 +0000 Subject: [PATCH 19/31] fix some bugs --- core/src/num/bigrat.rs | 17 ++++++++------ core/tests/integration_tests.rs | 40 +++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/core/src/num/bigrat.rs b/core/src/num/bigrat.rs index e6309113..fb6ce810 100644 --- a/core/src/num/bigrat.rs +++ b/core/src/num/bigrat.rs @@ -669,8 +669,6 @@ impl BigRat { if !self.is_definitely_zero() && let FormattingStyle::ScientificNotation(sf) = style { - let num_digits_of_int_part = formatted_integer_part.value.num_digits(); - let positive_exponent = !integer_part.is_definitely_zero(); let decimal = self.format_as_decimal( @@ -699,15 +697,20 @@ impl BigRat { let (mut value, exponent): (String, usize) = if positive_exponent { let mut string = decimal.to_string(); - if let Some(idx) = string.find(decimal_separator.decimal_separator()) { - string.remove(idx); - } - while string.ends_with('i') { string.remove(string.len() - 1); } - (string, num_digits_of_int_part - 1) + let exponent: usize; + + if let Some(idx) = string.find(decimal_separator.decimal_separator()) { + string.remove(idx); + exponent = idx - 1; + } else { + exponent = string.len() - 1; + } + + (string, exponent) } else { let string = decimal.to_string(); diff --git a/core/tests/integration_tests.rs b/core/tests/integration_tests.rs index b84bd48c..3be72b0e 100644 --- a/core/tests/integration_tests.rs +++ b/core/tests/integration_tests.rs @@ -6318,4 +6318,44 @@ fn test_scientific_notation_edge_cases() { .get_main_result(), "approx. 6.7 × 10^2" ); + + let mut context = Context::new(); + assert_eq!( + evaluate("199 to 2 sn", &mut context) + .unwrap() + .get_main_result(), + "approx. 2.0 × 10^2" + ); + + let mut context = Context::new(); + assert_eq!( + evaluate("999 to 1 sn", &mut context) + .unwrap() + .get_main_result(), + "approx. 1 × 10^3" + ); + + let mut context = Context::new(); + assert_eq!( + evaluate("999.9 to 1 sn", &mut context) + .unwrap() + .get_main_result(), + "approx. 1 × 10^3" + ); + + let mut context = Context::new(); + assert_eq!( + evaluate("999 to 2 sn", &mut context) + .unwrap() + .get_main_result(), + "approx. 1.0 × 10^3" + ); + + let mut context = Context::new(); + assert_eq!( + evaluate("999.9 to 2 sn", &mut context) + .unwrap() + .get_main_result(), + "approx. 1.0 × 10^3" + ); } From bca4e3a30135a7a374fc9e376977504362314b8d Mon Sep 17 00:00:00 2001 From: Joshix Date: Sun, 19 Jul 2026 19:00:00 +0000 Subject: [PATCH 20/31] add parentheses --- core/src/num/bigrat.rs | 6 +++++- core/tests/integration_tests.rs | 10 +++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/core/src/num/bigrat.rs b/core/src/num/bigrat.rs index fb6ce810..e88e7f72 100644 --- a/core/src/num/bigrat.rs +++ b/core/src/num/bigrat.rs @@ -1518,7 +1518,11 @@ impl fmt::Display for FormattedBigRat { write!(f, "{term}")?; } FormattedBigRatType::ScientificNotation(m, separator, sign, exponent, imag) => { - write!(f, "{m}{separator}{sign}{exponent}{imag}")?; + if imag.is_empty() { + write!(f, "{m}{separator}{sign}{exponent}")?; + } else { + write!(f, "({m}{separator}{sign}{exponent}){imag}")?; + } } } Ok(()) diff --git a/core/tests/integration_tests.rs b/core/tests/integration_tests.rs index 3be72b0e..838b56a5 100644 --- a/core/tests/integration_tests.rs +++ b/core/tests/integration_tests.rs @@ -6292,7 +6292,15 @@ fn test_scientific_notation_edge_cases() { evaluate("5i to 3 sn", &mut context) .unwrap() .get_main_result(), - "5.00 × 10^0i" + "(5.00 × 10^0)i" + ); + + let mut context = Context::new(); + assert_eq!( + evaluate("2 + pi i to 4 sn", &mut context) + .unwrap() + .get_main_result(), + "approx. 2.000 × 10^0 + (3.142 × 10^0)i" ); let mut context = Context::new(); From 16effe1ee683d0b175635babab0563f22e791403 Mon Sep 17 00:00:00 2001 From: Joshix Date: Sun, 19 Jul 2026 19:00:00 +0000 Subject: [PATCH 21/31] fix bug with rounding 0.99 to 1 --- core/src/num/bigrat.rs | 8 ++++---- core/tests/integration_tests.rs | 8 ++++++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/core/src/num/bigrat.rs b/core/src/num/bigrat.rs index e88e7f72..6bf8b289 100644 --- a/core/src/num/bigrat.rs +++ b/core/src/num/bigrat.rs @@ -694,9 +694,11 @@ impl BigRat { unreachable!() }; - let (mut value, exponent): (String, usize) = if positive_exponent { - let mut string = decimal.to_string(); + let mut string = decimal.to_string(); + + let positive_exponent = positive_exponent || !string.starts_with('0'); + let (mut value, exponent): (String, usize) = if positive_exponent { while string.ends_with('i') { string.remove(string.len() - 1); } @@ -712,8 +714,6 @@ impl BigRat { (string, exponent) } else { - let string = decimal.to_string(); - let trimmed_string: String = string .trim_start_matches(|ch| { ch == decimal_separator.decimal_separator() || ch == '0' diff --git a/core/tests/integration_tests.rs b/core/tests/integration_tests.rs index 838b56a5..0e619aaf 100644 --- a/core/tests/integration_tests.rs +++ b/core/tests/integration_tests.rs @@ -6366,4 +6366,12 @@ fn test_scientific_notation_edge_cases() { .get_main_result(), "approx. 1.0 × 10^3" ); + + let mut context = Context::new(); + assert_eq!( + evaluate("0.9999 to 2 sn", &mut context) + .unwrap() + .get_main_result(), + "approx. 1.0 × 10^0" + ); } From 3391c848ddeb27a3aed0acc8daa67f1301716076 Mon Sep 17 00:00:00 2001 From: Joshix Date: Sun, 19 Jul 2026 19:00:00 +0000 Subject: [PATCH 22/31] more tests --- core/tests/integration_tests.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/tests/integration_tests.rs b/core/tests/integration_tests.rs index 0e619aaf..60b0afd8 100644 --- a/core/tests/integration_tests.rs +++ b/core/tests/integration_tests.rs @@ -6374,4 +6374,12 @@ fn test_scientific_notation_edge_cases() { .get_main_result(), "approx. 1.0 × 10^0" ); + + let mut context = Context::new(); + assert_eq!( + evaluate("0.9995 + 0.99992345i to 3 sn", &mut context) + .unwrap() + .get_main_result(), + "approx. 1.00 × 10^0 + (1.00 × 10^0)i" + ); } From 95d5032392d3cba2c1e375b363ed67001bfc3514 Mon Sep 17 00:00:00 2001 From: Joshix Date: Sun, 19 Jul 2026 19:00:00 +0000 Subject: [PATCH 23/31] add carry logic --- core/src/num/base.rs | 8 +++++ core/src/num/biguint.rs | 62 +++++++++++++++++++-------------- core/tests/integration_tests.rs | 5 +++ 3 files changed, 48 insertions(+), 27 deletions(-) diff --git a/core/src/num/base.rs b/core/src/num/base.rs index d6a2aa89..1635d80f 100644 --- a/core/src/num/base.rs +++ b/core/src/num/base.rs @@ -35,6 +35,14 @@ impl Base { } } + pub(crate) const fn max_value(self) -> u8 { + self.base_as_u8() - 1 + } + + pub(crate) const fn max_char(self) -> char { + Self::digit_as_char(self.max_value() as _).expect("Max value is valid") + } + pub(crate) const fn from_zero_based_prefix_char(ch: char) -> FResult { Ok(match ch { 'x' => Self(BaseEnum::Hex), diff --git a/core/src/num/biguint.rs b/core/src/num/biguint.rs index 4cb68a52..dde3e570 100644 --- a/core/src/num/biguint.rs +++ b/core/src/num/biguint.rs @@ -1500,47 +1500,55 @@ impl fmt::Display for FormattedBigUint { FormattedBigUintType::Zero => write!(f, "0")?, FormattedBigUintType::Simple(i) => write!(f, "{i}")?, FormattedBigUintType::Complex(s, sf_limit) => { - let mut chars = s.chars().rev(); + debug_assert!(s.is_ascii()); + debug_assert_eq!(s.len(), s.chars().count()); - if let Some(sf_limit) = sf_limit { - for char in (&mut chars).take(sf_limit - 1) { - write!(f, "{char}")?; - } + if let Some(sf_limit) = sf_limit + && s.len() > *sf_limit + { + let s = s.as_bytes(); - let mut chars = chars.peekable(); + let after_last_char = + parse_char(char::from(s[s.len() - 1 - *sf_limit]), self.base); - if let Some(last_non_zero_char) = chars.next() { - let mut last_digit: u8 = parse_char(last_non_zero_char, self.base); - let after_digit = chars.peek().map_or(0u8, |ch| parse_char(*ch, self.base)); + let round_up = after_last_char >= self.base.base_as_u8().div_ceil(2); - let base = self.base.base_as_u8(); + let mut zeros_count = s.len() - sf_limit; - debug_assert!(last_digit < base); - debug_assert!(after_digit < base); + if round_up { + let max: u8 = self.base.max_char().try_into().expect("is ascii"); - if after_digit >= base.div_ceil(2) { - last_digit += 1; - } + let number = &s[s.len() - sf_limit..]; + + let trailing_max_count = number.iter().take_while(|p| **p == max).count(); - if last_digit == base { - write!(f, "10")?; + zeros_count += trailing_max_count; + + if trailing_max_count == *sf_limit { + write!(f, "1")?; } else { - debug_assert!(last_digit < base); - write!( - f, - "{}", - Base::digit_as_char(last_digit.into()) - .expect("needs to be valid char") - )?; + let mut chars = number.iter().rev(); + for ch in (&mut chars).take(sf_limit - trailing_max_count - 1) { + write!(f, "{}", char::from(*ch))?; + } + let mut num = parse_char(char::from(*chars.next().unwrap()), self.base); + debug_assert!(num < max); + num += 1; + write!(f, "{}", Base::digit_as_char(num.into()).unwrap())?; + } + } else { + // truncate + for ch in s.iter().rev().take(*sf_limit) { + write!(f, "{}", char::from(*ch))?; } } - for _ in chars { + for _ in 0..zeros_count { write!(f, "0")?; } } else { - for char in chars { - write!(f, "{char}")?; + for ch in s.as_bytes().iter().rev() { + write!(f, "{}", char::from(*ch))?; } } } diff --git a/core/tests/integration_tests.rs b/core/tests/integration_tests.rs index 62b477ce..2521ea1a 100644 --- a/core/tests/integration_tests.rs +++ b/core/tests/integration_tests.rs @@ -4232,6 +4232,9 @@ fn sf_1() { #[test] fn sf_2() { test_eval("1234567.55645 to 2 sf", "approx. 1200000"); + test_eval("99.9 to 2 sf", "approx. 100"); + test_eval("199 to 2 sf", "approx. 200"); + test_eval("999 to 2 sf", "approx. 1000"); } #[test] @@ -4292,11 +4295,13 @@ fn sf_13() { #[test] fn sf_hex_1() { test_eval("0xff to 1 sf", "approx. 0x100"); + test_eval("0xfff to 1 sf", "approx. 0x1000"); } #[test] fn sf_hex_2() { test_eval("0xff to 2 sf", "0xff"); + test_eval("0xfff to 2 sf", "approx. 0x1000"); } #[test] From 07680a1eb56a47db66acf59a2c5ffa13cc1edc33 Mon Sep 17 00:00:00 2001 From: Joshix Date: Sun, 19 Jul 2026 20:00:00 +0000 Subject: [PATCH 24/31] fix imaginary bug --- core/src/num/bigrat.rs | 1 + core/tests/integration_tests.rs | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/core/src/num/bigrat.rs b/core/src/num/bigrat.rs index 6bf8b289..d6034ec7 100644 --- a/core/src/num/bigrat.rs +++ b/core/src/num/bigrat.rs @@ -714,6 +714,7 @@ impl BigRat { (string, exponent) } else { + let string = string.trim_end_matches('i'); let trimmed_string: String = string .trim_start_matches(|ch| { ch == decimal_separator.decimal_separator() || ch == '0' diff --git a/core/tests/integration_tests.rs b/core/tests/integration_tests.rs index 60b0afd8..49bde2f9 100644 --- a/core/tests/integration_tests.rs +++ b/core/tests/integration_tests.rs @@ -6382,4 +6382,12 @@ fn test_scientific_notation_edge_cases() { .get_main_result(), "approx. 1.00 × 10^0 + (1.00 × 10^0)i" ); + + let mut context = Context::new(); + assert_eq!( + evaluate("0.5i to 4 sn", &mut context) + .unwrap() + .get_main_result(), + "(5.000 × 10^-1)i" + ); } From 4589cf4728f5a18414ed38dc3338513130ec5efc Mon Sep 17 00:00:00 2001 From: Joshix Date: Sun, 19 Jul 2026 20:00:00 +0000 Subject: [PATCH 25/31] add base prefix --- core/src/num/bigrat.rs | 6 +++++- core/tests/integration_tests.rs | 8 ++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/core/src/num/bigrat.rs b/core/src/num/bigrat.rs index d6034ec7..bbb6917f 100644 --- a/core/src/num/bigrat.rs +++ b/core/src/num/bigrat.rs @@ -756,6 +756,7 @@ impl BigRat { FormattedBigRat { sign, ty: FormattedBigRatType::ScientificNotation( + base, value.into(), separator, if positive_exponent { "" } else { "-" }, @@ -1431,6 +1432,7 @@ enum FormattedBigRatType { // exponent // string (empty, "i", "pi", etc.) ScientificNotation( + Base, Box, ScientificNotationSeparator, &'static str, @@ -1518,7 +1520,9 @@ impl fmt::Display for FormattedBigRat { } write!(f, "{term}")?; } - FormattedBigRatType::ScientificNotation(m, separator, sign, exponent, imag) => { + FormattedBigRatType::ScientificNotation(base, m, separator, sign, exponent, imag) => { + base.write_prefix(f)?; + if imag.is_empty() { write!(f, "{m}{separator}{sign}{exponent}")?; } else { diff --git a/core/tests/integration_tests.rs b/core/tests/integration_tests.rs index 49bde2f9..c7fed40a 100644 --- a/core/tests/integration_tests.rs +++ b/core/tests/integration_tests.rs @@ -6390,4 +6390,12 @@ fn test_scientific_notation_edge_cases() { .get_main_result(), "(5.000 × 10^-1)i" ); + + let mut context = Context::new(); + assert_eq!( + evaluate("0b1011 to 2 sn", &mut context) + .unwrap() + .get_main_result(), + "approx. 0b1.1 × 0b10^0b11" + ); } From 4158002d7f2ce32251e7f83eb761a868cc0e59f5 Mon Sep 17 00:00:00 2001 From: Joshix Date: Sun, 19 Jul 2026 20:00:00 +0000 Subject: [PATCH 26/31] fix bug with bases where i is a digit --- core/src/num/bigrat.rs | 9 ++++----- core/tests/integration_tests.rs | 8 ++++++++ 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/core/src/num/bigrat.rs b/core/src/num/bigrat.rs index bbb6917f..5642310b 100644 --- a/core/src/num/bigrat.rs +++ b/core/src/num/bigrat.rs @@ -696,13 +696,13 @@ impl BigRat { let mut string = decimal.to_string(); + if string.ends_with(is_imag) { + string.truncate(string.len() - is_imag.len()); + } + let positive_exponent = positive_exponent || !string.starts_with('0'); let (mut value, exponent): (String, usize) = if positive_exponent { - while string.ends_with('i') { - string.remove(string.len() - 1); - } - let exponent: usize; if let Some(idx) = string.find(decimal_separator.decimal_separator()) { @@ -714,7 +714,6 @@ impl BigRat { (string, exponent) } else { - let string = string.trim_end_matches('i'); let trimmed_string: String = string .trim_start_matches(|ch| { ch == decimal_separator.decimal_separator() || ch == '0' diff --git a/core/tests/integration_tests.rs b/core/tests/integration_tests.rs index c7fed40a..52d314c6 100644 --- a/core/tests/integration_tests.rs +++ b/core/tests/integration_tests.rs @@ -6398,4 +6398,12 @@ fn test_scientific_notation_edge_cases() { .get_main_result(), "approx. 0b1.1 × 0b10^0b11" ); + + let mut context = Context::new(); + assert_eq!( + evaluate("36#i to 1sn", &mut context) + .unwrap() + .get_main_result(), + "36#i × 36#10^36#0" + ); } From f75bca89babbcb072a6aee7656c72a0ce4c9539d Mon Sep 17 00:00:00 2001 From: Sami Farin Date: Sun, 19 Jul 2026 12:44:57 +0300 Subject: [PATCH 27/31] add tests from 390 pr --- core/tests/integration_tests.rs | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/core/tests/integration_tests.rs b/core/tests/integration_tests.rs index 2521ea1a..d859a74c 100644 --- a/core/tests/integration_tests.rs +++ b/core/tests/integration_tests.rs @@ -1,4 +1,4 @@ -use fend_core::{Context, evaluate}; +use fend_core::{evaluate, Context}; #[track_caller] fn test_serialization_roundtrip(context: &mut Context) { @@ -125,12 +125,10 @@ fn two_pi() { #[test] fn pi_to_fraction() { let mut ctx = Context::new(); - assert!( - evaluate("pi to fraction", &mut ctx) - .unwrap() - .get_main_result() - .starts_with("approx.") - ); + assert!(evaluate("pi to fraction", &mut ctx) + .unwrap() + .get_main_result() + .starts_with("approx.")); } const DIVISION_BY_ZERO_ERROR: &str = "division by zero"; @@ -4314,6 +4312,26 @@ fn sf_small_2() { test_eval("pi / 1000000 to 2 sf", "approx. 0.0000031"); } +#[test] +fn sf_hex_17f_1() { + test_eval("0x17f to 1 sf", "approx. 0x100"); +} + +#[test] +fn sf_hex_17f_2() { + test_eval("0x17f to 2 sf", "approx. 0x180"); +} + +#[test] +fn sf_hex_ff_1() { + test_eval("0xff to 1 sf", "approx. 0x100"); +} + +#[test] +fn sf_hex_ff_2() { + test_eval("0xff to 2 sf", "0xff"); +} + #[test] fn sf_rounding_integer_carry() { test_eval("123.9 to 3 sf", "approx. 124"); From 8518c2e1a29a41379f2daf602028540621d448fc Mon Sep 17 00:00:00 2001 From: Joshix Date: Mon, 20 Jul 2026 16:00:00 +0000 Subject: [PATCH 28/31] more test cases --- core/tests/integration_tests.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/core/tests/integration_tests.rs b/core/tests/integration_tests.rs index d859a74c..dafbc0be 100644 --- a/core/tests/integration_tests.rs +++ b/core/tests/integration_tests.rs @@ -4450,51 +4450,62 @@ fn million_pi_10_sf() { #[test] fn large_integer_to_1_sf() { test_eval("1234567 to 1 sf", "approx. 1000000"); + test_eval("999999 to 1 sf", "approx. 1000000"); } #[test] fn large_integer_to_2_sf() { test_eval("1234567 to 2 sf", "approx. 1200000"); + test_eval("999999 to 2 sf", "approx. 1000000"); } #[test] fn large_integer_to_3_sf() { test_eval("1234567 to 3 sf", "approx. 1230000"); + test_eval("999999 to 3 sf", "approx. 1000000"); } #[test] fn large_integer_to_4_sf() { test_eval("1234567 to 4 sf", "approx. 1235000"); + test_eval("999999 to 4 sf", "approx. 1000000"); } #[test] fn large_integer_to_5_sf() { test_eval("1234567 to 5 sf", "approx. 1234600"); + test_eval("999999 to 5 sf", "approx. 1000000"); } #[test] fn large_integer_to_6_sf() { test_eval("1234567 to 6 sf", "approx. 1234570"); + test_eval("999999 to 6 sf", "999999"); } #[test] fn large_integer_to_7_sf() { test_eval("1234567 to 7 sf", "1234567"); + test_eval("999999 to 7 sf", "999999"); + test_eval("9999999 to 7 sf", "9999999"); } #[test] fn large_integer_to_8_sf() { test_eval("1234567 to 8 sf", "1234567"); + test_eval("9999999 to 8 sf", "9999999"); } #[test] fn large_integer_to_9_sf() { test_eval("1234567 to 9 sf", "1234567"); + test_eval("9999999 to 9 sf", "9999999"); } #[test] fn large_integer_to_10_sf() { test_eval("1234567 to 10 sf", "1234567"); + test_eval("9999999 to 10 sf", "9999999"); } #[test] From c46b72eee710dd3f1d9ac0d6e57d12b96e6ee6a1 Mon Sep 17 00:00:00 2001 From: Joshix Date: Mon, 20 Jul 2026 16:00:00 +0000 Subject: [PATCH 29/31] more test cases --- core/tests/integration_tests.rs | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/core/tests/integration_tests.rs b/core/tests/integration_tests.rs index dafbc0be..b93e6242 100644 --- a/core/tests/integration_tests.rs +++ b/core/tests/integration_tests.rs @@ -1,4 +1,4 @@ -use fend_core::{evaluate, Context}; +use fend_core::{Context, evaluate}; #[track_caller] fn test_serialization_roundtrip(context: &mut Context) { @@ -125,10 +125,12 @@ fn two_pi() { #[test] fn pi_to_fraction() { let mut ctx = Context::new(); - assert!(evaluate("pi to fraction", &mut ctx) - .unwrap() - .get_main_result() - .starts_with("approx.")); + assert!( + evaluate("pi to fraction", &mut ctx) + .unwrap() + .get_main_result() + .starts_with("approx.") + ); } const DIVISION_BY_ZERO_ERROR: &str = "division by zero"; @@ -4450,42 +4452,49 @@ fn million_pi_10_sf() { #[test] fn large_integer_to_1_sf() { test_eval("1234567 to 1 sf", "approx. 1000000"); + test_eval("8999999 to 1 sf", "approx. 9000000"); test_eval("999999 to 1 sf", "approx. 1000000"); } #[test] fn large_integer_to_2_sf() { test_eval("1234567 to 2 sf", "approx. 1200000"); + test_eval("8999999 to 2 sf", "approx. 9000000"); test_eval("999999 to 2 sf", "approx. 1000000"); } #[test] fn large_integer_to_3_sf() { test_eval("1234567 to 3 sf", "approx. 1230000"); + test_eval("8999999 to 3 sf", "approx. 9000000"); test_eval("999999 to 3 sf", "approx. 1000000"); } #[test] fn large_integer_to_4_sf() { test_eval("1234567 to 4 sf", "approx. 1235000"); + test_eval("8999999 to 4 sf", "approx. 9000000"); test_eval("999999 to 4 sf", "approx. 1000000"); } #[test] fn large_integer_to_5_sf() { test_eval("1234567 to 5 sf", "approx. 1234600"); + test_eval("8999999 to 5 sf", "approx. 9000000"); test_eval("999999 to 5 sf", "approx. 1000000"); } #[test] fn large_integer_to_6_sf() { test_eval("1234567 to 6 sf", "approx. 1234570"); + test_eval("8999999 to 6 sf", "approx. 9000000"); test_eval("999999 to 6 sf", "999999"); } #[test] fn large_integer_to_7_sf() { test_eval("1234567 to 7 sf", "1234567"); + test_eval("8999999 to 7 sf", "8999999"); test_eval("999999 to 7 sf", "999999"); test_eval("9999999 to 7 sf", "9999999"); } @@ -4493,18 +4502,21 @@ fn large_integer_to_7_sf() { #[test] fn large_integer_to_8_sf() { test_eval("1234567 to 8 sf", "1234567"); + test_eval("8999999 to 8 sf", "8999999"); test_eval("9999999 to 8 sf", "9999999"); } #[test] fn large_integer_to_9_sf() { test_eval("1234567 to 9 sf", "1234567"); + test_eval("8999999 to 9 sf", "8999999"); test_eval("9999999 to 9 sf", "9999999"); } #[test] fn large_integer_to_10_sf() { test_eval("1234567 to 10 sf", "1234567"); + test_eval("8999999 to 10 sf", "8999999"); test_eval("9999999 to 10 sf", "9999999"); } @@ -4543,6 +4555,16 @@ fn trailing_zeroes_sf_7() { test_eval("12345601 to 8sf", "12345601"); } +#[test] +fn trailing_zeroes_sf_8() { + test_eval("1234599990 to 9sf", "1234599990"); + test_eval("1234599990 to 8sf", "approx. 1234600000"); + test_eval("1234599990 to 7sf", "approx. 1234600000"); + test_eval("1234599990 to 6sf", "approx. 1234600000"); + test_eval("1234599990 to 5sf", "approx. 1234600000"); + test_eval("1234599990 to 4sf", "approx. 1235000000"); +} + #[test] fn kwh_conversion() { test_eval("100 kWh/yr to watt", "approx. 11.4079552707 watts"); From cafbde9815b1f5682171c7893c6c963b7a7a022f Mon Sep 17 00:00:00 2001 From: Joshix Date: Mon, 20 Jul 2026 18:00:00 +0000 Subject: [PATCH 30/31] add failing test --- core/src/num/base.rs | 4 + core/src/num/bigrat.rs | 146 ++++++++++++++++++++++++++++++++ core/tests/integration_tests.rs | 8 ++ 3 files changed, 158 insertions(+) diff --git a/core/src/num/base.rs b/core/src/num/base.rs index 97fd77c6..c023b5ed 100644 --- a/core/src/num/base.rs +++ b/core/src/num/base.rs @@ -25,6 +25,10 @@ enum BaseEnum { impl Base { pub(crate) const HEX: Self = Self(BaseEnum::Hex); + #[cfg(test)] + pub(crate) const OCT: Self = Self(BaseEnum::Octal); + #[cfg(test)] + pub(crate) const BIN: Self = Self(BaseEnum::Binary); pub(crate) const fn is_plain(self) -> bool { matches!(self.0, BaseEnum::Plain(_)) diff --git a/core/src/num/bigrat.rs b/core/src/num/bigrat.rs index 5642310b..05e89145 100644 --- a/core/src/num/bigrat.rs +++ b/core/src/num/bigrat.rs @@ -671,6 +671,7 @@ impl BigRat { { let positive_exponent = !integer_part.is_definitely_zero(); + // println!("-- starting recursive call --"); let decimal = self.format_as_decimal( FormattingStyle::SignificantFigures(sf), Base::from_plain_base(base.base_as_u8()).expect("is valid base"), @@ -680,6 +681,9 @@ impl BigRat { decimal_separator, int, )?; + // println!("-- ending recursive call --"); + + // println!("[{self:?}] [{decimal:?}]"); let exact: bool = formatted_integer_part.exact && decimal.exact; @@ -729,6 +733,8 @@ impl BigRat { (trimmed_string, zeros) }; + // println!("[{self:?}] {:?}", (&value, exponent)); + if value.len() > sf { value.truncate(sf); } else { @@ -1538,8 +1544,12 @@ mod tests { use super::BigRat; use super::sign::Sign; + use crate::interrupt::Never; use crate::num::biguint::BigUint; + use crate::num::{Base, FormattingStyle}; use crate::result::FResult; + use crate::{Context, DecimalSeparatorStyle, evaluate}; + use std::fmt::Write as _; use std::mem; #[test] @@ -1583,4 +1593,140 @@ mod tests { } ); } + + #[test] + fn test_binary_format_as_decimal() { + let rat = BigRat { + sign: Sign::Negative, + num: BigUint::Small(1), + den: BigUint::Small(1), + }; + let result = rat + .format_as_decimal( + FormattingStyle::SignificantFigures(1), + Base::BIN, + Sign::Negative, + "", + || Ok(false), + DecimalSeparatorStyle::Comma, + &Never, + ) + .unwrap(); + + assert_eq!(result.value.to_string(), "-0b1"); + + let result = rat + .format_as_decimal( + FormattingStyle::ScientificNotation(1), + Base::BIN, + Sign::Negative, + "", + || Ok(false), + DecimalSeparatorStyle::Comma, + &Never, + ) + .unwrap(); + assert_eq!( + result.value.to_string(), + "-0b1 × 0b10^0b0", + "AAH: {rat:?} {result:?} " + ); + } + + #[test] + fn test_formatting_reversible() { + for decimal_separator in [DecimalSeparatorStyle::Comma, DecimalSeparatorStyle::Dot] { + for sign in [Sign::Negative, Sign::Positive] { + for num in 0..100 { + for den in [ + 1, 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, + 71, + ] { + if den != 1 && num % den == 0 { + continue; + } + + let rat = BigRat { + sign, + num: BigUint::Small(num), + den: BigUint::Small(den), + }; + for base in [10, 2, 3, 4, 8, 16, 33, 36] { + let base = match base { + 2 => Base::BIN, + 8 => Base::OCT, + 16 => Base::HEX, + 10 => Base::from_plain_base(10).unwrap(), + _ => Base::from_custom_base(base).unwrap(), + }; + + for sf in [1, 2, 3, 4, 10] { + // println!("==== GOT NEW NUMBER {rat:?} ===="); + + let style = FormattingStyle::ScientificNotation(sf); + + let result = rat.format_as_decimal( + style, + base, + sign, + "", + || Ok(false), + decimal_separator, + &Never, + ); + + let value = result.unwrap().value; + + // println!("[{rat:?}]: {value} ---"); + + if num != 0 { + assert!(value.to_string().contains(" × ")); + assert!(value.to_string().contains("10^")); + } + + let mut context = Context { + decimal_separator, + output_mode: crate::OutputMode::SimpleText, + ..Default::default() + }; + let result = + evaluate(&format!("{value} to {sf} sn"), &mut context).unwrap(); + assert_eq!(result.plain_result, value.to_string()); + + let mut manual_division = String::new(); + if sign == Sign::Negative { + manual_division.push('-'); + } + base.write_prefix(&mut manual_division).unwrap(); + write!(&mut manual_division, "{num} / ").unwrap(); + base.write_prefix(&mut manual_division).unwrap(); + write!(&mut manual_division, "{den}").unwrap(); + + let result2 = evaluate( + &format!("{manual_division} to {sf} sn"), + &mut context, + ) + .unwrap(); + + assert_eq!( + result2.plain_result, + value.to_string(), + "{manual_division} != {value} ({rat:?})" + ); + assert_eq!(result.get_main_result(), result2.get_main_result()); + + if den == 1 { + let result_base10 = + evaluate(&format!("{value} to base 10"), &mut context) + .unwrap(); + + assert_eq!(result_base10.plain_result, num.to_string()); + } + } + } + } + } + } + } + } } diff --git a/core/tests/integration_tests.rs b/core/tests/integration_tests.rs index 92067c2c..3c98af27 100644 --- a/core/tests/integration_tests.rs +++ b/core/tests/integration_tests.rs @@ -6457,4 +6457,12 @@ fn test_scientific_notation_edge_cases() { .get_main_result(), "36#i × 36#10^36#0" ); + + let mut context = Context::new(); + assert_eq!( + evaluate("36#i to 1sn", &mut context) + .unwrap() + .get_main_result(), + "36#i × 36#10^36#0" + ); } From 47fb69637334b34e17a3ec678281812aa343e40b Mon Sep 17 00:00:00 2001 From: Joshix Date: Mon, 20 Jul 2026 19:00:00 +0000 Subject: [PATCH 31/31] fix tests --- core/src/num/bigrat.rs | 149 ++++++++++++++++++++--------------------- 1 file changed, 72 insertions(+), 77 deletions(-) diff --git a/core/src/num/bigrat.rs b/core/src/num/bigrat.rs index 05e89145..bbb4f1ad 100644 --- a/core/src/num/bigrat.rs +++ b/core/src/num/bigrat.rs @@ -671,19 +671,20 @@ impl BigRat { { let positive_exponent = !integer_part.is_definitely_zero(); - // println!("-- starting recursive call --"); - let decimal = self.format_as_decimal( - FormattingStyle::SignificantFigures(sf), - Base::from_plain_base(base.base_as_u8()).expect("is valid base"), - sign, - term, - terminating, - decimal_separator, - int, - )?; - // println!("-- ending recursive call --"); - - // println!("[{self:?}] [{decimal:?}]"); + let silent_base = Base::from_plain_base(base.base_as_u8()).expect("is valid base"); + let decimal = if self.is_integer() { + Self::format_as_integer(&self.num, silent_base, sign, term, false, Some(sf), int)? + } else { + self.format_as_decimal( + FormattingStyle::SignificantFigures(sf), + silent_base, + sign, + term, + terminating, + decimal_separator, + int, + )? + }; let exact: bool = formatted_integer_part.exact && decimal.exact; @@ -733,8 +734,6 @@ impl BigRat { (trimmed_string, zeros) }; - // println!("[{self:?}] {:?}", (&value, exponent)); - if value.len() > sf { value.truncate(sf); } else { @@ -1544,12 +1543,13 @@ mod tests { use super::BigRat; use super::sign::Sign; + use crate::format::Format as _; use crate::interrupt::Never; + use crate::num::bigrat::FormatOptions; use crate::num::biguint::BigUint; use crate::num::{Base, FormattingStyle}; use crate::result::FResult; use crate::{Context, DecimalSeparatorStyle, evaluate}; - use std::fmt::Write as _; use std::mem; #[test] @@ -1602,13 +1602,14 @@ mod tests { den: BigUint::Small(1), }; let result = rat - .format_as_decimal( - FormattingStyle::SignificantFigures(1), - Base::BIN, - Sign::Negative, - "", - || Ok(false), - DecimalSeparatorStyle::Comma, + .format( + &FormatOptions { + base: Base::BIN, + style: FormattingStyle::SignificantFigures(1), + term: "", + use_parens_if_fraction: false, + decimal_separator: DecimalSeparatorStyle::Dot, + }, &Never, ) .unwrap(); @@ -1616,31 +1617,27 @@ mod tests { assert_eq!(result.value.to_string(), "-0b1"); let result = rat - .format_as_decimal( - FormattingStyle::ScientificNotation(1), - Base::BIN, - Sign::Negative, - "", - || Ok(false), - DecimalSeparatorStyle::Comma, + .format( + &FormatOptions { + base: Base::BIN, + style: FormattingStyle::ScientificNotation(1), + term: "", + use_parens_if_fraction: false, + decimal_separator: DecimalSeparatorStyle::Dot, + }, &Never, ) .unwrap(); - assert_eq!( - result.value.to_string(), - "-0b1 × 0b10^0b0", - "AAH: {rat:?} {result:?} " - ); + assert_eq!(result.value.to_string(), "-0b1 × 0b10^0b0"); } #[test] - fn test_formatting_reversible() { + fn test_scientific_formatting_reversible() { for decimal_separator in [DecimalSeparatorStyle::Comma, DecimalSeparatorStyle::Dot] { for sign in [Sign::Negative, Sign::Positive] { - for num in 0..100 { + for num in (1..10).into_iter().chain(100..110) { for den in [ 1, 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, - 71, ] { if den != 1 && num % den == 0 { continue; @@ -1651,6 +1648,9 @@ mod tests { num: BigUint::Small(num), den: BigUint::Small(den), }; + assert_eq!(rat.clone().simplify(&Never).unwrap().num, rat.num); + assert_eq!(rat.clone().simplify(&Never).unwrap().den, rat.den); + for base in [10, 2, 3, 4, 8, 16, 33, 36] { let base = match base { 2 => Base::BIN, @@ -1661,28 +1661,21 @@ mod tests { }; for sf in [1, 2, 3, 4, 10] { - // println!("==== GOT NEW NUMBER {rat:?} ===="); - - let style = FormattingStyle::ScientificNotation(sf); - - let result = rat.format_as_decimal( - style, - base, - sign, - "", - || Ok(false), - decimal_separator, + let result = rat.format( + &FormatOptions { + base, + style: FormattingStyle::ScientificNotation(sf), + term: "", + use_parens_if_fraction: false, + decimal_separator, + }, &Never, ); let value = result.unwrap().value; - // println!("[{rat:?}]: {value} ---"); - - if num != 0 { - assert!(value.to_string().contains(" × ")); - assert!(value.to_string().contains("10^")); - } + assert!(value.to_string().contains(" × ")); + assert!(value.to_string().contains("10^")); let mut context = Context { decimal_separator, @@ -1693,34 +1686,36 @@ mod tests { evaluate(&format!("{value} to {sf} sn"), &mut context).unwrap(); assert_eq!(result.plain_result, value.to_string()); - let mut manual_division = String::new(); - if sign == Sign::Negative { - manual_division.push('-'); + if base.base_as_u8() == 10 { + let sign_str = if sign == Sign::Negative { "-" } else { "" }; + let manual_division = + format!("({sign_str}{num} / {den} to {sf} sn"); + let result2 = evaluate(&manual_division, &mut context).unwrap(); + + assert_eq!( + result2 + .plain_result + .strip_prefix("approx. ") + .unwrap_or(result2.plain_result.as_str()), + value.to_string(), + "{manual_division} != {value} ({rat:?})" + ); + assert_eq!( + result.get_main_result(), + result2.get_main_result().replace("approx. ", "") + ); } - base.write_prefix(&mut manual_division).unwrap(); - write!(&mut manual_division, "{num} / ").unwrap(); - base.write_prefix(&mut manual_division).unwrap(); - write!(&mut manual_division, "{den}").unwrap(); - - let result2 = evaluate( - &format!("{manual_division} to {sf} sn"), - &mut context, - ) - .unwrap(); - - assert_eq!( - result2.plain_result, - value.to_string(), - "{manual_division} != {value} ({rat:?})" - ); - assert_eq!(result.get_main_result(), result2.get_main_result()); - if den == 1 { + if den == 1 && sf > (num.ilog(base.base_as_u8().into()) as usize) { let result_base10 = evaluate(&format!("{value} to base 10"), &mut context) .unwrap(); - assert_eq!(result_base10.plain_result, num.to_string()); + if sign == Sign::Negative { + assert_eq!(result_base10.plain_result, format!("-{num}")); + } else { + assert_eq!(result_base10.plain_result, num.to_string()); + } } } }