Display recurring digits (#162)

Implements #72.

```
> 1/7
0.[142857]...  (dimensionless)
```

Also changes the behavior of `to digits` so that it will attempt to find
long-period recurring digits and show the entire sequence. The new
default for `to digits` is to show up to 1000 recurring digits.

```
> googol/7 to digits
1428571428571428571428571428571428571428571428571428571428571428571428571428571428571428571428571428.[571428]...  (dimensionless)
> surveyfoot to digits
0.[304800609601219202438404876809753619507239014478028956057912115824231648463296926593853187706375412750825501651003302006604013208026416052832105664211328422656845313690627381254762509525019050038100076200152400, period 210]... millimeter (length)
```
This commit is contained in:
Tiffany Bennett 2024-04-13 14:21:56 -07:00 committed by GitHub
parent 6ce8d91b97
commit 92c58488d4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 467 additions and 95 deletions

27
Cargo.lock generated
View file

@ -425,7 +425,7 @@ dependencies = [
"atty",
"bitflags 1.3.2",
"clap_lex",
"indexmap",
"indexmap 1.9.3",
"strsim",
"termcolor",
"textwrap",
@ -617,6 +617,12 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d"
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "errno"
version = "0.3.8"
@ -816,6 +822,12 @@ version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hashbrown"
version = "0.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
[[package]]
name = "hermit-abi"
version = "0.1.19"
@ -883,7 +895,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [
"autocfg",
"hashbrown",
"hashbrown 0.12.3",
]
[[package]]
name = "indexmap"
version = "2.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
dependencies = [
"equivalent",
"hashbrown 0.14.3",
]
[[package]]
@ -1334,6 +1356,7 @@ dependencies = [
"chrono",
"chrono-humanize",
"chrono-tz",
"indexmap 2.2.6",
"num-bigint",
"num-rational",
"num-traits",

View file

@ -23,6 +23,7 @@ chrono-tz = { version = "0.5.2", default-features = false }
chrono-humanize = { version = "0.1.2", optional = true }
serde = { version = "1", features = ["rc"], default-features = false }
serde_derive = "1"
indexmap = "2"
[dev_dependencies]
serde_json = { version = "1", default-features = false }

View file

@ -3,10 +3,10 @@
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
use num_bigint::BigInt as NumInt;
use num_traits::{Num, One, ToPrimitive, Zero};
use num_traits::{Num, One, Signed, ToPrimitive, Zero};
use std::cmp::Ord;
use std::fmt;
use std::ops::{BitAnd, BitOr, BitXor, Div, Mul, Rem};
use std::ops::{Add, BitAnd, BitOr, BitXor, Div, Mul, Rem, Sub};
#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct BigInt {
@ -56,9 +56,28 @@ impl BigInt {
as usize
}
pub fn next_power_of(&self, base: u8) -> usize {
let mut value = BigInt::one();
let base = BigInt::from(base as i64);
let mut i = 0;
loop {
if self <= &value {
break i;
}
value = &value * &base;
i += 1;
}
}
pub fn as_int(&self) -> Option<i64> {
self.inner.to_i64()
}
pub fn abs(&self) -> BigInt {
BigInt {
inner: self.inner.abs(),
}
}
}
impl fmt::Display for BigInt {
@ -89,6 +108,32 @@ impl From<i64> for BigInt {
}
}
impl From<i32> for BigInt {
fn from(value: i32) -> BigInt {
(value as i64).into()
}
}
impl<'a> Add for &'a BigInt {
type Output = BigInt;
fn add(self, rhs: &'a BigInt) -> BigInt {
BigInt {
inner: &self.inner + &rhs.inner,
}
}
}
impl<'a> Sub for &'a BigInt {
type Output = BigInt;
fn sub(self, rhs: &'a BigInt) -> BigInt {
BigInt {
inner: &self.inner - &rhs.inner,
}
}
}
impl<'a> Mul for &'a BigInt {
type Output = BigInt;

View file

@ -2,6 +2,7 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
use indexmap::IndexSet;
use num_rational::BigRational as NumRat;
use num_traits::{sign::Signed, One, ToPrimitive, Zero};
use serde_derive::{Deserialize, Serialize};
@ -9,9 +10,11 @@ use std::cmp::Ord;
use std::fmt;
use std::ops::{Add, Div, Mul, Neg, Rem, Sub};
use crate::output::Digits;
use super::BigInt;
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Hash)]
pub struct BigRat {
inner: NumRat,
}
@ -65,6 +68,193 @@ impl BigRat {
pub fn as_float(&self) -> f64 {
self.inner.to_f64().unwrap()
}
// Checks if this is a small-period recurring number. Returns the
// digits that recur as well as the period.
//
// Indicate recurring decimal sequences up to 10 digits long. The
// rule here checks if the current remainder (`cursor`) divides
// cleanly into b^N-1 where b is the base and N is the number of
// digits to check, then the result of that division is the digits
// that recur. So for example in 1/7, after the decimal point the
// remainder will be 1/7, and 7 divides cleanly into 999999 to
// produce 142857, the digits which recur. For remainders with a
// numerator other than 1, the numerator is ignored during this
// check, and then multiply the digits by it afterwards.
//
// This is done in machine ints because the extra range isn't
// necessary.
fn is_recurring(&self, base: u8, max_period: u32) -> Option<(i64, u32)> {
assert!(max_period < 18);
let numer = self.numer().as_int()?;
let denom = self.denom().as_int()?;
for i in 1..max_period {
let test = (base as i64).pow(i) - 1;
if test % denom == 0 {
// Recurring digits
let digits = (test / denom) * numer;
return Some((digits, i));
}
}
None
}
fn to_digits_impl(&self, base: u8, digits: Digits) -> (bool, String) {
let sign = *self < BigRat::zero();
let rational = self.abs();
let num = rational.numer();
let den = rational.denom();
let intdigits = (&num / &den).size_in_base(base) as u32;
let mut buf = String::new();
if sign {
buf.push('-');
}
let zero = BigRat::zero();
let one = BigInt::one();
let ten = BigInt::from(base as u64);
let ten_rational = BigRat::ratio(&ten, &one);
let mut cursor = &rational / &BigRat::ratio(&ten.pow(intdigits), &one);
let mut n = 0;
let mut only_zeros = true;
let mut zeros = 0;
let mut placed_decimal = false;
let mut seen_remainders = IndexSet::new();
loop {
let exact = cursor == zero;
let placed_ints = n >= intdigits;
let ndigits = match digits {
Digits::Default => 6,
Digits::FullInt => 1000,
Digits::Digits(n) => intdigits as i32 + n as i32,
};
// Conditions for exiting:
// 1. The number is already exact and all the integer
// positions have been placed, or
// 2. The number is not exact, but all the integer positions
// have been placed, and no more digits should be added
// as the number is getting too long.
let after_radix = n as i32 - zeros as i32;
let max_radix = std::cmp::max(intdigits as i32, ndigits);
let bail = (exact && placed_ints) || after_radix > max_radix;
// Before bailing, first check if adding a few
// extra digits would yield a recurring decimal.
if bail && !exact {
if let Some((digits, period)) = cursor.is_recurring(base, 4) {
buf.push('[');
for n in 1..=period {
let digit = digits / (base as i64).pow(period - n) % base as i64;
buf.push(std::char::from_digit(digit as u32, base as u32).unwrap());
}
buf.push_str("]...");
return (true, buf);
}
}
if bail {
return (exact, buf);
}
if n == intdigits {
buf.push('.');
placed_decimal = true;
}
// Handle recurring decimals
if placed_decimal {
// This catches really long period ones like 1/3937.
if let (index, false) = seen_remainders.insert_full(cursor.clone()) {
// If the remainder is the same as a previous one, then it's recurring.
let period = n - intdigits - index as u32;
buf.insert(buf.len() - period as usize, '[');
if period > 10 {
buf.push_str(", period ");
buf.push_str(&period.to_string());
}
buf.push_str("]...");
return (true, buf);
}
// This catches really short period ones that would be
// missed from bailing early.
if let Some((digits, period)) = cursor.is_recurring(base, 10) {
buf.push('[');
for n in 1..=period {
let digit = digits / (base as i64).pow(period - n) % base as i64;
buf.push(std::char::from_digit(digit as u32, base as u32).unwrap());
}
buf.push_str("]...");
return (true, buf);
}
}
let digit = &(&(&cursor.numer() * &ten) / &cursor.denom()) % &ten;
let v: Option<i64> = digit.as_int();
let v = v.unwrap();
if v != 0 {
only_zeros = false
} else if only_zeros {
zeros += 1;
}
if v != 0 || !only_zeros || n >= intdigits - 1 {
buf.push(std::char::from_digit(v as u32, base as u32).unwrap());
}
cursor = &cursor * &ten_rational;
cursor = &cursor - &BigRat::ratio(&digit, &one);
n += 1;
}
}
pub fn to_scientific(&self, base: u8, digits: Digits) -> (bool, String) {
let num = self.numer();
let den = self.denom();
let absnum = num.abs();
let intdigits = if &absnum > &den {
(&absnum / &den).next_power_of(base) as i64 - 1
} else {
-((&den / &absnum).next_power_of(base) as i64)
};
let absexp = BigInt::from(base as i64).pow((intdigits as i64).abs() as u32);
let rational = if intdigits > 0 {
self * &BigRat::ratio(&BigInt::one(), &absexp)
} else {
self * &BigRat::ratio(&absexp, &BigInt::one())
};
let ten = BigRat::small_ratio(base as i64, 1);
let (rational, intdigits) = if rational.abs() == ten {
(&rational / &ten, intdigits + 1)
} else {
(rational, intdigits)
};
let (is_exact, mut result) = rational.to_digits_impl(base, digits);
if !result.contains('.') {
result.push('.');
result.push('0');
}
result.push('e');
result.push_str(&format!("{}", intdigits));
(is_exact, result)
}
pub fn to_string(&self, base: u8, digits: Digits) -> (bool, String) {
if self == &BigRat::small_ratio(0, 1) {
return (true, "0".to_owned());
}
let abs = self.abs();
let is_computer_base = base == 2 || base == 8 || base == 16 || base == 32;
let is_computer_integer = is_computer_base && self.denom() == BigInt::one();
let can_use_sci = digits == Digits::Default && !is_computer_integer;
if can_use_sci
&& (&abs >= &BigRat::small_ratio(1_000_000_000, 1)
|| &abs <= &BigRat::small_ratio(1, 1_000_000_000))
{
self.to_scientific(base, digits)
} else {
self.to_digits_impl(base, digits)
}
}
}
impl From<NumRat> for BigRat {
@ -145,3 +335,66 @@ impl<'a> Rem for &'a BigRat {
}
}
}
#[cfg(test)]
mod tests {
use crate::{
output::Digits,
types::{BigInt, BigRat},
};
#[test]
fn test_scientific_small() {
let googol = BigRat::ratio(&BigInt::pow(&BigInt::from(10), 100), &BigInt::one());
assert_eq!(
googol.to_scientific(10, Digits::Default),
(true, "1.0e100".to_owned())
);
assert_eq!(
(&BigRat::one() / &googol).to_scientific(10, Digits::Default),
(true, "1.0e-100".to_owned())
);
assert_eq!(
(-&googol).to_scientific(10, Digits::Default),
(true, "-1.0e100".to_owned())
);
assert_eq!(
(&-&BigRat::one() / &googol).to_scientific(10, Digits::Default),
(true, "-1.0e-100".to_owned())
);
let googol_plus = &googol + &BigRat::one();
assert_eq!(
googol_plus.to_scientific(10, Digits::Default),
(false, "1.000000e100".to_owned())
);
assert_eq!(
(&BigRat::one() / &googol_plus).to_scientific(10, Digits::Default),
(false, "9.999999e-101".to_owned())
);
assert_eq!(
(-&googol_plus).to_scientific(10, Digits::Default),
(false, "-1.000000e100".to_owned())
);
assert_eq!(
(&-&BigRat::one() / &googol_plus).to_scientific(10, Digits::Default),
(false, "-9.999999e-101".to_owned())
);
let googol_minus = &googol - &BigRat::one();
assert_eq!(
googol_minus.to_scientific(10, Digits::Default),
(false, "9.999999e99".to_owned())
);
assert_eq!(
(&BigRat::one() / &googol_minus).to_scientific(10, Digits::Default),
(false, "1.000000e-100".to_owned())
);
assert_eq!(
(-&googol_minus).to_scientific(10, Digits::Default),
(false, "-9.999999e99".to_owned())
);
assert_eq!(
(&-&BigRat::one() / &googol_minus).to_scientific(10, Digits::Default),
(false, "-1.000000e-100".to_owned())
);
}
}

