Compare commits

..

4 commits

Author SHA1 Message Date
cbfb411bb6 Switch to Forgejo 2024-09-13 08:05:05 +03:00
cc592b358b Publish 3.1.0 2024-09-12 18:53:30 +03:00
961096d42e Merge branch 'byte_conversions' into 'trunk'
Add bigi.from_bytes() and bigi.to_bytes()

Closes #1

See merge request Nicd/bigi!1
2024-09-12 08:45:32 +00:00
Richard Viney
8989637fb4 Add bigi.from_bytes() and bigi.to_bytes() 2024-09-12 13:57:34 +12:00
7 changed files with 242 additions and 11 deletions

View file

@ -1,2 +1,2 @@
gleam 1.0.0
gleam 1.4.1
erlang 26.1.2

View file

@ -1,3 +1,9 @@
3.1.0
-----
+ Added bigi.from_bytes() and bigi.to_bytes() functions.
Thanks to Richard Viney.
3.0.0
-----

View file

@ -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/.

View file

@ -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")

View file

@ -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;

View file

@ -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;
}

View file

@ -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))