Repository: sharkdp/numbat (rust)

Issue:
Large numbers in pretty printing
This is not helpful:
```
>>> let sun_luminosity = 3.828e26 W

  let sun_luminosity = 382800000000000000000000000 watt
```

Pull request:
Better formatting of large numbers
- Thousands separator for large inters
- Always use '+' in scientific notation (1e+3)

closes #152

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index aed6010..a8a33f7 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -80,6 +80,12 @@ dependencies = [
  "num-traits",
 ]
 
+[[package]]
+name = "arrayvec"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
+
 [[package]]
 name = "assert_cmd"
 version = "2.0.12"
@@ -760,6 +766,16 @@ dependencies = [
  "num-traits",
 ]
 
+[[package]]
+name = "num-format"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a652d9771a63711fd3c3deb670acfbe5c30a4072e664d7a3bf5a9e1056ac72c3"
+dependencies = [
+ "arrayvec",
+ "itoa",
+]
+
 [[package]]
 name = "num-integer"
 version = "0.1.45"
@@ -803,6 +819,7 @@ dependencies = [
  "insta",
  "itertools 0.11.0",
  "libc",
+ "num-format",
  "num-integer",
  "num-rational",
  "num-traits",
diff --git a/numbat/Cargo.toml b/numbat/Cargo.toml
index b99aa05..982937f 100644
--- a/numbat/Cargo.toml
+++ b/numbat/Cargo.toml
@@ -27,6 +27,7 @@ unicode-ident = "1.0.11"
 unicode-width = "0.1.10"
 libc = "0.2.147"
 rust-embed = { version = "8.0.0", features = ["interpolate-folder-path"] }
+num-format = "0.4.4"
 
 [dev-dependencies]
 approx = "0.5"
diff --git a/numbat/src/number.rs b/numbat/src/number.rs
index c7f1f9d..651e0b1 100644
--- a/numbat/src/number.rs
+++ b/numbat/src/number.rs
@@ -1,4 +1,4 @@
-use num_traits::Pow;
+use num_traits::{Pow, ToPrimitive};
 
 #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] // TODO: we probably want to remove 'Copy' once we move to a more sophisticated numerical type
 pub struct Number(pub f64);
@@ -26,12 +26,29 @@ impl Number {
     pub fn pretty_print(self) -> String {
         let number = self.0;
 
-        // 64-bit floats can accurately represent integers up to 2^52 [1].
+        // 64-bit floats can accurately represent integers up to 2^52 [1],
+        // which is approximately 4.5 × 10^15.
         //
         // [1] https://stackoverflow.com/a/43656339
         //
         if self.is_integer() && self.0.abs() < 1e15 {
-            format!("{number}")
+            use num_format::{CustomFormat, Grouping, ToFormattedString};
+
+            let format = CustomFormat::builder()
+                .grouping(if self.0.abs() >= 100_000.0 {
+                    Grouping::Standard
+                } else {
+                    Grouping::Posix
+                })
+                .minus_sign("-")
+                .separator("_")
+                .build()
+                .unwrap();
+
+            number
+                .to_i64()
+                .expect("small enough integers are representable as i64")
+                .to_formatted_string(&format)
         } else {
             use pretty_dtoa::{dtoa, FmtFloatConfig};
 
@@ -50,6 +67,8 @@ impl Number {
                 } else {
                     formatted_number.to_string()
                 }
+            } else if formatted_number.contains('e') && !formatted_number.contains("e-") {
+                formatted_number.replace("e", "e+")
             } else {
                 formatted_number
             }
@@ -108,21 +127,27 @@ fn test_pretty_print() {
     assert_eq!(Number::from_f64(1.).pretty_print(), "1");
     assert_eq!(Number::from_f64(100.).pretty_print(), "100");
     assert_eq!(Number::from_f64(1.234).pretty_print(), "1.234");
-    assert_eq!(Number::from_f64(1.234e50).pretty_print(), "1.234e50");
-    assert_eq!(Number::from_f64(-1.234e50).pretty_print(), "-1.234e50");
+    assert_eq!(Number::from_f64(1.234e50).pretty_print(), "1.234e+50");
+    assert_eq!(Number::from_f64(-1.234e50).pretty_print(), "-1.234e+50");
     assert_eq!(Number::from_f64(1.234e-50).pretty_print(), "1.234e-50");
     assert_eq!(Number::from_f64(-1.234e-50).pretty_print(), "-1.234e-50");
 
-    assert_eq!(Number::from_f64(1234567890.).pretty_print(), "1234567890");
+    assert_eq!(Number::from_f64(1234.).pretty_print(), "1234");
+    assert_eq!(Number::from_f64(12345.).pretty_print(), "12345");
+    assert_eq!(Number::from_f64(123456.).pretty_print(), "123_456");
+    assert_eq!(
+        Number::from_f64(1234567890.).pretty_print(),
+        "1_234_567_890"
+    );
     assert_eq!(
         Number::from_f64(1234567890000000.).pretty_print(),
-        "1.23457e15"
+        "1.23457e+15"
     );
 
     assert_eq!(Number::from_f64(1.23456789).pretty_print(), "1.23457");
     assert_eq!(
         Number::from_f64(1234567890000.1).pretty_print(),
-        "1.23457e12"
+        "1.23457e+12"
     );
 
     assert_eq!(Number::from_f64(100.00001).pretty_print(), "100.0");
diff --git a/numbat/src/typed_ast.rs b/numbat/src/typed_ast.rs
index 6a1a6a4..5e72dee 100644
--- a/numbat/src/typed_ast.rs
+++ b/numbat/src/typed_ast.rs
@@ -386,8 +386,8 @@ impl PrettyPrint for Statement {
     }
 }
 
-fn pretty_scalar(Number(n): Number) -> Markup {
-    m::value(format!("{n}"))
+fn pretty_scalar(n: Number) -> Markup {
+    m::value(format!("{}", n.pretty_print()))
 }
 
 fn with_parens(expr: &Expression) -> Markup {


Test patch:
diff --git a/numbat/tests/interpreter.rs b/numbat/tests/interpreter.rs
index 0c45246..d32340b 100644
--- a/numbat/tests/interpreter.rs
+++ b/numbat/tests/interpreter.rs
@@ -71,7 +71,7 @@ fn simple_value() {
     expect_failure("0b0.0", "Expected base-2 digit");
 
     expect_output("0o0", "0");
-    expect_output("0o01234567", 0o01234567.to_string());
+    expect_output("0o01234567", "342_391");
     expect_output("0o0_0", "0");
     expect_failure("0o012345678", "Expected base-8 digit");
     expect_failure("0o", "Expected base-8 digit");
@@ -81,7 +81,7 @@ fn simple_value() {
     expect_failure("0o0.0", "Expected base-8 digit");
 
     expect_output("0x0", "0");
-    expect_output("0x0123456789abcdef", "8.19855e16");
+    expect_output("0x0123456789abcdef", "8.19855e+16");
     expect_output("0x0_0", "0");
     expect_failure("0x0123456789abcdefg", "Expected base-16 digit");
     expect_failure("0x", "Expected base-16 digit");
@@ -361,12 +361,12 @@ fn test_last_result_identifier() {
 #[test]
 fn test_misc_examples() {
     expect_output("1920/16*9", "1080");
-    expect_output("2^32", "4294967296");
+    expect_output("2^32", "4_294_967_296");
     expect_output("sqrt(1.4^2 + 1.5^2) * cos(pi/3)^2", "0.512957");
 
     expect_output("2min + 30s", "2.5 min");
     expect_output("2min + 30s -> sec", "150 s");
-    expect_output("4/3 * pi * (6000km)³", "9.04779e11 km³");
+    expect_output("4/3 * pi * (6000km)³", "9.04779e+11 km³");
     expect_output("40kg * 9.8m/s^2 * 150cm", "588 kg·m²/s²");
     expect_output("sin(30°)", "0.5");