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..27a9d460 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,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::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 +194,12 @@ 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/base.rs b/core/src/num/base.rs index d6a2aa89..c023b5ed 100644 --- a/core/src/num/base.rs +++ b/core/src/num/base.rs @@ -25,6 +25,14 @@ 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(_)) + } pub(crate) const fn base_as_u8(self) -> u8 { match self.0 { @@ -35,6 +43,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), @@ -62,7 +78,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 04a6161b..bbb4f1ad 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, Display, Write}; use std::{cmp, fmt, hash, ops}; pub(crate) mod sign { @@ -636,7 +637,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 +648,15 @@ 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 +666,121 @@ impl BigRat { int, )?; + if !self.is_definitely_zero() + && let FormattingStyle::ScientificNotation(sf) = style + { + let positive_exponent = !integer_part.is_definitely_zero(); + + 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; + + let decimal = FormattedBigRat { + sign: Sign::Positive, + ty: decimal.value.ty, + }; + + let (FormattedBigRatType::Integer(_, _, is_imag, _) + | FormattedBigRatType::Decimal(_, _, is_imag)) = decimal.ty + else { + unreachable!() + }; + + 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 { + 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 trimmed_string: String = string + .trim_start_matches(|ch| { + ch == decimal_separator.decimal_separator() || ch == '0' + }) + .into(); + + let zeros = string + .chars() + .count() + .saturating_sub(trimmed_string.chars().count()) + .saturating_sub(1); + + (trimmed_string, zeros) + }; + + if value.len() > sf { + value.truncate(sf); + } else { + while value.len() < sf { + value.push('0'); + } + } + + if sf > 1 { + 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( + base, + value.into(), + separator, + if positive_exponent { "" } else { "-" }, + BigUint::Small(exponent as u64) + .format( + &biguint::FormatOptions { + base, + write_base_prefix: true, + sf_limit: None, + }, + int, + )? + .value, + is_imag, + ), + }, + exact, + )); + } + let num_trailing_digits_to_print = if style == FormattingStyle::ExactFloat || (style == FormattingStyle::Auto && terminating()?) || style == FormattingStyle::Exact @@ -1236,7 +1355,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 +1430,40 @@ 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 + // string (empty, "i", "pi", etc.) + ScientificNotation( + Base, + 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] @@ -1371,6 +1524,15 @@ impl fmt::Display for FormattedBigRat { } write!(f, "{term}")?; } + FormattedBigRatType::ScientificNotation(base, m, separator, sign, exponent, imag) => { + base.write_prefix(f)?; + + if imag.is_empty() { + write!(f, "{m}{separator}{sign}{exponent}")?; + } else { + write!(f, "({m}{separator}{sign}{exponent}){imag}")?; + } + } } Ok(()) } @@ -1381,8 +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::mem; #[test] @@ -1426,4 +1593,135 @@ 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( + &FormatOptions { + base: Base::BIN, + style: FormattingStyle::SignificantFigures(1), + term: "", + use_parens_if_fraction: false, + decimal_separator: DecimalSeparatorStyle::Dot, + }, + &Never, + ) + .unwrap(); + + assert_eq!(result.value.to_string(), "-0b1"); + + let result = rat + .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"); + } + + #[test] + fn test_scientific_formatting_reversible() { + for decimal_separator in [DecimalSeparatorStyle::Comma, DecimalSeparatorStyle::Dot] { + for sign in [Sign::Negative, Sign::Positive] { + 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, + ] { + if den != 1 && num % den == 0 { + continue; + } + + let rat = BigRat { + sign, + 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, + 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] { + let result = rat.format( + &FormatOptions { + base, + style: FormattingStyle::ScientificNotation(sf), + term: "", + use_parens_if_fraction: false, + decimal_separator, + }, + &Never, + ); + + let value = result.unwrap().value; + + 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()); + + 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. ", "") + ); + } + + 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(); + + if sign == Sign::Negative { + assert_eq!(result_base10.plain_result, format!("-{num}")); + } else { + assert_eq!(result_base10.plain_result, num.to_string()); + } + } + } + } + } + } + } + } + } } diff --git a/core/src/num/biguint.rs b/core/src/num/biguint.rs index f64cbb28..dde3e570 100644 --- a/core/src/num/biguint.rs +++ b/core/src/num/biguint.rs @@ -1379,16 +1379,15 @@ 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) + let base = if params.write_base_prefix { + params.base } else { - None + Base::from_plain_base(params.base.base_as_u8()).expect("is valid base") }; - if self.is_zero() { return Ok(Exact::new( FormattedBigUint { - base: base_prefix, + base, ty: FormattedBigUintType::Zero, }, true, @@ -1400,7 +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 { - base: base_prefix, + base, + ty: FormattedBigUintType::Simple(num.get(0)), }, true, @@ -1454,7 +1454,8 @@ impl Format for BigUint { .is_none_or(|sf| sf >= output.len() - num_leading_zeroes); Exact::new( FormattedBigUint { - base: base_prefix, + base, + ty: FormattedBigUintType::Complex(output, params.sf_limit), }, exact, @@ -1474,24 +1475,80 @@ enum FormattedBigUintType { #[must_use] #[derive(Debug)] pub(crate) struct FormattedBigUint { - base: Option, + base: Base, ty: FormattedBigUintType, } +#[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"); + } +} + 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)?; - } + self.base.write_prefix(f)?; + match &self.ty { 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 { - write!(f, "0")?; + debug_assert!(s.is_ascii()); + debug_assert_eq!(s.len(), s.chars().count()); + + if let Some(sf_limit) = sf_limit + && s.len() > *sf_limit + { + let s = s.as_bytes(); + + let after_last_char = + parse_char(char::from(s[s.len() - 1 - *sf_limit]), self.base); + + let round_up = after_last_char >= self.base.base_as_u8().div_ceil(2); + + let mut zeros_count = s.len() - sf_limit; + + if round_up { + let max: u8 = self.base.max_char().try_into().expect("is ascii"); + + let number = &s[s.len() - sf_limit..]; + + let trailing_max_count = number.iter().take_while(|p| **p == max).count(); + + zeros_count += trailing_max_count; + + if trailing_max_count == *sf_limit { + write!(f, "1")?; + } else { + 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 { - write!(f, "{ch}")?; + // truncate + for ch in s.iter().rev().take(*sf_limit) { + write!(f, "{}", char::from(*ch))?; + } + } + + for _ in 0..zeros_count { + write!(f, "0")?; + } + } else { + for ch in s.as_bytes().iter().rev() { + write!(f, "{}", char::from(*ch))?; } } } @@ -1530,6 +1587,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); diff --git a/core/src/num/formatting_style.rs b/core/src/num/formatting_style.rs index 5950b5c5..5db4fa83 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..7ce2d8ab 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,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::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 +155,7 @@ impl Value { 13u8.serialize(write)?; d.serialize(write)?; } + Self::Sn => 14u8.serialize(write)?, } Ok(()) } @@ -189,6 +194,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 +210,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 +302,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 +426,7 @@ impl Value { Ok(res) } + #[allow(clippy::too_many_lines)] pub(crate) fn format( &self, indent: usize, @@ -450,6 +467,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 +568,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:?})") diff --git a/core/tests/integration_tests.rs b/core/tests/integration_tests.rs index 4ac63ae0..3c98af27 100644 --- a/core/tests/integration_tests.rs +++ b/core/tests/integration_tests.rs @@ -4226,11 +4226,15 @@ 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] 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] @@ -4240,17 +4244,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] @@ -4288,6 +4292,18 @@ 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_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] fn sf_small_1() { test_eval("pi / 1000000 to 1 sf", "approx. 0.000003"); @@ -4298,6 +4314,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"); @@ -4380,12 +4416,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] @@ -4416,56 +4452,77 @@ 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. 1234000"); + 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. 1234500"); + 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. 1234560"); + 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"); } #[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"); } #[test] fn trailing_zeroes_sf_1() { - test_eval("1234560 to 5sf", "approx. 1234500"); + test_eval("1234560 to 5sf", "approx. 1234600"); } #[test] @@ -4498,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"); @@ -6167,3 +6234,235 @@ 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" + ); +} + +#[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" + ); + + let mut context = Context::new(); + assert_eq!( + evaluate("5i to 3 sn", &mut context) + .unwrap() + .get_main_result(), + "(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(); + assert_eq!( + evaluate("123.456 to 5 sn", &mut context) + .unwrap() + .get_main_result(), + "approx. 1.2346 × 10^2" + ); + + let mut context = Context::new(); + assert_eq!( + evaluate("12389 to 3 sn", &mut context) + .unwrap() + .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" + ); + + 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" + ); + + let mut context = Context::new(); + assert_eq!( + evaluate("0.9999 to 2 sn", &mut context) + .unwrap() + .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" + ); + + let mut context = Context::new(); + assert_eq!( + evaluate("0.5i to 4 sn", &mut context) + .unwrap() + .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" + ); + + let mut context = Context::new(); + assert_eq!( + evaluate("36#i to 1sn", &mut context) + .unwrap() + .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" + ); +}