From 010d6469405e3f1989c85b9326ae35ec4300b7bd Mon Sep 17 00:00:00 2001 From: ZHU Yuhao Date: Fri, 6 Mar 2026 20:59:25 +0100 Subject: [PATCH 1/2] Add manual --- docs/plans/api_roadmap.md | 23 +- docs/plans/cli_calculator.md | 48 +- docs/user_manual.md | 1031 ++++++++++++++++++++++++++++++++++ docs/user_manual_cli.md | 416 ++++++++++++++ src/decimo/str.mojo | 11 + 5 files changed, 1503 insertions(+), 26 deletions(-) create mode 100644 docs/user_manual.md create mode 100644 docs/user_manual_cli.md diff --git a/docs/plans/api_roadmap.md b/docs/plans/api_roadmap.md index 44226a01..c4e0dd4f 100644 --- a/docs/plans/api_roadmap.md +++ b/docs/plans/api_roadmap.md @@ -249,16 +249,7 @@ var d = x.digit(i) # Returns i-th decimal digit var y = x.clamp(lower, upper) # clamp between bounds ``` -### 5.8 `sign()` Method Returning Int - -```mojo -# Python's math.copysign pattern: -var s = x.sign() # Returns -1, 0, or 1 as Int (not Bool) -``` - -Currently `sign` is a `Bool` field on BigDecimal. Consider a `signum()` method that returns `Int` (-1/0/1). - -### 5.9 Approximate Equality +### 5.8 Approximate Equality ```mojo # Useful for testing and numerical code: @@ -266,18 +257,6 @@ x.is_close(y, tolerance=BigDecimal("0.001")) x.is_close(y, rel_tol=BigDecimal("1e-9")) ``` -### 5.10 Format Control - -```mojo -# Format with locale-aware separators: -x.format(decimal_places=2, thousands_separator=",") # "1,234,567.89" -x.format(decimal_places=2, thousands_separator="_") # "1_234_567.89" -``` - -BigInt has `to_string_with_separators()`. This should be extended to BigDecimal. - ---- - ## Part VI: Remaining Tasks (Re-ranked) > Updated after audit on 2026-02-27. Items already completed are removed. diff --git a/docs/plans/cli_calculator.md b/docs/plans/cli_calculator.md index 8e7c8294..129d6a76 100644 --- a/docs/plans/cli_calculator.md +++ b/docs/plans/cli_calculator.md @@ -257,6 +257,18 @@ Format the final `BigDecimal` result based on CLI flags: 6. Handle unary minus. 7. Test with basic expressions. +| # | Task | Status | Notes | +| --- | ------------------------------------------------ | :----: | ----------------------------- | +| 1.1 | Project structure (src/cli/, calculator module) | ✓ | | +| 1.2 | Tokenizer (numbers, `+ - * /`, parens) | ✓ | | +| 1.3 | Shunting-yard parser (infix → RPN) | ✓ | | +| 1.4 | RPN evaluator using `BigDecimal` | ✓ | | +| 1.5 | ArgMojo wiring (`expr`, `--precision`, `--help`) | ✓ | | +| 1.6 | Unary minus | ✓ | | +| 1.7 | Basic expression tests | ✓ | 4 test files, 118 tests total | +| 1.8 | Pipeline / stdin input (`echo "1+2" \| decimo`) | ✗ | Not yet implemented | +| 1.9 | File input (`decimo file.dm`) | ✗ | Not yet implemented | + ### Phase 2: Power and Functions 1. Add `^` operator with right associativity. @@ -265,6 +277,19 @@ Format the final `BigDecimal` result based on CLI flags: 4. Add built-in constants (`pi`, `e`). 5. Add output formatting flags (`--scientific` or `-s`, `--engineering` or `-e`, `--pad-to-precision` or `-P`). +| # | Task | Status | Notes | +| ---- | ----------------------------------------------------------------- | :----: | ----------------------------------------------- | +| 2.1 | `^` operator (right-associative, `**` alias) | ✓ | | +| 2.2 | Single-arg functions: `sqrt`, `cbrt`, `ln`, `log10`, `exp`, `abs` | ✓ | | +| 2.3 | Trig functions: `sin`, `cos`, `tan`, `cot`, `csc` | ✓ | `cot` and `csc` are extras beyond original plan | +| 2.4 | Multi-arg functions: `root(x, n)`, `log(x, base)` | ✓ | `log(x, base)` is an extra | +| 2.5 | Constants: `pi`, `e` | ✓ | | +| 2.6 | `--scientific` / `-s` | ✓ | | +| 2.7 | `--engineering` / `-e` | ✓ | | +| 2.8 | `--pad` / `-P` (trailing zeros) | ✓ | | +| 2.9 | `--delimiter` / `-d` (digit grouping) | ✓ | Extra feature beyond original plan | +| 2.10 | `--rounding-mode` / `-r` | ✓ | 7 modes; extra feature beyond original plan | + ### Phase 3: Polish 1. Error messages: clear diagnostics for malformed expressions (e.g., "Unexpected token '*' at position 5"). @@ -274,6 +299,15 @@ Format the final `BigDecimal` result based on CLI flags: 5. Documentation and examples in README. 6. Build and distribute as a single binary. +| # | Task | Status | Notes | +| --- | ---------------------------------------------------------------------- | :----: | --------------------------------------------- | +| 3.1 | Error messages with position info + caret display | ✓ | Colored stderr: red `Error:`, green `^` caret | +| 3.2 | Edge cases (div-by-zero, negative sqrt, empty expression, etc.) | ✓ | 27 error-handling tests | +| 3.3 | ArgMojo v0.2.0 upgrade (`add_tip()`, `allow_negative_numbers()`, etc.) | ? | Blocked: waiting for ArgMojo v0.2.0 in pixi | +| 3.4 | Performance validation | ✗ | No CLI-level benchmarks yet | +| 3.5 | Documentation (user manual for CLI) | ✗ | Will be `docs/user_manual_cli.md` | +| 3.6 | Build and distribute as single binary | ✗ | | + ### Phase 4: Interactive REPL 1. Read-eval-print loop: read a line from stdin, evaluate, print result, repeat. @@ -300,12 +334,20 @@ Error: division by zero decimo> exit ``` +| # | Task | Status | Notes | +| --- | ---------------------------------------- | :----: | ----- | +| 4.1 | Read-eval-print loop | ✗ | | +| 4.2 | Custom prompt (`decimo>`) | ✗ | | +| 4.3 | `ans` variable (previous result) | ✗ | | +| 4.4 | Variable assignment (`x = expr`) | ✗ | | +| 4.5 | Session-level precision (`:precision N`) | ✗ | | +| 4.6 | Graceful exit (`exit`, `quit`, Ctrl-D) | ✗ | | +| 4.7 | Error recovery (don't crash session) | ✗ | | + ### Phase 5: Future Enhancements 1. Detect full-width digits/operators for CJK users while parsing. ---- - ### ArgMojo v0.2.0 Upgrade Tasks > **Prerequisite:** ArgMojo ≥ v0.2.0 is available as a pixi package. @@ -367,8 +409,6 @@ Although not needed immediately, the new `add_subcommand()` API could later supp This is deferred to Phase 4 planning. ---- - ## Design Decisions ### All Numbers Are `BigDecimal` diff --git a/docs/user_manual.md b/docs/user_manual.md new file mode 100644 index 00000000..2a917b03 --- /dev/null +++ b/docs/user_manual.md @@ -0,0 +1,1031 @@ +# Decimo — User Manual + +> Comprehensive guide to using the Decimo arbitrary-precision arithmetic library in Mojo. + +All code examples below assume that you have imported the prelude at the top of +your Mojo file: + +```mojo +from decimo.prelude import * +``` + +- [Part I — BigInt (`BInt`)](#part-i--bigint-bint) + - [Overview](#overview) + - [Construction](#construction) + - [From zero](#from-zero) + - [From `Int`](#from-int) + - [From `String`](#from-string) + - [From `Scalar` (any integral SIMD type)](#from-scalar-any-integral-simd-type) + - [Summary of constructors](#summary-of-constructors) + - [Arithmetic Operations](#arithmetic-operations) + - [Binary operators](#binary-operators) + - [Unary operators](#unary-operators) + - [In-place operators](#in-place-operators) + - [Division Semantics](#division-semantics) + - [Comparison](#comparison) + - [Bitwise Operations](#bitwise-operations) + - [Shift Operations](#shift-operations) + - [Mathematical Functions](#mathematical-functions) + - [Exponentiation](#exponentiation) + - [Integer square root](#integer-square-root) + - [Number Theory](#number-theory) + - [GCD — Greatest Common Divisor](#gcd--greatest-common-divisor) + - [LCM — Least Common Multiple](#lcm--least-common-multiple) + - [Extended GCD](#extended-gcd) + - [Modular Exponentiation](#modular-exponentiation) + - [Modular Inverse](#modular-inverse) + - [Conversion and Output](#conversion-and-output) + - [String conversions](#string-conversions) + - [Numeric conversions](#numeric-conversions) + - [Query Methods](#query-methods) + - [Constants and Factory Methods](#constants-and-factory-methods) +- [Part II — BigDecimal (`Decimal`)](#part-ii--bigdecimal-decimal) + - [Overview](#overview-1) + - [How Precision Works](#how-precision-works) + - [Construction](#construction-1) + - [From zero](#from-zero-1) + - [From `Int`](#from-int-1) + - [From `String`](#from-string-1) + - [From integral scalars](#from-integral-scalars) + - [From floating-point — `from_float()`](#from-floating-point--from_float) + - [From Python — `from_python_decimal()`](#from-python--from_python_decimal) + - [Summary of constructors](#summary-of-constructors-1) + - [Arithmetic Operations](#arithmetic-operations-1) + - [Division Methods](#division-methods) + - [`true_divide()` — recommended for decimal division](#true_divide--recommended-for-decimal-division) + - [Operator `/` — true division with default precision](#operator---true-division-with-default-precision) + - [Operator `//` — truncated (integer) division](#operator---truncated-integer-division) + - [Comparison](#comparison-1) + - [Rounding and Formatting](#rounding-and-formatting) + - [`round()` — round to decimal places](#round--round-to-decimal-places) + - [`quantize()` — match scale of another decimal](#quantize--match-scale-of-another-decimal) + - [`normalize()` — remove trailing zeros](#normalize--remove-trailing-zeros) + - [`__ceil__`, `__floor__`, `__trunc__`](#__ceil__-__floor__-__trunc__) + - [RoundingMode](#roundingmode) + - [Mathematical Functions — Roots and Powers](#mathematical-functions--roots-and-powers) + - [Square root](#square-root) + - [Cube root](#cube-root) + - [Nth root](#nth-root) + - [Power / exponentiation](#power--exponentiation) + - [Mathematical Functions — Exponential and Logarithmic](#mathematical-functions--exponential-and-logarithmic) + - [Exponential (e^x)](#exponential-ex) + - [Natural logarithm](#natural-logarithm) + - [Logarithm with arbitrary base](#logarithm-with-arbitrary-base) + - [Base-10 logarithm](#base-10-logarithm) + - [Mathematical Functions — Trigonometric](#mathematical-functions--trigonometric) + - [Basic functions](#basic-functions) + - [Reciprocal functions](#reciprocal-functions) + - [Inverse functions](#inverse-functions) + - [Mathematical Constants](#mathematical-constants) + - [π (pi)](#π-pi) + - [e (Euler's number)](#e-eulers-number) + - [Conversion and Output](#conversion-and-output-1) + - [String output](#string-output) + - [`repr()`](#repr) + - [Numeric conversions](#numeric-conversions-1) + - [Query Methods](#query-methods-1) + - [`as_tuple()` — Python-compatible decomposition](#as_tuple--python-compatible-decomposition) + - [Other methods](#other-methods) + - [Python Interoperability](#python-interoperability) + - [From Python](#from-python) + - [Matching Python's API](#matching-pythons-api) + - [Appendix A — Import Paths](#appendix-a--import-paths) + - [Appendix B — Traits Implemented](#appendix-b--traits-implemented) + - [BigInt](#bigint) + - [BigDecimal](#bigdecimal) + - [Appendix C — Complete API Tables](#appendix-c--complete-api-tables) + - [BigInt — All Operators](#bigint--all-operators) + - [BigDecimal — Mathematical Functions](#bigdecimal--mathematical-functions) + +## Installation + +Decimo is available in the [modular-community](https://repo.prefix.dev/modular-community) package repository. Add it to your `channels` list in `pixi.toml`: + +```toml +channels = ["https://conda.modular.com/max", "https://repo.prefix.dev/modular-community", "conda-forge"] +``` + +Then install: + +```bash +pixi add decimo +``` + +Or add it manually to `pixi.toml`: + +```toml +decimo = "==0.8.0" +``` + +Then run `pixi install`. + +## Quick Start + +```mojo +from decimo.prelude import * + + +fn main() raises: + # Arbitrary-precision integer + var a = BInt("12345678901234567890") + var b = BInt(42) + print(a * b) # 518518513851851851180 + print(BInt(2) ** 256) # 2^256, all 78 digits + + # Arbitrary-precision decimal + var x = Decimal("123456789.123456789") + var y = Decimal("1234.56789") + print(x + y) # 123458023.691346789 (exact) + print(x.true_divide(y, precision=50)) # 50 significant digits + print(x.sqrt(precision=100)) # 100 significant digits + print(Decimal.pi(precision=1000)) # 1000 digits of π +``` + +# Part I — BigInt (`BInt`) + +## Overview + +`BigInt` (alias `BInt`) is an arbitrary-precision signed integer type — the Mojo-native equivalent of Python's `int`. It supports unlimited-precision integer arithmetic, bitwise operations, and number-theoretic functions. + +| Property | Value | +| ----------------- | ---------------------------- | +| Full name | `BigInt` | +| Short alias | `BInt` | +| Internal base | 2^32 (binary representation) | +| Word type | `UInt32` (little-endian) | +| Python equivalent | `int` | + +## Construction + +### From zero + +```mojo +var x = BInt() # 0 +``` + +### From `Int` + +```mojo +var x = BInt(42) +var y = BInt(-100) +var z: BInt = 42 # Implicit conversion from Int +``` + +The constructor is marked `@implicit`, so Mojo can automatically convert `Int` to `BInt` where expected. + +### From `String` + +```mojo +var a = BInt("12345678901234567890") # Basic decimal string +var b = BInt("-98765") # Negative number +var c = BInt("1_000_000") # Underscores as separators +var d = BInt("1,234,567") # Commas as separators +var e = BInt("1.23e5") # Scientific notation (= 123000) +var f = BInt("1991_10,18") # Mixed separators (= 19911018) +``` + +> **Note:** The string must represent an integer. `BInt("1.5")` raises an error. Scientific notation like `"1.23e5"` is accepted only if the result is an integer. + +### From `Scalar` (any integral SIMD type) + +```mojo +var x = BInt(UInt32(42)) +var y = BInt(Int64(-5)) +var z: BInt = UInt32(99) # Implicit conversion +``` + +Accepts any integral scalar type (`Int8` through `Int256`, `UInt8` through `UInt256`, etc.). + +### Summary of constructors + +| Constructor | Description | +| --------------------------- | ------------------------------------- | +| `BInt()` | Zero | +| `BInt(value: Int)` | From `Int` (implicit) | +| `BInt(value: String)` | From decimal string (raises) | +| `BInt(value: Scalar)` | From integral scalar (implicit) | +| `BInt.from_uint64(value)` | From `UInt64` | +| `BInt.from_uint128(value)` | From `UInt128` | +| `BInt.from_string(value)` | Explicit factory from string (raises) | +| `BInt.from_bigint10(value)` | Convert from `BigInt10` | + +## Arithmetic Operations + +### Binary operators + +| Expression | Description | Raises? | +| ------------- | --------------------------------- | ------------------ | +| `a + b` | Addition | No | +| `a - b` | Subtraction | No | +| `a * b` | Multiplication | No | +| `a // b` | Floor division (rounds toward −∞) | Yes (zero div) | +| `a % b` | Floor modulo (Python semantics) | Yes (zero div) | +| `divmod(a,b)` | Floor quotient and remainder | Yes (zero div) | +| `a ** b` | Exponentiation | Yes (negative exp) | + +### Unary operators + +| Expression | Description | +| ---------- | ------------------------------ | +| `-a` | Negation | +| `+a` | Unary plus (returns copy) | +| `abs(a)` | Absolute value | +| `bool(a)` | `True` if nonzero | +| `~a` | Bitwise NOT (two's complement) | + +### In-place operators + +`+=`, `-=`, `*=`, `//=`, `%=`, `<<=`, `>>=` are all supported and perform true in-place mutation to reduce memory allocation. + +```mojo +var a = BInt("12345678901234567890") +var b = BInt(12345) +print(a + b) # 12345678901234580235 +print(a - b) # 12345678901234555545 +print(a * b) # 152415787814108380241050 +print(a // b) # 999650944609516 +print(a % b) # 9615 +print(BInt(2) ** 10) # 1024 +``` + +## Division Semantics + +BigInt supports two division conventions: + +| Name | Operator / Method | Quotient | Python equivalent | +| ----------------- | ------------------------------------------ | ----------- | ----------------- | +| Floor division | `//`, `%`, `divmod()` | Toward −∞ | `//`, `%` | +| Truncate division | `.truncate_divide()`, `.truncate_modulo()` | Toward zero | C/Java `/`, `%` | + +The difference matters for negative operands: + +```mojo +var a = BInt(7) +var b = BInt(-2) + +# Floor division (Python semantics) +print(a // b) # -4 +print(a % b) # -1 + +# Truncate division (C/Java semantics) +print(a.truncate_divide(b)) # -3 +print(a.truncate_modulo(b)) # 1 +``` + +## Comparison + +All six comparison operators (`==`, `!=`, `>`, `>=`, `<`, `<=`) are supported. Each accepts both `BInt` and `Int` as the right operand. + +```mojo +var a = BInt("12345678901234567890") +print(a > 1000) # True +print(a == BInt("12345678901234567890")) # True +print(a != 0) # True +``` + +Additional methods: + +```mojo +a.compare(b) # Returns Int8: 1, 0, or -1 +a.compare_magnitudes(b) # Compares |a| vs |b| +``` + +## Bitwise Operations + +All bitwise operations follow **Python's two's complement semantics** for negative numbers. + +| Operator | Description | +| -------- | ---------------------------- | +| `a & b` | Bitwise AND | +| `a \| b` | Bitwise OR | +| `a ^ b` | Bitwise XOR | +| `~a` | Bitwise NOT: $~x = -(x + 1)$ | + +Each accepts both `BInt` and `Int` as the right operand. In-place variants (`&=`, `|=`, `^=`) are also available. + +```mojo +var a = BInt(0b1100) +var b = BInt(0b1010) +print(a & b) # 8 (0b1000) +print(a | b) # 14 (0b1110) +print(a ^ b) # 6 (0b0110) +print(~a) # -13 + +# Negative numbers use two's complement: +print(BInt(-1) & BInt(255)) # 255 +``` + +## Shift Operations + +| Operator | Description | +| -------- | ----------------------------------- | +| `a << n` | Left shift (multiply by $2^n$) | +| `a >> n` | Right shift (floor divide by $2^n$) | + +```mojo +var x = BInt(1) +print(x << 100) # 1267650600228229401496703205376 (= 2^100) +print(BInt(1024) >> 5) # 32 +``` + +## Mathematical Functions + +### Exponentiation + +```mojo +print(BInt(2).power(100)) # 2^100 +print(BInt(2) ** 100) # Same via ** operator +``` + +Both `power(exponent: Int)` and `power(exponent: BigInt)` are supported. The exponent must be non-negative. + +### Integer square root + +```mojo +var x = BInt("100000000000000000000") +print(x.sqrt()) # 10000000000 (largest y such that y² ≤ x) +print(x.isqrt()) # Same as sqrt() +``` + +Raises if the value is negative. + +## Number Theory + +All number-theory operations are available as both **instance methods** and **free functions**: + +```mojo +from decimo import BInt, gcd, lcm, extended_gcd, mod_pow, mod_inverse +``` + +### GCD — Greatest Common Divisor + +```mojo +var a = BInt(48) +var b = BInt(18) +print(a.gcd(b)) # 6 +print(gcd(a, b)) # 6 (free function) +``` + +### LCM — Least Common Multiple + +```mojo +print(BInt(12).lcm(BInt(18))) # 36 +print(lcm(BInt(12), BInt(18))) # 36 +``` + +### Extended GCD + +Returns `(g, x, y)` such that `a*x + b*y = g`: + +```mojo +var result = BInt(35).extended_gcd(BInt(15)) +# result = (5, 1, -2) — since 35×1 + 15×(−2) = 5 +``` + +### Modular Exponentiation + +Computes $(base^{exp}) \mod m$ efficiently without computing the full power: + +```mojo +print(BInt(2).mod_pow(BInt(100), BInt(1000000007))) +print(mod_pow(BInt(2), BInt(100), BInt(1000000007))) # free function +``` + +### Modular Inverse + +Finds $x$ such that $(a \cdot x) \equiv 1 \pmod{m}$: + +```mojo +print(BInt(3).mod_inverse(BInt(7))) # 5 (since 3×5 = 15 ≡ 1 mod 7) +print(mod_inverse(BInt(3), BInt(7))) # 5 +``` + +## Conversion and Output + +### String conversions + +| Method | Example output | +| ---------------------------------- | ------------------- | +| `str(x)` / `String(x)` | `"12345"` | +| `repr(x)` | `'BigInt("12345")'` | +| `x.to_string_with_separators("_")` | `"1_234_567"` | +| `x.to_string_with_separators(",")` | `"1,234,567"` | +| `x.to_hex_string()` | `"0x1A2B3C"` | +| `x.to_binary_string()` | `"0b110101"` | +| `x.to_string(line_width=20)` | Multi-line output | + +### Numeric conversions + +| Method | Description | +| ----------------- | ------------------------------------------------- | +| `int(x)` | Convert to `Int` (raises if exceeds 64-bit range) | +| `float(x)` | Convert to `Float64` (may lose precision) | +| `x.to_bigint10()` | Convert to `BigInt10` (base-10^9) | + +```mojo +var x = BInt("123456789012345678901234567890") +print(x.to_string_with_separators()) # 123_456_789_012_345_678_901_234_567_890 +print(x.to_hex_string()) # 0x... +``` + +## Query Methods + +| Method | Return | Description | +| ---------------------- | ------ | ------------------------------------- | +| `x.is_zero()` | `Bool` | `True` if value is 0 | +| `x.is_negative()` | `Bool` | `True` if value < 0 | +| `x.is_positive()` | `Bool` | `True` if value > 0 | +| `x.is_one()` | `Bool` | `True` if value is 1 | +| `x.bit_length()` | `Int` | Number of bits in the magnitude | +| `x.bit_count()` | `Int` | Population count (number of set bits) | +| `x.number_of_words()` | `Int` | Number of `UInt32` words | +| `x.number_of_digits()` | `Int` | Number of decimal digits | + +```mojo +var x = BInt(13) +print(x.bit_length()) # 4 (13 = 0b1101) +print(x.bit_count()) # 3 (three 1-bits) +print(x.number_of_digits()) # 2 +print(x.is_positive()) # True +``` + +## Constants and Factory Methods + +| Method / Constant | Value | +| ---------------------- | ----- | +| `BInt.zero()` | 0 | +| `BInt.one()` | 1 | +| `BInt.negative_one()` | −1 | +| `BigInt.ZERO` | 0 | +| `BigInt.ONE` | 1 | +| `BigInt.BITS_PER_WORD` | 32 | + +# Part II — BigDecimal (`Decimal`) + +## Overview + +`BigDecimal` (aliases `Decimal`, `BDec`) is an arbitrary-precision decimal type — the Mojo-native equivalent of Python's `decimal.Decimal`. It can represent numbers with unlimited digits and decimal places, making it suitable for financial modeling, scientific computing, and applications where floating-point errors are unacceptable. + +| Property | Value | +| ----------------- | --------------------------------------- | +| Full name | `BigDecimal` | +| Aliases | `Decimal`, `BDec` | +| Internal base | Base-10^9 (each word stores ≤ 9 digits) | +| Default precision | 28 significant digits | +| Python equivalent | `decimal.Decimal` | + +`Decimal`, `BDec`, and `BigDecimal` are all the same type — use whichever you prefer: + +- `Decimal` — familiar to Python users. +- `BDec` — short and concise. +- `BigDecimal` — full explicit name. + +## How Precision Works + +- **Addition, subtraction, multiplication** are always **exact** — no precision loss. +- **Division** and **mathematical functions** (`sqrt`, `ln`, `exp`, etc.) accept an optional `precision` parameter specifying the number of **significant digits** in the result. +- The default precision is **28** significant digits, matching Python's `decimal` module. + +```mojo +var x = Decimal("2") +print(x.sqrt()) # 28 significant digits (default) +print(x.sqrt(precision=100)) # 100 significant digits +print(x.sqrt(precision=1000)) # 1000 significant digits +``` + +> **Note:** The default precision of 28 will be configurable globally in a future version when Mojo supports global variables. + +## Construction + +### From zero + +```mojo +var x = Decimal() # 0 +``` + +### From `Int` + +```mojo +var x = Decimal(42) +var y: Decimal = 100 # Implicit conversion +``` + +### From `String` + +```mojo +var a = Decimal("123456789.123456789") # Plain notation +var b = BDec("1.23E+10") # Scientific notation +var c = Decimal("-0.000001") # Negative +var d = Decimal("1_000_000.50") # Separator support +``` + +### From integral scalars + +```mojo +var x = Decimal(Int64(123456789)) +var y = Decimal(UInt128(99999999999999)) +``` + +Works with all integral SIMD types. **Floating-point scalars are rejected at compile time** — use `from_float()` instead. + +### From floating-point — `from_float()` + +```mojo +var x = Decimal.from_float(3.14159) +var y = BDec.from_float(Float64(2.71828)) +``` + +> **Why no implicit Float64 constructor?** Implicit conversion from float would silently introduce floating-point artifacts (e.g., `0.1` → `0.1000000000000000055...`). The `from_float()` method makes this explicit. + +### From Python — `from_python_decimal()` + +```mojo +from python import Python + +var decimal = Python.import_module("decimal") +var py_dec = decimal.Decimal("123.456") + +var a = BigDecimal.from_python_decimal(py_dec) +var b = BigDecimal(py=py_dec) # Alternative keyword syntax +``` + +### Summary of constructors + +| Constructor | Description | +| ------------------------------------- | ------------------------------- | +| `Decimal()` | Zero | +| `Decimal(value: Int)` | From `Int` (implicit) | +| `Decimal(value: String)` | From string (raises) | +| `Decimal(value: Scalar)` | From integral scalar (implicit) | +| `Decimal.from_float(value)` | From floating-point (raises) | +| `Decimal.from_python_decimal(py_obj)` | From Python `Decimal` (raises) | +| `Decimal(coefficient, scale, sign)` | From raw components | + +## Arithmetic Operations + +Addition, subtraction, and multiplication are always **exact** (no precision loss). + +| Expression | Description | Exact? | +| ------------- | ------------------------------------- | -------------------- | +| `a + b` | Addition | ✓ Always exact | +| `a - b` | Subtraction | ✓ Always exact | +| `a * b` | Multiplication | ✓ Always exact | +| `a / b` | True division (default precision=28) | Rounded to precision | +| `a // b` | Truncated division (toward zero) | ✓ Integer part | +| `a % b` | Truncated modulo | — | +| `a ** b` | Exponentiation (default precision=28) | Rounded to precision | +| `divmod(a,b)` | Returns `(a // b, a % b)` | — | + +Built-in integral types are **implicitly converted** when used in arithmetic: + +```mojo +var c = Decimal("3.14") + 1 # Int → Decimal +var d = Decimal("100") * UInt(8) # UInt → Decimal +``` + +In-place operators (`+=`, `-=`, `*=`) perform true in-place mutation for reduced allocation. + +```mojo +var a = Decimal("123456789.123456789") +var b = Decimal("1234.56789") +print(a + b) # 123458023.691346789 +print(a - b) # 123455554.555566789 +print(a * b) # 152415787654.32099750190521 +``` + +## Division Methods + +Division is the primary operation where precision matters. Decimo provides several variants: + +### `true_divide()` — recommended for decimal division + +```mojo +var a = Decimal("1") +var b = Decimal("3") +print(a.true_divide(b)) # 0.3333333333333333333333333333 (28 digits) +print(a.true_divide(b, precision=50)) # 50 significant digits +print(a.true_divide(b, precision=200)) # 200 significant digits +``` + +### Operator `/` — true division with default precision + +```mojo +var result = a / b # Same as a.true_divide(b, precision=28) +``` + +### Operator `//` — truncated (integer) division + +```mojo +print(Decimal("7") // Decimal("4")) # 1 +print(Decimal("-7") // Decimal("4")) # -1 (toward zero) +``` + +## Comparison + +All six comparison operators are supported: + +```mojo +var a = Decimal("123.456") +var b = Decimal("123.4560") # Same value, different scale +print(a == b) # True (comparison by value) +print(a > 100) # True (Int implicitly converted) +``` + +Additional methods: + +```mojo +a.compare(b) # Returns Int8: 1, 0, or -1 +a.compare_absolute(b) # Compares |a| vs |b| +a.max(b) # Returns the larger value +a.min(b) # Returns the smaller value +``` + +## Rounding and Formatting + +### `round()` — round to decimal places + +```mojo +var x = Decimal("123.456") +print(x.round(2)) # 123.46 (ROUND_HALF_EVEN) +print(x.round(1)) # 123.5 +print(x.round(0)) # 123 +print(x.round(-1)) # 12E+1 +print(x.round(2, ROUND_DOWN)) # 123.45 +print(x.round(2, ROUND_UP)) # 123.46 +``` + +Also works with `round()` builtin: + +```mojo +print(round(Decimal("123.456"), 2)) # 123.46 +``` + +### `quantize()` — match scale of another decimal + +Adjusts the scale (number of decimal places) to match the scale of `exp`. The actual value of `exp` is ignored — only its scale matters. + +```mojo +var x = Decimal("1.2345") +print(x.quantize(Decimal("0.01"))) # 1.23 (2 decimal places) +print(x.quantize(Decimal("0.1"))) # 1.2 (1 decimal place) +print(x.quantize(Decimal("1"))) # 1 (0 decimal places) + +# Currency formatting: +var price = Decimal("19.999") +print(price.quantize(Decimal("0.01"))) # 20.00 +``` + +### `normalize()` — remove trailing zeros + +```mojo +print(Decimal("1.2345000").normalize()) # 1.2345 +``` + +### `__ceil__`, `__floor__`, `__trunc__` + +```mojo +from math import ceil, floor, trunc +print(ceil(Decimal("1.1"))) # 2 +print(floor(Decimal("1.9"))) # 1 +print(trunc(Decimal("-1.9"))) # -1 +``` + +## RoundingMode + +Seven rounding modes are available: + +| Constant | Description | +| ----------------- | --------------------------------------- | +| `ROUND_DOWN` | Truncate toward zero | +| `ROUND_UP` | Round away from zero | +| `ROUND_HALF_UP` | Round half away from zero (traditional) | +| `ROUND_HALF_DOWN` | Round half toward zero | +| `ROUND_HALF_EVEN` | Banker's rounding (default) | +| `ROUND_CEILING` | Round toward +∞ | +| `ROUND_FLOOR` | Round toward −∞ | + +```mojo +var x = Decimal("2.5") +print(x.round(0, ROUND_HALF_UP)) # 3 +print(x.round(0, ROUND_HALF_EVEN)) # 2 (banker's rounding) +print(x.round(0, ROUND_DOWN)) # 2 +print(x.round(0, ROUND_UP)) # 3 +print(x.round(0, ROUND_CEILING)) # 3 +print(x.round(0, ROUND_FLOOR)) # 2 +``` + +## Mathematical Functions — Roots and Powers + +All mathematical functions accept an optional `precision` parameter (default=28). + +### Square root + +```mojo +print(Decimal("2").sqrt()) # 1.414213562373095048801688724 +print(Decimal("2").sqrt(precision=100)) # 100 significant digits +``` + +### Cube root + +```mojo +print(Decimal("27").cbrt()) # 3 +print(Decimal("2").cbrt(precision=50)) +``` + +### Nth root + +```mojo +print(Decimal("256").root(Decimal("8"))) # 2 +print(Decimal("100").root(Decimal("3"))) # 4.641588833612778892... +``` + +### Power / exponentiation + +```mojo +print(Decimal("2").power(Decimal("10"))) # 1024 +print(Decimal("2").power(Decimal("0.5"), precision=50)) # sqrt(2) to 50 digits +print(Decimal("2") ** 10) # 1024 +``` + +## Mathematical Functions — Exponential and Logarithmic + +### Exponential (e^x) + +```mojo +print(Decimal("1").exp()) # e ≈ 2.718281828459045235360287471 +print(Decimal("10").exp(precision=50)) # e^10 to 50 digits +``` + +### Natural logarithm + +```mojo +print(Decimal("10").ln(precision=50)) # ln(10) to 50 digits +``` + +For repeated calls, a `MathCache` can be used to avoid recomputing cached constants: + +```mojo +from decimo.bigdecimal.exponential import MathCache + +var cache = MathCache() +var r1 = x1.ln(100, cache) +var r2 = x2.ln(100, cache) # Reuses cached ln(2) and ln(1.25) +``` + +### Logarithm with arbitrary base + +```mojo +print(Decimal("100").log(Decimal("10"))) # 2 +print(Decimal("8").log(Decimal("2"))) # 3 +``` + +### Base-10 logarithm + +```mojo +print(Decimal("1000").log10()) # 3 (exact for powers of 10) +print(Decimal("2").log10(precision=50)) +``` + +## Mathematical Functions — Trigonometric + +All trigonometric functions take inputs in **radians** and accept an optional `precision` parameter. + +### Basic functions + +```mojo +print(Decimal("0.5").sin(precision=50)) +print(Decimal("0.5").cos(precision=50)) +print(Decimal("0.5").tan(precision=50)) +``` + +### Reciprocal functions + +```mojo +print(Decimal("1").cot(precision=50)) # cos/sin +print(Decimal("1").csc(precision=50)) # 1/sin +print(Decimal("1").sec(precision=50)) # 1/cos +``` + +### Inverse functions + +```mojo +print(Decimal("1").arctan(precision=50)) # π/4 to 50 digits +``` + +## Mathematical Constants + +### π (pi) + +Computed using the **Chudnovsky algorithm** with binary splitting: + +```mojo +print(Decimal.pi(precision=100)) # 100 digits of π +print(Decimal.pi(precision=1000)) # 1000 digits of π +``` + +### e (Euler's number) + +Computed as `exp(1)`: + +```mojo +print(Decimal.e(precision=100)) # 100 digits of e +print(Decimal.e(precision=1000)) # 1000 digits of e +``` + +## Conversion and Output + +### String output + +The `to_string()` method provides flexible formatting: + +```mojo +var x = Decimal("123456789.123456789") +print(x) # 123456789.123456789 +print(x.to_string(scientific=True)) # 1.23456789123456789E+8 +print(x.to_string(engineering=True)) # 123.456789123456789E+6 +print(x.to_string(delimiter="_")) # 123_456_789.123_456_789 +print(x.to_string(line_width=20)) # Multi-line output +print(x.to_string(force_plain=True)) # Suppress auto-scientific notation +``` + +Default output follows CPython's `Decimal.__str__()` rules: plain notation when feasible, scientific notation when there would be more than 6 leading zeros. + +Convenience aliases: + +```mojo +x.to_scientific_string() # to_string(scientific=True) +x.to_eng_string() # to_string(engineering=True) +x.to_string_with_separators("_") # to_string(delimiter="_") +``` + +### `repr()` + +```mojo +print(repr(Decimal("123.45"))) # BigDecimal("123.45") +``` + +### Numeric conversions + +```mojo +var n = Int(Decimal("123.99")) # 123 (truncates) +var f = Float64(Decimal("3.14")) # 3.14 (may lose precision) +``` + +## Query Methods + +| Method | Return | Description | +| ---------------------- | ------ | ----------------------------------------- | +| `x.is_zero()` | `Bool` | `True` if value is zero | +| `x.is_one()` | `Bool` | `True` if value is exactly 1 | +| `x.is_integer()` | `Bool` | `True` if no fractional part | +| `x.is_negative()` | `Bool` | `True` if negative | +| `x.is_positive()` | `Bool` | `True` if strictly positive | +| `x.is_odd()` | `Bool` | `True` if odd integer | +| `x.number_of_digits()` | `Int` | Total digits in coefficient | +| `x.adjusted()` | `Int` | Adjusted exponent (≈ floor(log10(\|x\|))) | +| `x.same_quantum(y)` | `Bool` | `True` if both have same scale | + +### `as_tuple()` — Python-compatible decomposition + +```mojo +var sign, digits, exp = Decimal("7.25").as_tuple() +# sign=False, digits=[7, 2, 5], exp=-2 +``` + +### Other methods + +```mojo +x.copy_abs() # Copy with positive sign +x.copy_negate() # Copy with inverted sign +x.copy_sign(other) # Copy of x with sign of other +x.fma(a, b) # Fused multiply-add: x*a+b (exact) +x.scaleb(n) # Multiply by 10^n (O(1), adjusts scale only) +``` + +## Python Interoperability + +### From Python + +```mojo +from python import Python + +var decimal = Python.import_module("decimal") +var py_val = decimal.Decimal("3.14159265358979323846") + +var d = BigDecimal.from_python_decimal(py_val) +# Or: +var d = BigDecimal(py=py_val) +``` + +### Matching Python's API + +Many methods mirror Python's `decimal.Decimal` API: + +| Python `Decimal` method | Decimo equivalent | +| ----------------------- | ----------------------- | +| `d.quantize(exp)` | `x.quantize(exp)` | +| `d.normalize()` | `x.normalize()` | +| `d.as_tuple()` | `x.as_tuple()` | +| `d.copy_abs()` | `x.copy_abs()` | +| `d.copy_negate()` | `x.copy_negate()` | +| `d.copy_sign(other)` | `x.copy_sign(other)` | +| `d.fma(a, b)` | `x.fma(a, b)` | +| `d.adjusted()` | `x.adjusted()` | +| `d.same_quantum(other)` | `x.same_quantum(other)` | + +## Appendix A — Import Paths + +```mojo +# Recommended: import everything commonly needed +from decimo.prelude import * +# Brings in: BInt, Decimal, BDec, Dec128, RoundingMode, +# ROUND_DOWN, ROUND_HALF_UP, ROUND_HALF_EVEN, ROUND_UP, ROUND_CEILING, ROUND_FLOOR + +# Or import specific types +from decimo import BInt, BigInt +from decimo import Decimal, BDec, BigDecimal +from decimo import RoundingMode + +# Number-theory free functions +from decimo import gcd, lcm, extended_gcd, mod_pow, mod_inverse +``` + +## Appendix B — Traits Implemented + +### BigInt + +| Trait | What it enables | +| ------------------ | -------------------------------- | +| `Absable` | `abs(x)` | +| `Comparable` | `<`, `<=`, `>`, `>=`, `==`, `!=` | +| `Copyable` | Value-semantic copy | +| `Movable` | Move semantics | +| `FloatableRaising` | `Float64(x)` | +| `IntableRaising` | `Int(x)` | +| `Representable` | `repr(x)` | +| `Stringable` | `String(x)`, `str(x)` | +| `Writable` | `print(x)`, writer protocol | + +### BigDecimal + +| Trait | What it enables | +| ------------------ | -------------------------------- | +| `Absable` | `abs(x)` | +| `Comparable` | `<`, `<=`, `>`, `>=`, `==`, `!=` | +| `Copyable` | Value-semantic copy | +| `Movable` | Move semantics | +| `FloatableRaising` | `Float64(x)` | +| `IntableRaising` | `Int(x)` | +| `Representable` | `repr(x)` | +| `Roundable` | `round(x)`, `round(x, ndigits)` | +| `Stringable` | `String(x)`, `str(x)` | +| `Writable` | `print(x)`, writer protocol | + +## Appendix C — Complete API Tables + +### BigInt — All Operators + +| Operator / Method | Accepts | Raises? | Description | +| ------------------- | -------------------- | ------- | ---------------------- | +| `a + b` | `BInt`, `Int` | No | Addition | +| `a - b` | `BInt`, `Int` | No | Subtraction | +| `a * b` | `BInt`, `Int` | No | Multiplication | +| `a // b` | `BInt`, `Int` | Yes | Floor division | +| `a % b` | `BInt`, `Int` | Yes | Floor modulo | +| `a ** b` | `BInt`, `Int` | Yes | Power | +| `a << n` | `Int` | No | Left shift | +| `a >> n` | `Int` | No | Right shift | +| `a & b` | `BInt`, `Int` | No | Bitwise AND | +| `a \| b` | `BInt`, `Int` | No | Bitwise OR | +| `a ^ b` | `BInt`, `Int` | No | Bitwise XOR | +| `~a` | — | No | Bitwise NOT | +| `-a` | — | No | Negation | +| `abs(a)` | — | No | Absolute value | +| `a.sqrt()` | — | Yes | Integer square root | +| `a.gcd(b)` | `BInt` | No | GCD | +| `a.lcm(b)` | `BInt` | Yes | LCM | +| `a.extended_gcd(b)` | `BInt` | Yes | Extended GCD | +| `a.mod_pow(e, m)` | `BInt`/`Int`, `BInt` | Yes | Modular exponentiation | +| `a.mod_inverse(m)` | `BInt` | Yes | Modular inverse | + +### BigDecimal — Mathematical Functions + +| Function | Signature | Default | Description | +| -------- | ---------------------------- | ------- | -------------------- | +| `sqrt` | `x.sqrt(precision=28)` | 28 | Square root | +| `cbrt` | `x.cbrt(precision=28)` | 28 | Cube root | +| `root` | `x.root(n, precision=28)` | 28 | Nth root | +| `power` | `x.power(exp, precision=28)` | 28 | Exponentiation | +| `exp` | `x.exp(precision=28)` | 28 | e^x | +| `ln` | `x.ln(precision=28)` | 28 | Natural logarithm | +| `log` | `x.log(base, precision=28)` | 28 | Logarithm (any base) | +| `log10` | `x.log10(precision=28)` | 28 | Base-10 logarithm | +| `sin` | `x.sin(precision=28)` | 28 | Sine (radians) | +| `cos` | `x.cos(precision=28)` | 28 | Cosine (radians) | +| `tan` | `x.tan(precision=28)` | 28 | Tangent (radians) | +| `cot` | `x.cot(precision=28)` | 28 | Cotangent (radians) | +| `csc` | `x.csc(precision=28)` | 28 | Cosecant (radians) | +| `sec` | `x.sec(precision=28)` | 28 | Secant (radians) | +| `arctan` | `x.arctan(precision=28)` | 28 | Arctangent (radians) | +| `pi` | `Decimal.pi(precision)` | — | Compute π | +| `e` | `Decimal.e(precision)` | — | Compute e | diff --git a/docs/user_manual_cli.md b/docs/user_manual_cli.md new file mode 100644 index 00000000..1f62cb95 --- /dev/null +++ b/docs/user_manual_cli.md @@ -0,0 +1,416 @@ +# Decimo CLI Calculator — User Manual + +> `decimo` — A native arbitrary-precision command-line calculator powered by Decimo and ArgMojo. + +- [Overview](#overview) +- [Installation](#installation) +- [Quick Start](#quick-start) +- [Expression Syntax](#expression-syntax) + - [Numbers](#numbers) + - [Operators](#operators) + - [Operator Precedence](#operator-precedence) + - [Functions](#functions) + - [Constants](#constants) +- [CLI Options](#cli-options) + - [Precision (`--precision`, `-p`)](#precision---precision--p) + - [Scientific Notation (`--scientific`, `-s`)](#scientific-notation---scientific--s) + - [Engineering Notation (`--engineering`, `-e`)](#engineering-notation---engineering--e) + - [Pad to Precision (`--pad`, `-P`)](#pad-to-precision---pad--p) + - [Digit Separator (`--delimiter`, `-d`)](#digit-separator---delimiter--d) + - [Rounding Mode (`--rounding-mode`, `-r`)](#rounding-mode---rounding-mode--r) +- [Shell Integration](#shell-integration) + - [Quoting Expressions](#quoting-expressions) + - [Using noglob](#using-noglob) +- [Examples](#examples) + - [Basic Arithmetic](#basic-arithmetic) + - [High-Precision Calculations](#high-precision-calculations) + - [Mathematical Functions](#mathematical-functions) + - [Output Formatting](#output-formatting) + - [Rounding Modes](#rounding-modes) +- [Error Messages](#error-messages) +- [Full `--help` Reference](#full---help-reference) + +## Overview + +`decimo` is a command-line calculator that supports: + +- **Arbitrary-precision arithmetic** — no limit on the number of digits. +- **High-precision mathematical functions** — `sqrt`, `ln`, `exp`, `sin`, `cos`, `tan`, and more. +- **Mathematical constants** — `pi` and `e` to any number of digits. +- **Output formatting** — scientific, engineering, digit separators, trailing zero padding. +- **Multiple rounding modes** — half-even (banker's), half-up, half-down, up, down, ceiling, floor. + +It compiles to a **single native binary** with zero runtime dependencies. + +## Installation + +Build the CLI from source: + +```bash +cd /path/to/decimo +mojo build -I src -I src/cli src/cli/main.mojo -o decimo +``` + +Then move the binary to a directory in your `$PATH`: + +```bash +mv decimo /usr/local/bin/ +``` + +## Quick Start + +```bash +# Basic arithmetic +decimo "1 + 2 * 3" +# → 7 + +# High-precision division +decimo "1/3" -p 100 +# → 0.3333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333 + +# Square root of 2 to 50 digits +decimo "sqrt(2)" -p 50 +# → 1.4142135623730950488016887242096980785696718753770 + +# 1000 digits of pi +decimo "pi" -p 1000 + +# Large integer exponentiation +decimo "2^256" +# → 115792089237316195423570985008687907853269984665640564039457584007913129639936 +``` + +## Expression Syntax + +### Numbers + +- **Integers:** `42`, `-7`, `1000000` +- **Decimals:** `3.14`, `0.001`, `.5` +- **Negative numbers:** `-3`, `-3.14`, `(-5 + 2)` +- **Unary minus:** `2 * -3`, `sqrt(-1 + 2)` + +### Operators + +| Operator | Description | Example | Result | +| -------- | -------------- | ------------- | ----------- | +| `+` | Addition | `2 + 3` | `5` | +| `-` | Subtraction | `10 - 4` | `6` | +| `*` | Multiplication | `6 * 7` | `42` | +| `/` | True division | `1 / 3` | `0.3333...` | +| `^` | Power | `2 ^ 10` | `1024` | +| `**` | Power (alias) | `2 ** 10` | `1024` | +| `(`, `)` | Grouping | `(2 + 3) * 4` | `20` | + +> **Note:** Division always produces a decimal result. `7 / 2` gives `3.5`, not `3`. + +### Operator Precedence + +From lowest to highest: + +| Precedence | Operators | Associativity | +| :--------: | ---------- | :-----------: | +| 1 (low) | `+`, `-` | Left | +| 2 | `*`, `/` | Left | +| 3 | `^` / `**` | Right | +| 4 (high) | unary `-` | Right | + +Right-associativity of `^` means `2^3^2` = `2^(3^2)` = `2^9` = `512`, not `(2^3)^2` = `64`. + +### Functions + +All functions use the CLI's precision setting (default 50, configurable with `-p`). + +**Single-argument functions:** + +| Function | Description | Example | +| ---------- | ------------------- | --------------------- | +| `sqrt(x)` | Square root | `sqrt(2)` | +| `cbrt(x)` | Cube root | `cbrt(27)` → `3` | +| `abs(x)` | Absolute value | `abs(-5)` → `5` | +| `ln(x)` | Natural logarithm | `ln(e)` → `1` | +| `log10(x)` | Base-10 logarithm | `log10(1000)` → `3` | +| `exp(x)` | Exponential (e^x) | `exp(1)` → `2.718...` | +| `sin(x)` | Sine (radians) | `sin(pi/2)` → `1` | +| `cos(x)` | Cosine (radians) | `cos(0)` → `1` | +| `tan(x)` | Tangent (radians) | `tan(pi/4)` → `1` | +| `cot(x)` | Cotangent (radians) | `cot(pi/4)` → `1` | +| `csc(x)` | Cosecant (radians) | `csc(pi/2)` → `1` | + +**Multi-argument functions:** + +| Function | Description | Example | +| -------------- | ----------------------- | ------------------- | +| `root(x, n)` | Nth root of x | `root(27, 3)` → `3` | +| `log(x, base)` | Logarithm with any base | `log(8, 2)` → `3` | + +Functions can be nested: + +```bash +decimo "sqrt(abs(1.1 * -12 - 23/17))" +decimo "ln(exp(1))" # → 1 +``` + +### Constants + +| Constant | Description | +| -------- | --------------------------- | +| `pi` | π (3.14159...) | +| `e` | Euler's number (2.71828...) | + +Constants are computed to the requested precision: + +```bash +decimo "pi" -p 100 # 100 digits of π +decimo "e" -p 500 # 500 digits of e +decimo "2 * pi * 6371" # Circumference using π +``` + +## CLI Options + +### Precision (`--precision`, `-p`) + +Number of significant digits in the result. Default: **50**. + +```bash +decimo "1/7" -p 10 # 0.1428571429 +decimo "1/7" -p 100 # 100 significant digits +decimo "1/7" -p 200 # 200 significant digits +``` + +### Scientific Notation (`--scientific`, `-s`) + +Output in scientific notation (e.g., `1.23E+10`). + +```bash +decimo "123456789 * 987654321" -s +# → 1.21932631112635269E+17 +``` + +### Engineering Notation (`--engineering`, `-e`) + +Output in engineering notation (exponent is always a multiple of 3). + +```bash +decimo "123456789 * 987654321" -e +# → 121.932631112635269E+15 +``` + +> `--scientific` and `--engineering` are mutually exclusive. + +### Pad to Precision (`--pad`, `-P`) + +Pad trailing zeros so the fractional part has exactly `precision` digits. + +```bash +decimo "1.5" -P -p 10 +# → 1.5000000000 +``` + +### Digit Separator (`--delimiter`, `-d`) + +Insert a character every 3 digits for readability. + +```bash +decimo "2^64" -d _ +# → 18_446_744_073_709_551_616 + +decimo "pi" -p 30 -d _ +# → 3.141_592_653_589_793_238_462_643_383_28 +``` + +### Rounding Mode (`--rounding-mode`, `-r`) + +Choose how the final result is rounded. Default: **half-even** (banker's rounding). + +| Mode | Description | +| ----------- | ----------------------------------- | +| `half-even` | Round to nearest even (default) | +| `half-up` | Round half away from zero | +| `half-down` | Round half toward zero | +| `up` | Always round away from zero | +| `down` | Always round toward zero (truncate) | +| `ceiling` | Round toward +∞ | +| `floor` | Round toward −∞ | + +```bash +decimo "1/6" -p 5 -r half-up # 0.16667 +decimo "1/6" -p 5 -r half-even # 0.16667 +decimo "1/6" -p 5 -r down # 0.16666 +decimo "1/6" -p 5 -r up # 0.16667 +``` + +## Shell Integration + +### Quoting Expressions + +The shell interprets `*`, `(`, `)`, and other characters before `decimo` sees them. **Always wrap expressions in quotes:** + +```bash +# ✓ Correct: quoted +decimo "2 * (3 + 4)" + +# ✗ Wrong: shell may glob or split +decimo 2 * (3 + 4) +``` + +### Using noglob + +On zsh, you can use `noglob` to prevent shell interpretation: + +```bash +noglob decimo 2*(3+4) +``` + +Or set up a permanent alias: + +```bash +# Add to ~/.zshrc: +alias decimo='noglob decimo' + +# Then use without quotes: +decimo 2*(3+4) +``` + +## Examples + +### Basic Arithmetic + +```bash +decimo "100 * 12 - 23/17" +# → 1198.647058823529411764705882352941176470588235294118 + +decimo "(1 + 2) * (3 + 4)" +# → 21 + +decimo "2 ^ 256" +# → 115792089237316195423570985008687907853269984665640564039457584007913129639936 +``` + +### High-Precision Calculations + +```bash +# 200 digits of 1/7 +decimo "1/7" -p 200 + +# π to 1000 digits +decimo "pi" -p 1000 + +# e to 500 digits +decimo "e" -p 500 + +# sqrt(2) to 100 digits +decimo "sqrt(2)" -p 100 +``` + +### Mathematical Functions + +```bash +# Trigonometry +decimo "sin(pi/6)" -p 50 # → 0.5 +decimo "cos(pi/3)" -p 50 # → 0.5 +decimo "tan(pi/4)" -p 50 # → 1 + +# Logarithms +decimo "ln(2)" -p 100 +decimo "log10(1000)" # → 3 +decimo "log(256, 2)" # → 8 + +# Nested functions +decimo "sqrt(abs(1.1 * -12 - 23/17))" -p 30 +decimo "exp(ln(100))" -p 30 # → 100 + +# Cube root +decimo "cbrt(27)" # → 3 +decimo "root(1000000, 6)" # → 10 +``` + +### Output Formatting + +```bash +# Scientific notation +decimo "123456789.987654321" -s +# → 1.23456789987654321E+8 + +# Engineering notation +decimo "123456789.987654321" -e +# → 123.456789987654321E+6 + +# Digit separators +decimo "2^100" -d _ +# → 1_267_650_600_228_229_401_496_703_205_376 + +# Pad trailing zeros +decimo "1/4" -p 20 -P +# → 0.25000000000000000000 +``` + +### Rounding Modes + +```bash +# Compare rounding of 2.5 to 0 decimal places: +decimo "2.5 + 0" -p 1 -r half-even # → 2 (banker's: round to even) +decimo "2.5 + 0" -p 1 -r half-up # → 3 (traditional) +decimo "2.5 + 0" -p 1 -r down # → 2 (truncate) +decimo "2.5 + 0" -p 1 -r ceiling # → 3 (toward +∞) +decimo "2.5 + 0" -p 1 -r floor # → 2 (toward −∞) +``` + +## Error Messages + +The calculator provides clear error diagnostics with position indicators: + +```bash +$ decimo "1 + * 2" +Error: missing operand for '*' + 1 + * 2 + ^ + +$ decimo "sqrt(-1)" +Error: sqrt() is undefined for negative numbers + +$ decimo "1 / 0" +Error: division by zero + +$ decimo "hello + 1" +Error: unknown identifier 'hello' + hello + 1 + ^^^^^ + +$ decimo "2 * (3 + 4" +Error: mismatched parentheses: missing closing ')' +``` + +## Full `--help` Reference + +```txt +Arbitrary-precision CLI calculator powered by Decimo. + +Note: if your expression contains *, ( or ), your shell may +intercept them before decimo runs. Use quotes or noglob: + decimo "2 * (3 + 4)" # with quotes + noglob decimo 2*(3+4) # with noglob + alias decimo='noglob decimo' # add to ~/.zshrc + +Usage: decimo [OPTIONS] + +Arguments: + expr Math expression to evaluate (e.g. 'sqrt(abs(1.1*-12-23/17))') + +Options: + -p, --precision + Number of significant digits (default: 50) + -s, --scientific + Output in scientific notation (e.g. 1.23E+10) + -e, --engineering + Output in engineering notation (exponent multiple of 3) + -P, --pad + Pad trailing zeros to the specified precision + -d, --delimiter + Digit-group separator inserted every 3 digits (e.g. '_' gives 1_234.567_89) + -r, --rounding-mode {half-even,half-up,half-down,up,down,ceiling,floor} + Rounding mode for the final result (default: half-even) + -h, --help + Show this help message + -V, --version + Show version +``` diff --git a/src/decimo/str.mojo b/src/decimo/str.mojo index 9d4ba805..4d7061b5 100644 --- a/src/decimo/str.mojo +++ b/src/decimo/str.mojo @@ -71,6 +71,17 @@ fn parse_numeric_string( End of examples. """ + # [Mojo Miji] + # The sytax of numeric values in EBNF form is as follows: + # number ::= integer | real + # integer ::= sign digit ((separator | epsilon) digit)* + # real ::= integer exponent | decimal (exponent | epsilon) + # decimal ::= sign (( digit ((separator | epsilon) digit)* ) | epsilon) '.' digit ((separator | epsilon) digit)* + # exponent ::= ('e' | 'E') (sign | epsilon) digit ((separator | epsilon) digit)* + # sign ::= '+' | '-' | epsilon + # digit ::= '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' + # separator ::= ' ' | ',' | '_' + var value_bytes = value.as_string_slice().as_bytes() var n = len(value_bytes) From e6b5cbb655083643bf0ef6d5dca4cbac66bd4b2b Mon Sep 17 00:00:00 2001 From: ZHU Yuhao Date: Fri, 6 Mar 2026 21:19:18 +0100 Subject: [PATCH 2/2] Fix typos --- docs/user_manual_cli.md | 20 ++++++++++++++------ src/decimo/str.mojo | 12 ++++++++---- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/docs/user_manual_cli.md b/docs/user_manual_cli.md index 1f62cb95..3d3467be 100644 --- a/docs/user_manual_cli.md +++ b/docs/user_manual_cli.md @@ -199,11 +199,13 @@ decimo "123456789 * 987654321" -e ### Pad to Precision (`--pad`, `-P`) -Pad trailing zeros so the fractional part has exactly `precision` digits. +Pad trailing zeros so the fractional part has exactly `precision` digits after the decimal point. + +When used together with `--precision`, the `precision` value is treated as the number of fractional digits for padding purposes, not as a strict limit on total significant digits. As a result, the formatted number can have more than `precision` significant digits. ```bash decimo "1.5" -P -p 10 -# → 1.5000000000 +# → 1.5000000000 (10 fractional digits, 11 significant digits) ``` ### Digit Separator (`--delimiter`, `-d`) @@ -361,15 +363,19 @@ The calculator provides clear error diagnostics with position indicators: ```bash $ decimo "1 + * 2" -Error: missing operand for '*' +Error: missing operand for '+' 1 + * 2 - ^ + ^ $ decimo "sqrt(-1)" -Error: sqrt() is undefined for negative numbers +Error: sqrt() is undefined for negative numbers (got -1) + sqrt(-1) + ^^^^ $ decimo "1 / 0" Error: division by zero + 1 / 0 + ^ $ decimo "hello + 1" Error: unknown identifier 'hello' @@ -377,7 +383,9 @@ Error: unknown identifier 'hello' ^^^^^ $ decimo "2 * (3 + 4" -Error: mismatched parentheses: missing closing ')' +Error: unmatched '(' + 2 * (3 + 4 + ^ ``` ## Full `--help` Reference diff --git a/src/decimo/str.mojo b/src/decimo/str.mojo index 4d7061b5..7676641c 100644 --- a/src/decimo/str.mojo +++ b/src/decimo/str.mojo @@ -72,15 +72,19 @@ fn parse_numeric_string( """ # [Mojo Miji] - # The sytax of numeric values in EBNF form is as follows: + # The syntax of numeric values in EBNF form is as follows: # number ::= integer | real - # integer ::= sign digit ((separator | epsilon) digit)* + # integer ::= (sign | epsilon) digit ((separator | epsilon) digit)* # real ::= integer exponent | decimal (exponent | epsilon) - # decimal ::= sign (( digit ((separator | epsilon) digit)* ) | epsilon) '.' digit ((separator | epsilon) digit)* + # decimal ::= (sign | epsilon) ((digit ((separator | epsilon) digit)*) | epsilon) '.' (digit ((separator | epsilon) digit)* | epsilon) + # # At least one digit must appear either before or after the decimal point. # exponent ::= ('e' | 'E') (sign | epsilon) digit ((separator | epsilon) digit)* - # sign ::= '+' | '-' | epsilon + # sign ::= '+' | '-' # digit ::= '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' # separator ::= ' ' | ',' | '_' + # # Separator characters are ignored by the parser. In particular: + # # - Space ' ' may appear anywhere in the string and is ignored. + # # - Comma ',' and underscore '_' may appear anywhere between digits and are ignored. var value_bytes = value.as_string_slice().as_bytes() var n = len(value_bytes)