diff --git a/README.md b/README.md index 73cb276..0bed1b5 100644 --- a/README.md +++ b/README.md @@ -3,14 +3,44 @@ [![Package Version](https://img.shields.io/hexpm/v/bigi)](https://hex.pm/packages/bigi) [![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/bigi/) +Arbitrary precision integer arithmetic for Gleam. + +In the Erlang target, all integers are automatically "big integers" that are subject to +arbitrary precision arithmetic. This means there is no need for this package if you are only +targeting Erlang. + +In JavaScript, Gleam's `Int` type corresponds to the `number` type, which is implemented using +floating-point arithmetic. That means it's subject to the following limits: + +- [Maximum safe integer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER) + and + [minimum safe integer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MIN_SAFE_INTEGER) + beyond which the accuracy of arithmetic suffers, and +- [maximum value](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_VALUE) + and + [minimum value](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MIN_VALUE) + beyond which the numbers are converted to `Infinity` or `-Infinity`. + +This package thus provides big integers for the JavaScript target and additionally provides a +consistent interface for packages that target both Erlang and JavaScript. In Erlang, regular +integers are used. In JavaScript, the +[`BigInt`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt) +type is used. + ```sh gleam add bigi ``` + ```gleam import bigi pub fn main() { - // TODO: An example of the project in use + bigi.power( + bigi.from_int(2), + bigi.from_int(65_535) + ) + + // 1001764965203423232489536175780127875223912737784875709632508486855447029778... } ``` diff --git a/gleam.toml b/gleam.toml index 5d2906c..ccc6c67 100644 --- a/gleam.toml +++ b/gleam.toml @@ -12,6 +12,8 @@ version = "1.0.0" # For a full reference of all the available options, you can have a look at # https://gleam.run/writing-gleam/gleam-toml/. -[dev-dependencies] +[dependencies] gleam_stdlib = "~> 0.34 or ~> 1.0" + +[dev-dependencies] gleeunit = "~> 1.0" diff --git a/src/bigi.gleam b/src/bigi.gleam index b9a5af4..0bfed49 100644 --- a/src/bigi.gleam +++ b/src/bigi.gleam @@ -1,45 +1,138 @@ +import gleam/order + +/// A big integer. pub type BigInt -@external(erlang, "bigi_ffi", "from") -@external(javascript, "./bigi_ffi.mjs", "from") -pub fn from_int(int: Int) -> BigInt - -@external(erlang, "bigi_ffi", "to") -@external(javascript, "./bigi_ffi.mjs", "to") -pub fn to_int(bigint: BigInt) -> Result(Int, Nil) - -@external(erlang, "bigi_ffi", "to_string") -@external(javascript, "./bigi_ffi.mjs", "to_string") -pub fn to_string(bigint: BigInt) -> String - +/// Create a big integer representing zero. @external(erlang, "bigi_ffi", "zero") @external(javascript, "./bigi_ffi.mjs", "zero") pub fn zero() -> BigInt +/// Create a big integer from a regular integer. +@external(erlang, "bigi_ffi", "from") +@external(javascript, "./bigi_ffi.mjs", "from") +pub fn from_int(int: Int) -> BigInt + +/// Convert the big integer to a regular integer. +/// +/// In Erlang, this cannot fail, as all Erlang integers are big integers. In the +/// JavaScript target, this will fail if the integer is bigger than the +/// [maximum safe integer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER) +/// or smaller than the +/// [minimum safe integer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MIN_SAFE_INTEGER). +@external(erlang, "bigi_ffi", "to") +@external(javascript, "./bigi_ffi.mjs", "to") +pub fn to_int(bigint: BigInt) -> Result(Int, Nil) + +/// Convert the big integer into a simple string - a sequence of digits. +@external(erlang, "erlang", "integer_to_binary") +@external(javascript, "./bigi_ffi.mjs", "to_string") +pub fn to_string(bigint: BigInt) -> String + +/// Compare two big integers, returning an order that denotes if `b` is lower, +/// bigger than, or equal to `a`. +@external(erlang, "bigi_ffi", "compare") +@external(javascript, "./bigi_ffi.mjs", "compare") +pub fn compare(a: BigInt, with b: BigInt) -> order.Order + +/// Get the absolute value of a big integer. +@external(erlang, "erlang", "abs") +@external(javascript, "./bigi_ffi.mjs", "absolute") +pub fn absolute(bigint: BigInt) -> BigInt + +/// Add two big integers together. @external(erlang, "bigi_ffi", "add") @external(javascript, "./bigi_ffi.mjs", "add") pub fn add(a: BigInt, b: BigInt) -> BigInt +/// Subtract the subtrahend from the minuend. @external(erlang, "bigi_ffi", "subtract") @external(javascript, "./bigi_ffi.mjs", "subtract") -pub fn subtract(a: BigInt, b: BigInt) -> BigInt +pub fn subtract(minuend a: BigInt, subtrahend b: BigInt) -> BigInt +/// Multiply two big integers together. @external(erlang, "bigi_ffi", "multiply") @external(javascript, "./bigi_ffi.mjs", "multiply") pub fn multiply(a: BigInt, b: BigInt) -> BigInt +/// Divide the dividend with the divisor using integer division. +/// +/// Follows the standard Gleam divide-by-zero rule of 0 when the divisor is 0. @external(erlang, "bigi_ffi", "divide") @external(javascript, "./bigi_ffi.mjs", "divide") -pub fn divide(a: BigInt, b: BigInt) -> BigInt +pub fn divide(dividend a: BigInt, divisor b: BigInt) -> BigInt +/// Divide the dividend with the divisor using integer division. +/// +/// Returns an error if the divisor is 0. +@external(erlang, "bigi_ffi", "divide_no_zero") +@external(javascript, "./bigi_ffi.mjs", "divide_no_zero") +pub fn divide_no_zero( + dividend a: BigInt, + divisor b: BigInt, +) -> Result(BigInt, Nil) + +/// Divide the dividend with the divisor using integer division and return the +/// remainder. +/// +/// Follows the standard Gleam divide-by-zero rule of 0 when the divisor is 0. @external(erlang, "bigi_ffi", "remainder") @external(javascript, "./bigi_ffi.mjs", "remainder") -pub fn remainder(a: BigInt, b: BigInt) -> BigInt +pub fn remainder(dividend a: BigInt, divisor b: BigInt) -> BigInt +/// Divide the dividend with the divisor using integer division and return the +/// remainder. +/// +/// Returns an error if the divisor is 0. +@external(erlang, "bigi_ffi", "remainder_no_zero") +@external(javascript, "./bigi_ffi.mjs", "remainder_no_zero") +pub fn remainder_no_zero( + dividend a: BigInt, + divisor b: BigInt, +) -> Result(BigInt, Nil) + +/// Calculate a mathematical modulo operation. +/// +/// Follows the standard Gleam divide-by-zero rule of 0 when the divisor is 0. @external(erlang, "bigi_ffi", "modulo") @external(javascript, "./bigi_ffi.mjs", "modulo") -pub fn modulo(a: BigInt, b: BigInt) -> BigInt +pub fn modulo(dividend a: BigInt, divisor b: BigInt) -> BigInt +/// Calculate a mathematical modulo operation. +/// +/// Returns an error if the divisor is 0. +@external(erlang, "bigi_ffi", "modulo_no_zero") +@external(javascript, "./bigi_ffi.mjs", "modulo_no_zero") +pub fn modulo_no_zero( + dividend a: BigInt, + divisor b: BigInt, +) -> Result(BigInt, Nil) + +/// Raise the base to the exponent. @external(erlang, "bigi_ffi", "power") @external(javascript, "./bigi_ffi.mjs", "power") -pub fn power(a: BigInt, b: BigInt) -> BigInt +pub fn power(base a: BigInt, exponent b: BigInt) -> BigInt + +/// Get the digits in a given bigint as a list of integers. +/// +/// The list is ordered starting from the most significant digit. +pub fn digits(bigint: BigInt) { + let divisor = from_int(10) + get_digit(bigint, [], divisor) +} + +fn get_digit(bigint: BigInt, digits: List(Int), divisor: BigInt) { + case compare(bigint, divisor) { + order.Gt -> { + let assert Ok(digit) = to_int(bigint) + [digit, ..digits] + } + _ -> { + let assert Ok(digit) = + remainder(bigint, divisor) + |> to_int() + let digits = [digit, ..digits] + get_digit(divide(bigint, divisor), digits, divisor) + } + } +} diff --git a/src/bigi_ffi.erl b/src/bigi_ffi.erl index 3c69912..ddfaaf5 100644 --- a/src/bigi_ffi.erl +++ b/src/bigi_ffi.erl @@ -3,14 +3,17 @@ -export([ from/1, to/1, - to_string/1, zero/0, + compare/2, add/2, subtract/2, multiply/2, divide/2, + divide_no_zero/2, remainder/2, + remainder_no_zero/2, modulo/2, + modulo_no_zero/2, power/2 ]). @@ -18,22 +21,36 @@ from(Int) -> Int. to(BigInt) -> {ok, BigInt}. -to_string(BigInt) -> erlang:integer_to_binary(BigInt). - zero() -> 0. +compare(A, B) when A < B -> gt; +compare(A, B) when A > B -> lt; +compare(_, _) -> eq. + add(A, B) -> A + B. subtract(A, B) -> A - B. multiply(A, B) -> A * B. +divide(_, 0) -> 0; divide(A, B) -> A div B. +divide_no_zero(_, 0) -> {error, nil}; +divide_no_zero(A, B) -> {ok, divide(A, B)}. + +remainder(_, 0) -> 0; remainder(A, B) -> A rem B. +remainder_no_zero(_, 0) -> {error, nil}; +remainder_no_zero(A, B) -> {ok, remainder(A, B)}. + +modulo(_, 0) -> 0; modulo(A, B) -> ((A rem B) + B) rem B. +modulo_no_zero(_, 0) -> {error, nil}; +modulo_no_zero(A, B) -> {ok, modulo(A, B)}. + power(_, 0) -> 1; power(A, 1) -> diff --git a/src/bigi_ffi.mjs b/src/bigi_ffi.mjs index 5fd52b8..11864a9 100644 --- a/src/bigi_ffi.mjs +++ b/src/bigi_ffi.mjs @@ -1,4 +1,5 @@ import { Ok, Error } from "./gleam.mjs"; +import { Lt, Eq, Gt } from "../gleam_stdlib/gleam/order.mjs"; export function from(int) { return BigInt(int); @@ -20,6 +21,24 @@ export function zero() { return 0n; } +export function compare(a, b) { + if (a < b) { + return new Gt(); + } else if (a > b) { + return new Lt(); + } else { + return new Eq(); + } +} + +export function absolute(bigint) { + if (bigint < 0) { + return -bigint; + } else { + return bigint; + } +} + export function add(a, b) { return a + b; } @@ -33,17 +52,53 @@ export function multiply(a, b) { } export function divide(a, b) { + if (b === 0n) { + return 0n; + } + return a / b; } +export function divide_no_zero(a, b) { + if (b === 0n) { + return new Error(undefined); + } + + return new Ok(divide(a, b)); +} + export function remainder(a, b) { + if (b === 0n) { + return 0n; + } + return a % b; } +export function remainder_no_zero(a, b) { + if (b === 0n) { + return new Error(undefined); + } + + return new Ok(remainder(a, b)); +} + export function modulo(a, b) { + if (b === 0n) { + return 0n; + } + return ((a % b) + b) % b; } +export function modulo_no_zero(a, b) { + if (b === 0n) { + return new Error(undefined); + } + + return new Ok(modulo(a, b)); +} + export function power(a, b) { return a ** b; } diff --git a/test/bigi_test.gleam b/test/bigi_test.gleam index 1ce0e70..979a6b3 100644 --- a/test/bigi_test.gleam +++ b/test/bigi_test.gleam @@ -1,3 +1,4 @@ +import gleam/order import gleeunit import gleeunit/should import bigi @@ -19,6 +20,17 @@ pub fn zero_test() { ) } +pub fn compare_test() { + should.equal(bigi.compare(bigi.from_int(1), bigi.from_int(2)), order.Gt) + should.equal(bigi.compare(bigi.from_int(2), bigi.from_int(1)), order.Lt) + should.equal(bigi.compare(bigi.from_int(1), bigi.from_int(1)), order.Eq) +} + +pub fn absolute_test() { + should.equal(bigi.absolute(bigi.from_int(1)), bigi.from_int(1)) + should.equal(bigi.absolute(bigi.from_int(-1234)), bigi.from_int(1234)) +} + pub fn add_test() { should.equal( bigi.add(bigi.from_int(js_max_safe_int), bigi.from_int(2)) @@ -59,6 +71,21 @@ pub fn divide_test() { ) } +pub fn divide_zero_test() { + should.equal(bigi.divide(bigi.from_int(1), bigi.zero()), bigi.zero()) +} + +pub fn divide_no_zero_test() { + should.equal( + bigi.divide_no_zero(bigi.from_int(1), bigi.from_int(1)), + Ok(bigi.from_int(1)), + ) +} + +pub fn divide_no_zero_error_test() { + should.be_error(bigi.divide_no_zero(bigi.from_int(1), bigi.zero())) +} + pub fn remainder_test() { should.equal( bigi.remainder(bigi.from_int(-5), bigi.from_int(3)), @@ -66,6 +93,21 @@ pub fn remainder_test() { ) } +pub fn remainder_zero_test() { + should.equal(bigi.remainder(bigi.from_int(1), bigi.zero()), bigi.zero()) +} + +pub fn remainder_no_zero_test() { + should.equal( + bigi.remainder_no_zero(bigi.from_int(1), bigi.from_int(1)), + Ok(bigi.from_int(0)), + ) +} + +pub fn remainder_no_zero_error_test() { + should.be_error(bigi.remainder_no_zero(bigi.from_int(1), bigi.zero())) +} + pub fn modulo_test() { should.equal( bigi.modulo(bigi.from_int(-5), bigi.from_int(3)), @@ -73,6 +115,26 @@ pub fn modulo_test() { ) } +pub fn modulo_zero_test() { + should.equal(bigi.modulo(bigi.from_int(1), bigi.zero()), bigi.zero()) +} + +pub fn modulo_no_zero_test() { + should.equal( + bigi.modulo_no_zero(bigi.from_int(1), bigi.from_int(1)), + Ok(bigi.from_int(0)), + ) +} + +pub fn modulo_no_zero_error_test() { + should.be_error(bigi.modulo_no_zero(bigi.from_int(1), bigi.zero())) +} + +pub fn digits_test() { + let bigint = bigi.power(bigi.from_int(2), bigi.from_int(16)) + should.equal(bigi.digits(bigint), [6, 5, 5, 3, 6]) +} + pub fn power_test() { should.equal( bigi.to_string(bigi.power(bigi.from_int(2), bigi.from_int(65_535))),