View file

@ -35,6 +35,10 @@ impl Numeric {
Numeric::Rational(BigRat::zero())
}
pub fn from_frac(num: impl Into<BigInt>, den: impl Into<BigInt>) -> Numeric {
Numeric::Rational(BigRat::ratio(&num.into(), &den.into()))
}
pub fn abs(&self) -> Numeric {
match *self {
Numeric::Rational(ref rational) => Numeric::Rational(rational.abs()),
@ -114,93 +118,18 @@ impl Numeric {
/// Returns (is_exact, repr).
pub fn to_string(&self, base: u8, digits: Digits) -> (bool, String) {
use std::char::from_digit;
use std::num::FpCategory;
if let Numeric::Float(value) = *self {
match value.classify() {
match *self {
Numeric::Rational(ref rational) => rational.to_string(base, digits),
Numeric::Float(value) => match value.classify() {
FpCategory::Nan => return (false, "NaN".to_owned()),
FpCategory::Infinite if value.is_sign_positive() => {
return (false, "Inf".to_owned())
}
FpCategory::Infinite => return (false, "-Inf".to_owned()),
_ => (),
}
}
let sign = *self < Numeric::zero();
let rational = self.abs();
let (num, den) = rational.to_rational();
let rational = match rational {
Numeric::Rational(rational) => rational,
Numeric::Float(f) => BigRat::from(f),
};
let intdigits = (&num / &den).size_in_base(base) as u32;
let mut buf = String::new();
if sign {
buf.push('-');
}
let zero = BigRat::zero();
let one = BigInt::one();
let ten = BigInt::from(base as u64);
let ten_rational = BigRat::ratio(&ten, &one);
let mut cursor = &rational / &BigRat::ratio(&ten.pow(intdigits), &one);
let mut n = 0;
let mut only_zeros = true;
let mut zeros = 0;
let mut placed_decimal = false;
loop {
let exact = cursor == zero;
let use_sci = if digits != Digits::Default
|| den == one && (base == 2 || base == 8 || base == 16 || base == 32)
{
false
} else {
intdigits + zeros > 9 * 10 / base as u32
};
let placed_ints = n >= intdigits;
let ndigits = match digits {
Digits::Default | Digits::FullInt => 6,
Digits::Digits(n) => intdigits as i32 + n as i32,
};
let bail = (exact && (placed_ints || use_sci))
|| (n as i32 - zeros as i32 > ndigits && use_sci)
|| n as i32 - zeros as i32 > ::std::cmp::max(intdigits as i32, ndigits);
if bail && use_sci {
// scientific notation
let off = if n < intdigits { 0 } else { zeros };
buf = buf[off as usize + placed_decimal as usize + sign as usize..].to_owned();
buf.insert(1, '.');
if buf.len() == 2 {
buf.insert(2, '0');
}
if sign {
buf.insert(0, '-');
}
buf.push_str(&*format!("e{}", intdigits as i32 - zeros as i32 - 1));
return (exact, buf);
}
if bail {
return (exact, buf);
}
if n == intdigits {
buf.push('.');
placed_decimal = true;
}
let digit = &(&(&cursor.numer() * &ten) / &cursor.denom()) % &ten;
let v: Option<i64> = digit.as_int();
let v = v.unwrap();
if v != 0 {
only_zeros = false
} else if only_zeros {
zeros += 1;
}
if !(v == 0 && only_zeros && n < intdigits - 1) {
buf.push(from_digit(v as u32, base as u32).unwrap());
}
cursor = &cursor * &ten_rational;
cursor = &cursor - &BigRat::ratio(&digit, &one);
n += 1;
_ => BigRat::from(value).to_string(base, digits),
},
}
}
@ -308,3 +237,127 @@ impl<'a> Neg for &'a Numeric {
}
}
}
#[cfg(test)]
mod tests {
use crate::{output::Digits, types::Numeric};
#[test]
fn test_tostring_simple() {
assert_eq!(
Numeric::from_frac(1, 1).to_string(10, Digits::Default),
(true, "1".to_owned())
);
assert_eq!(
Numeric::from_frac(1, 2).to_string(10, Digits::Default),
(true, "0.5".to_owned())
);
assert_eq!(
Numeric::from_frac(1, 8).to_string(10, Digits::Default),
(true, "0.125".to_owned())
);
assert_eq!(
Numeric::from_frac(7, 8).to_string(10, Digits::Default),
(true, "0.875".to_owned())
);
assert_eq!(
Numeric::from_frac(123456, 100).to_string(10, Digits::Default),
(true, "1234.56".to_owned())
);
}
#[test]
fn test_recurring_fraction() {
assert_eq!(
Numeric::from_frac(1, 3).to_string(10, Digits::Default),
(true, "0.[3]...".to_owned())
);
assert_eq!(
Numeric::from_frac(2, 3).to_string(10, Digits::Default),
(true, "0.[6]...".to_owned())
);
assert_eq!(
Numeric::from_frac(1, 7).to_string(10, Digits::Default),
(true, "0.[142857]...".to_owned())
);
assert_eq!(
Numeric::from_frac(1000, 3).to_string(10, Digits::Default),
(true, "333.[3]...".to_owned())
);
}
#[test]
fn test_exponent() {
assert_eq!(
Numeric::from_frac(1_000_000_000_000_000i64, 1).to_string(10, Digits::Default),
(true, "1.0e15".to_owned())
);
assert_eq!(
Numeric::from_frac(1_000_000_000_000_000i64, 3).to_string(10, Digits::Default),
(true, "3.[3]...e14".to_owned())
);
}
#[test]
fn test_negatives() {
assert_eq!(
Numeric::from_frac(-123, 1).to_string(10, Digits::Default),
(true, "-123".to_owned())
);
assert_eq!(
Numeric::from_frac(-1000, 3).to_string(10, Digits::Default),
(true, "-333.[3]...".to_owned())
);
assert_eq!(
Numeric::from_frac(-1_000_000_000_000_000i64, 1).to_string(10, Digits::Default),
(true, "-1.0e15".to_owned())
);
}
#[test]
fn test_base2() {
assert_eq!(
Numeric::from_frac(1, 1).to_string(2, Digits::Default),
(true, "1".to_owned())
);
assert_eq!(
Numeric::from_frac(2, 1).to_string(2, Digits::Default),
(true, "10".to_owned())
);
assert_eq!(
Numeric::from_frac(3, 1).to_string(2, Digits::Default),
(true, "11".to_owned())
);
assert_eq!(
Numeric::from_frac(1, 2).to_string(2, Digits::Default),
(true, "0.1".to_owned())
);
assert_eq!(
Numeric::from_frac(1, 3).to_string(2, Digits::Default),
(true, "0.[01]...".to_owned())
);
assert_eq!(
Numeric::from_frac(1, 5).to_string(2, Digits::Default),
(true, "0.[0011]...".to_owned())
);
assert_eq!(
Numeric::from_frac(1, 7).to_string(2, Digits::Default),
(true, "0.[001]...".to_owned())
);
assert_eq!(
Numeric::from_frac(1, 9).to_string(2, Digits::Default),
(true, "0.[000111]...".to_owned())
);
assert_eq!(
Numeric::from_frac(-1000, 3).to_string(2, Digits::Default),
(true, "-101001101.[01]...".to_owned())
);
assert_eq!(
Numeric::from_frac(-1_000_000_000_000_000i64, 1).to_string(2, Digits::Default),
(
true,
"-11100011010111111010100100110001101000000000000000".to_owned()
)
);
}
}

View file

@ -87,7 +87,7 @@ fn test_sqrt_errors() {
#[test]
fn test_number_regress() {
test("953 mega", "9.53e8 (dimensionless)");
test("953 mega", "953000000 (dimensionless)");
}
#[test]
@ -265,7 +265,7 @@ fn test_convert_to_substances() {
"egg: USA large egg. \
mass = 1 kilogram; \
egg_shelled = 20 egg; \
egg_white = 100/3, approx. 33.33333 egg; \
egg_white = 33.[3]... egg; \
egg_yolk = 5000/93, approx. 53.76344 egg",
);
}
@ -393,7 +393,7 @@ fn test_functions() {
#[test]
fn test_equal_rhs() {
test("1 -> a=3", "1/3, approx. 0.3333333 a (dimensionless)");
test("1 -> a=3", "0.[3]... a (dimensionless)");
}
#[test]
@ -520,14 +520,11 @@ fn test_digits() {
"ln(1234) -> digits 100",
"approx. 7.11801620446533345187845043255947530269622802734375 (dimensionless)",
);
test(
"1/7 -> digits 50",
"1/7, approx. 0.1428571428571428571428571428571428571428571428571428 (dimensionless)",
);
test("trillion / 7", "approx. 1.428571e11 (dimensionless)");
test("1/7 -> digits 50", "0.[142857]... (dimensionless)");
test("trillion / 7", "1.[428571]...e11 (dimensionless)");
test(
"trillion / 7 to digits",
"approx. 142857142857.1 (dimensionless)",
"142857142857.[142857]... (dimensionless)",
);
}
@ -742,7 +739,7 @@ fn test_tim() {
// Issue #151, rink crashing due to stack overflow
test(
"Tim",
"Definition: Tim = 12^-4 hour = 3125/18, approx. 173.6111 millisecond (time; s)",
"Definition: Tim = 12^-4 hour = 173.6[1]... millisecond (time; s)",
);
}