Compare commits
4 commits
5c07aad495
...
cbfb411bb6
Author | SHA1 | Date | |
---|---|---|---|
cbfb411bb6 | |||
cc592b358b | |||
961096d42e | |||
|
8989637fb4 |
7 changed files with 242 additions and 11 deletions
|
@ -1,2 +1,2 @@
|
|||
gleam 1.0.0
|
||||
gleam 1.4.1
|
||||
erlang 26.1.2
|
||||
|
|
|
@ -1,3 +1,9 @@
|
|||
3.1.0
|
||||
-----
|
||||
|
||||
+ Added bigi.from_bytes() and bigi.to_bytes() functions.
|
||||
Thanks to Richard Viney.
|
||||
|
||||
3.0.0
|
||||
-----
|
||||
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
name = "bigi"
|
||||
version = "3.0.0"
|
||||
version = "3.1.0"
|
||||
|
||||
# Fill out these fields if you intend to generate HTML documentation or publish
|
||||
# your project to the Hex package manager.
|
||||
#
|
||||
description = "Arbitrary precision integer arithmetic for Gleam"
|
||||
licences = ["MIT"]
|
||||
repository = { type = "gitlab", user = "Nicd", repo = "bigi" }
|
||||
repository = { type = "forgejo", host = "git.ahlcode.fi", user = "nicd", repo = "bigi" }
|
||||
#
|
||||
# For a full reference of all the available options, you can have a look at
|
||||
# https://gleam.run/writing-gleam/gleam-toml/.
|
||||
|
|
|
@ -1,9 +1,23 @@
|
|||
import gleam/order
|
||||
import gleam/dynamic
|
||||
import gleam/order
|
||||
|
||||
/// A big integer.
|
||||
pub type BigInt
|
||||
|
||||
/// Endianness specifier used when converting between a big integer and
|
||||
/// raw bytes.
|
||||
pub type Endianness {
|
||||
LittleEndian
|
||||
BigEndian
|
||||
}
|
||||
|
||||
/// Signedness specifier used when converting between a big integer and
|
||||
/// raw bytes.
|
||||
pub type Signedness {
|
||||
Signed
|
||||
Unsigned
|
||||
}
|
||||
|
||||
/// Create a big integer representing zero.
|
||||
@external(erlang, "bigi_ffi", "zero")
|
||||
@external(javascript, "./bigi_ffi.mjs", "zero")
|
||||
|
@ -30,6 +44,18 @@ pub fn from_int(int: Int) -> BigInt
|
|||
@external(javascript, "./bigi_ffi.mjs", "from_string")
|
||||
pub fn from_string(str: String) -> Result(BigInt, Nil)
|
||||
|
||||
/// Convert raw bytes into a big integer.
|
||||
///
|
||||
/// If the bit array does not contain a whole number of bytes then an error is
|
||||
/// returned.
|
||||
@external(erlang, "bigi_ffi", "from_bytes")
|
||||
@external(javascript, "./bigi_ffi.mjs", "from_bytes")
|
||||
pub fn from_bytes(
|
||||
bytes: BitArray,
|
||||
endianness: Endianness,
|
||||
signedness: Signedness,
|
||||
) -> Result(BigInt, Nil)
|
||||
|
||||
/// Convert a big integer to a regular integer.
|
||||
///
|
||||
/// In Erlang, this cannot fail, as all Erlang integers are big integers. In the
|
||||
|
@ -46,6 +72,20 @@ pub fn to_int(bigint: BigInt) -> Result(Int, Nil)
|
|||
@external(javascript, "./bigi_ffi.mjs", "to_string")
|
||||
pub fn to_string(bigint: BigInt) -> String
|
||||
|
||||
/// Convert a big integer to raw bytes.
|
||||
///
|
||||
/// The size of the returned bit array is specified by `byte_count`, e.g. 8 will
|
||||
/// return a bit array containing 8 bytes (64 bits). If the big integer doesn't
|
||||
/// fit in the specified number of bytes then an error is returned.
|
||||
@external(erlang, "bigi_ffi", "to_bytes")
|
||||
@external(javascript, "./bigi_ffi.mjs", "to_bytes")
|
||||
pub fn to_bytes(
|
||||
bigint: BigInt,
|
||||
endianness: Endianness,
|
||||
signedness: Signedness,
|
||||
byte_count: Int,
|
||||
) -> Result(BitArray, Nil)
|
||||
|
||||
/// Compare two big integers, returning an order that denotes if `a` is lower,
|
||||
/// bigger than, or equal to `b`.
|
||||
@external(erlang, "bigi_ffi", "compare")
|
||||
|
|
|
@ -3,7 +3,9 @@
|
|||
-export([
|
||||
from/1,
|
||||
from_string/1,
|
||||
from_bytes/3,
|
||||
to/1,
|
||||
to_bytes/4,
|
||||
zero/0,
|
||||
compare/2,
|
||||
add/2,
|
||||
|
@ -27,8 +29,60 @@ from_string(Str) ->
|
|||
{Int, _} -> {ok, Int}
|
||||
end.
|
||||
|
||||
from_bytes(Bytes, Endianness, Signedness) ->
|
||||
BitSize = erlang:bit_size(Bytes),
|
||||
|
||||
case BitSize rem 8 of
|
||||
0 -> case Endianness of
|
||||
little_endian -> case Signedness of
|
||||
signed ->
|
||||
<<Int:BitSize/little-signed-integer>> = Bytes,
|
||||
{ok, Int};
|
||||
unsigned ->
|
||||
<<Int:BitSize/little-unsigned-integer>> = Bytes,
|
||||
{ok, Int}
|
||||
end;
|
||||
big_endian -> case Signedness of
|
||||
signed ->
|
||||
<<Int:BitSize/big-signed-integer>> = Bytes,
|
||||
{ok, Int};
|
||||
unsigned ->
|
||||
<<Int:BitSize/big-unsigned-integer>> = Bytes,
|
||||
{ok, Int}
|
||||
end
|
||||
end;
|
||||
|
||||
_ -> {error, nil}
|
||||
end.
|
||||
|
||||
to(BigInt) -> {ok, BigInt}.
|
||||
|
||||
to_bytes(BigInt, Endianness, Signedness, ByteCount) ->
|
||||
case ByteCount * 8 of
|
||||
BitCount when BitCount >= 8 ->
|
||||
RangeMin = case Signedness of
|
||||
signed -> -(1 bsl (BitCount - 1));
|
||||
unsigned -> 0
|
||||
end,
|
||||
|
||||
RangeMax = case Signedness of
|
||||
signed -> (1 bsl (BitCount - 1)) - 1;
|
||||
unsigned -> (1 bsl BitCount) - 1
|
||||
end,
|
||||
|
||||
% Error if the value is out of range for the available bits
|
||||
case BigInt >= RangeMin andalso BigInt =< RangeMax of
|
||||
true ->
|
||||
case Endianness of
|
||||
little_endian -> {ok, <<BigInt:BitCount/little-integer>> };
|
||||
big_endian -> {ok, <<BigInt:BitCount/big-integer>>}
|
||||
end;
|
||||
|
||||
false -> {error, nil}
|
||||
end;
|
||||
|
||||
_ -> {error, nil}
|
||||
end.
|
||||
zero() -> 0.
|
||||
|
||||
compare(A, B) when A < B -> lt;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Ok, Error, toList } from "./gleam.mjs";
|
||||
import { Ok, Error, toList, BitArray } from "./gleam.mjs";
|
||||
import { Lt, Eq, Gt } from "../gleam_stdlib/gleam/order.mjs";
|
||||
import { DecodeError } from "../gleam_stdlib/gleam/dynamic.mjs";
|
||||
import { BigEndian, Signed } from "./bigi.mjs";
|
||||
|
||||
export function from(int) {
|
||||
return BigInt(int);
|
||||
|
@ -14,6 +15,35 @@ export function from_string(string) {
|
|||
}
|
||||
}
|
||||
|
||||
export function from_bytes(bit_array, endianness, signedness) {
|
||||
let value = 0n;
|
||||
|
||||
// Read bytes as an unsigned integer value
|
||||
if (endianness instanceof BigEndian) {
|
||||
for (let i = 0; i < bit_array.length; i++) {
|
||||
value = value * 256n + BigInt(bit_array.byteAt(i));
|
||||
}
|
||||
} else {
|
||||
for (let i = bit_array.length - 1; i >= 0; i--) {
|
||||
value = value * 256n + BigInt(bit_array.byteAt(i));
|
||||
}
|
||||
}
|
||||
|
||||
if (signedness instanceof Signed) {
|
||||
const byteSize = BigInt(bit_array.length);
|
||||
|
||||
const highBit = 2n ** (byteSize * 8n - 1n);
|
||||
|
||||
// If the high bit is set and this is a signed integer, reinterpret as
|
||||
// two's complement
|
||||
if (value >= highBit) {
|
||||
value -= highBit * 2n;
|
||||
}
|
||||
}
|
||||
|
||||
return new Ok(value);
|
||||
}
|
||||
|
||||
export function to(bigint) {
|
||||
if (bigint > Number.MAX_SAFE_INTEGER || bigint < Number.MIN_SAFE_INTEGER) {
|
||||
return new Error(undefined);
|
||||
|
@ -26,6 +56,52 @@ export function to_string(bigint) {
|
|||
return bigint.toString();
|
||||
}
|
||||
|
||||
export function to_bytes(bigint, endianness, signedness, byte_count) {
|
||||
const bit_count = BigInt(byte_count * 8);
|
||||
|
||||
if (bit_count < 8n) {
|
||||
return new Error(undefined);
|
||||
}
|
||||
|
||||
let range_min = 0n;
|
||||
let range_max = 0n;
|
||||
|
||||
// Error if the value is out of range for the available bits
|
||||
if (signedness instanceof Signed) {
|
||||
range_min = -(2n ** (bit_count - 1n));
|
||||
range_max = -range_min - 1n;
|
||||
} else {
|
||||
range_max = 2n ** bit_count - 1n;
|
||||
}
|
||||
|
||||
if (bigint < range_min || bigint > range_max) {
|
||||
return new Error(undefined);
|
||||
}
|
||||
|
||||
// Convert negative number to two's complement representation
|
||||
if (bigint < 0) {
|
||||
bigint = (1n << bit_count) + bigint;
|
||||
}
|
||||
|
||||
const byteArray = new Uint8Array(byte_count);
|
||||
|
||||
if (endianness instanceof BigEndian) {
|
||||
for (let i = byteArray.length - 1; i >= 0; i--) {
|
||||
const byte = bigint % 256n;
|
||||
byteArray[i] = Number(byte);
|
||||
bigint = (bigint - byte) / 256n;
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < byteArray.length; i++) {
|
||||
const byte = bigint % 256n;
|
||||
byteArray[i] = Number(byte);
|
||||
bigint = (bigint - byte) / 256n;
|
||||
}
|
||||
}
|
||||
|
||||
return new Ok(new BitArray(byteArray));
|
||||
}
|
||||
|
||||
export function zero() {
|
||||
return 0n;
|
||||
}
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import gleam/order
|
||||
import bigi
|
||||
import gleam/bit_array
|
||||
import gleam/dynamic
|
||||
import gleam/list
|
||||
import gleam/order
|
||||
import gleeunit
|
||||
import gleeunit/should
|
||||
import bigi
|
||||
|
||||
const js_max_safe_int = 9_007_199_254_740_991
|
||||
|
||||
|
@ -16,7 +18,7 @@ pub fn main() {
|
|||
pub fn zero_test() {
|
||||
should.equal(
|
||||
bigi.zero()
|
||||
|> bigi.to_int(),
|
||||
|> bigi.to_int(),
|
||||
Ok(0),
|
||||
)
|
||||
}
|
||||
|
@ -35,7 +37,7 @@ pub fn absolute_test() {
|
|||
pub fn add_test() {
|
||||
should.equal(
|
||||
bigi.add(bigi.from_int(js_max_safe_int), bigi.from_int(2))
|
||||
|> bigi.to_string(),
|
||||
|> bigi.to_string(),
|
||||
"9007199254740993",
|
||||
)
|
||||
}
|
||||
|
@ -43,7 +45,7 @@ pub fn add_test() {
|
|||
pub fn subtract_test() {
|
||||
should.equal(
|
||||
bigi.subtract(bigi.from_int(js_min_safe_int), bigi.from_int(2))
|
||||
|> bigi.to_string(),
|
||||
|> bigi.to_string(),
|
||||
"-9007199254740993",
|
||||
)
|
||||
}
|
||||
|
@ -54,7 +56,7 @@ pub fn multiply_test() {
|
|||
bigi.from_int(js_max_safe_int),
|
||||
bigi.from_int(js_max_safe_int),
|
||||
)
|
||||
|> bigi.to_string(),
|
||||
|> bigi.to_string(),
|
||||
"81129638414606663681390495662081",
|
||||
)
|
||||
}
|
||||
|
@ -174,6 +176,59 @@ pub fn from_bad_string_test() {
|
|||
should.be_error(parsed)
|
||||
}
|
||||
|
||||
pub fn from_bytes_test() {
|
||||
[
|
||||
#("4328719365", <<1, 2, 3, 4, 5>>, bigi.BigEndian, bigi.Unsigned),
|
||||
#(
|
||||
"-4685255690516759574262",
|
||||
<<255, 2, 3, 4, 5, 6, 7, 8, 9, 10>>,
|
||||
bigi.BigEndian,
|
||||
bigi.Signed,
|
||||
),
|
||||
#("21542142465", <<1, 2, 3, 4, 5>>, bigi.LittleEndian, bigi.Unsigned),
|
||||
#(
|
||||
"-4555767348510506941951",
|
||||
<<1, 2, 3, 4, 5, 6, 7, 8, 9, 255>>,
|
||||
bigi.LittleEndian,
|
||||
bigi.Signed,
|
||||
),
|
||||
]
|
||||
|> list.each(fn(x) {
|
||||
let #(bigi_string, bytes, endianness, signedness) = x
|
||||
|
||||
let assert Ok(i) = bigi.from_string(bigi_string)
|
||||
|
||||
bigi.from_bytes(bytes, endianness, signedness)
|
||||
|> should.equal(Ok(i))
|
||||
|
||||
bigi.to_bytes(i, endianness, signedness, bit_array.byte_size(bytes))
|
||||
|> should.equal(Ok(bytes))
|
||||
})
|
||||
|
||||
// Check error is returned when the integer is out of range
|
||||
bigi.to_bytes(
|
||||
bigi.from_int(4_294_967_296),
|
||||
bigi.LittleEndian,
|
||||
bigi.Unsigned,
|
||||
4,
|
||||
)
|
||||
|> should.equal(Error(Nil))
|
||||
|
||||
bigi.to_bytes(bigi.from_int(-1), bigi.LittleEndian, bigi.Unsigned, 4)
|
||||
|> should.equal(Error(Nil))
|
||||
|
||||
bigi.to_bytes(bigi.from_int(2_147_483_648), bigi.LittleEndian, bigi.Signed, 4)
|
||||
|> should.equal(Error(Nil))
|
||||
|
||||
bigi.to_bytes(
|
||||
bigi.from_int(-2_147_483_649),
|
||||
bigi.LittleEndian,
|
||||
bigi.Signed,
|
||||
4,
|
||||
)
|
||||
|> should.equal(Error(Nil))
|
||||
}
|
||||
|
||||
pub fn power_test() {
|
||||
let assert Ok(bigint) = bigi.power(bigi.from_int(2), bigi.from_int(65_535))
|
||||
|
||||
|
|
Loading…
Reference in a new issue