Add docs, change second parameter of math operations to bigint

This commit is contained in:
Mikko Ahlroth 2024-02-12 21:50:37 +02:00
parent 3a6031482d
commit ebb333586e
25 changed files with 568 additions and 147 deletions

235
README.md
View file

@ -3,23 +3,232 @@
[![Package Version](https://img.shields.io/hexpm/v/ranged_int)](https://hex.pm/packages/ranged_int)
[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/ranged_int/)
```sh
gleam add ranged_int
```
```gleam
import ranged_int
Type safe ranged integer operations for Gleam.
pub fn main() {
// TODO: An example of the project in use
Some features:
- Builtin types for the most used ranges
- Create custom types for your own ranges in a type safe way
- Big integer arithmetic to avoid loss of precision
- Opt-in support for overflow and underflow for all ranges (with both a minimum and maximum limit)
- Partially limited ranges (only a minimum or maximum limit)
- Generic ranged integers for cases where ranges are not known at compile time
This library uses the [bigi](https://hex.pm/packages/bigi) library for handling big
integers. The API operates on big integers and only a few builtin types have helpers for
working with regular Gleam `Int`s. This is to ensure correctness on both targets by
avoiding the JavaScript
[number type limitations](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number#integer_range_for_number).
When dealing with math operations, it is useful to check the
[bigi documentation](https://hexdocs.pm/bigi), as the math operations merely delegate to
the bigi math functions.
## Overflow
Whenever a ranged integer is created from an unranged big integer, or a math operation
is performed on a ranged integer, the result is either a new ranged integer, or
overflow. In this context overflow includes both overflow and underflow, i.e. the
ranged integer's maximum or minimum limit being broken, respectively.
If wanted, the resulting `utils.Overflow` type can then be passed on to the
`interface.overflow` or `interface.eject` functions to decide how to handle the
situation: the former will calculate a new value with the overflow algorithm, and the
latter will return an unranged big integer. An example with `ranged_int/builtin/uint8`,
where these functions are wrapped:
```gleam
let assert Ok(a) = uint8.from_bigint(bigi.from_int(120))
let b = bigi.from_int(160)
uint8.overflow(uint8.add(a, b)) // Uint8(24)
uint8.eject(uint8.add(a, b)) // BigInt(280)
```
### Overflow algorithm
The overflow algorithm is based on how unsigned integer overflow is done in the C
language. In C it is not defined for signed integers, but this library defines it for all
ranged integers. In a nutshell:
1. Shift the range into a positive range, starting from 0.
1. Shift the value to overflow the same amount.
1. Do unsigned integer overflow or underflow.
1. Shift the answer back to the original range.
## The builtin types
In the `ranged_int/builtin` namespace there are builtin types for the most popular
ranges: unsigned and signed integers from 8 to 128 bits, and an unsigned integer of
unlimited size. When implementing your own integer range, you may wish to consult the
sources of these implementations as a template.
## Implementing your own type
If none of the builtin types match your use case, you can create your own type with
custom limits. Creating your own type allows for type safety, for example preventing
accidentally passing a regular `Int` in place of your ranged integer, or mixing different
kinds of ranged integers in the same `List`.
Creating your own type requires that the possible limits are known at compile time. If
you don't know the limits until runtime, you should take a look at the builtin generic
ranged integer.
### Minimum viable product
Let's say you want an integer that tracks the years the band
[Katzenjammer](<https://en.wikipedia.org/wiki/Katzenjammer_(band)>) was active. It's
understandable, those were some good years. Let's make a minimal ranged integer that
goes from 2007 to 2015:
```gleam
import bigi.{type BigInt}
import ranged_int/interface.{type Interface, Interface}
const max_limit = 2015
const min_limit = 2007
pub opaque type Katzen {
Katzen(data: BigInt)
}
```
Further documentation can be found at <https://hexdocs.pm/ranged_int>.
This is just a start, and you can't do anything with this yet. But we define the limits
and an opaque type to represent our data. The type is opaque to prevent anyone from
outside the module touching it, because that would ruin our correctness guarantees.
## Development
Next, let's define the minimal set of functions to operate with the type:
```sh
gleam run # Run the project
gleam test # Run the tests
gleam shell # Run an Erlang shell
```gleam
pub fn to_bigint(value: Katzen) {
value.data
}
pub fn limits() {
interface.overflowable_limits(
bigi.from_int(min_limit),
bigi.from_int(max_limit),
)
}
pub fn from_bigint_unsafe(value: BigInt) {
Katzen(data: value)
}
```
- `to_bigint` converts the ranged integer to an unlimited big integer.
- `limits` sets the limits of the integer. There are helper functions in `interface` for
constructing these limits.
- `from_bigint_unsafe` constructs the value _unsafely_ from an unlimited big integer.
This function is for the library's internal use and not for other use, as it breaks the
guarantees of the library.
Finally, we need an _interface_ structure that tells the library how to use your type:
```gleam
pub const iface: Interface(Katzen, interface.Overflowable) = Interface(
from_bigint_unsafe: from_bigint_unsafe,
to_bigint: to_bigint,
limits: limits,
)
```
The interface contains references to the aforementioned functions, that the library
needs to operate on your integers. The second type parameter defines if your integer can
use the [overflow](#overflow) operation: if `interface.NonOverflowable` was passed, the
type system would prevent that. Note that overflowing is still opt-in, and must be
explicitly called.
Note that [due to a Gleam bug](https://github.com/gleam-lang/gleam/issues/2178), the
functions used in a public constant have to be public, even though `limits` and
`from_bigint_unsafe` would be better suited as private. If you define the constant as
private and define wrapper functions for all the `interface` API functions (described
later), then these functions may also be private.
Now this module is ready to be used:
```gleam
import gleam/order
import bigi
import gleeunit/should
import ranged_int/interface
import ranged_int/utils
import mvp
pub fn mvp_test() {
let assert Ok(a) = interface.from_bigint(bigi.from_int(2007), mvp.iface)
let assert Ok(b) = interface.from_bigint(bigi.from_int(2009), mvp.iface)
let c = interface.compare(a, b, mvp.iface)
should.equal(c, order.Gt)
}
```
We can also do math on the integers:
```gleam
pub fn mvp_add_test() {
let assert Ok(a) = interface.from_bigint(bigi.from_int(2007), mvp.iface)
let b = bigi.from_int(9)
let c = interface.math_op(a, b, mvp.iface, bigi.add)
should.equal(c, Error(utils.DidOverflow(bigi.from_int(1))))
}
```
Here we see that the addition failed because it would have overflowed the allowed range.
### Prettifying the API
Now, `interface.math_op(a, b, mvp.iface, bigi.add)` is really a mouthful. It calls the
`interface` module's generic math operation function, passing the operands, the ranged
integer's interface, and the math operation itself (from `bigi`). It works, but it's not
nice to use, and is prone to mistakes (you could replace `bigi.add` with `bigi.subtract`
and the type system wouldn't be able to help you).
To make the type easier to use, we can wrap the functions of the `interface` module:
```gleam
pub fn from_bigint(value: BigInt) {
interface.from_bigint(value, iface)
}
pub fn add(a: Katzen, b: BigInt) {
interface.math_op(a, b, iface, bigi.add)
}
pub fn compare(a: Katzen, b: Katzen) {
interface.compare(a, b, iface)
}
```
This way, the user can call e.g. `mvp.add(a, b)` directly, without having to mess with
the interface and the underlying math functions. The API is now nicer and safer to use!
Additionally, the interface constant and the `limits` and `from_bigint_unsafe` can be
made private, meaning the only way to use the integer is via the functions you choose to
implement.
We can now see that the integer's usage is simpler:
```gleam
import gleam/order
import bigi
import gleeunit/should
import ranged_int/utils
import readme/mvp2
pub fn mvp2_test() {
let assert Ok(a) = mvp2.from_bigint(bigi.from_int(2007))
let assert Ok(b) = mvp2.from_bigint(bigi.from_int(2009))
let c = mvp2.compare(a, b)
should.equal(c, order.Gt)
}
pub fn mvp2_add_test() {
let assert Ok(a) = mvp2.from_bigint(bigi.from_int(2007))
let b = bigi.from_int(9)
let c = mvp2.add(a, b)
should.equal(c, Error(utils.DidOverflow(bigi.from_int(1))))
}
```
You may wish to check out the sources of the builtin implementations for seeing how they
can be done, and for easier copying of the boilerplate.

View file

@ -4,9 +4,9 @@ version = "1.0.0"
# Fill out these fields if you intend to generate HTML documentation or publish
# your project to the Hex package manager.
#
# description = ""
# licences = ["Apache-2.0"]
# repository = { type = "github", user = "username", repo = "project" }
description = "Type safe ranged integer operations for Gleam."
licences = ["MIT"]
repository = { type = "gitlab", user = "Nicd", repo = "ranged_int" }
# links = [{ title = "Website", href = "https://gleam.run" }]
#
# For a full reference of all the available options, you can have a look at

View file

@ -1 +0,0 @@

View file

@ -1,12 +1,24 @@
//// A generic ranged integer.
////
//// This module is meant for cases where the integer range cannot be known at
//// compile time. Generic ranged integers are less type safe and have lower
//// performance. It's always suggested to use one of the builtin types or to
//// create your own type when you can.
////
//// Any two-operand math operations will use the range of the first operand as
//// the range of the output value.
import bigi.{type BigInt}
import ranged_int/interface.{type Interface, type Overflowable, Interface}
/// The interface of a generic ranged integer.
pub opaque type GenericInterface(overflow_mode) {
GenericInterface(
interface: Interface(RangedInt(overflow_mode), overflow_mode),
)
}
/// A generic ranged integer, carrying its own interface along with the data.
pub opaque type RangedInt(overflow_mode) {
RangedInt(
data: BigInt,
@ -14,16 +26,20 @@ pub opaque type RangedInt(overflow_mode) {
)
}
/// Create from a big integer and a minimum and maximum value, both inclusive.
/// The created integer can be used with `overflow`.
pub fn from_bigint_overflowable(value: BigInt, min min: BigInt, max max: BigInt) {
let iface = gen_overflowable_interface(min, max)
interface.from_bigint(value, iface)
}
/// Create from a big integer and a minimum value, inclusive.
pub fn from_bigint_min(value: BigInt, min min: BigInt) {
let iface = gen_min_interface(min)
interface.from_bigint(value, iface)
}
/// Create from a big integer and a maximum value, inclusive.
pub fn from_bigint_max(value: BigInt, max max: BigInt) {
let iface = gen_max_interface(max)
interface.from_bigint(value, iface)
@ -33,6 +49,8 @@ pub fn to_bigint(int: RangedInt(overflow_mode)) {
int.data
}
/// Get the interface of the integer. This interface is required for `overflow`
/// and `eject`.
pub fn get_interface(int: RangedInt(overflow_mode)) {
GenericInterface(int.interface())
}
@ -41,31 +59,31 @@ pub fn compare(a: RangedInt(overflow_mode), b: RangedInt(overflow_mode)) {
interface.compare(a, b, a.interface())
}
pub fn add(a: RangedInt(overflow_mode), b: RangedInt(overflow_mode)) {
pub fn add(a: RangedInt(overflow_mode), b: BigInt) {
interface.math_op(a, b, a.interface(), bigi.add)
}
pub fn subtract(a: RangedInt(overflow_mode), b: RangedInt(overflow_mode)) {
pub fn subtract(a: RangedInt(overflow_mode), b: BigInt) {
interface.math_op(a, b, a.interface(), bigi.subtract)
}
pub fn multiply(a: RangedInt(overflow_mode), b: RangedInt(overflow_mode)) {
pub fn multiply(a: RangedInt(overflow_mode), b: BigInt) {
interface.math_op(a, b, a.interface(), bigi.multiply)
}
pub fn divide(a: RangedInt(overflow_mode), b: RangedInt(overflow_mode)) {
pub fn divide(a: RangedInt(overflow_mode), b: BigInt) {
interface.math_op(a, b, a.interface(), bigi.divide)
}
pub fn modulo(a: RangedInt(overflow_mode), b: RangedInt(overflow_mode)) {
pub fn modulo(a: RangedInt(overflow_mode), b: BigInt) {
interface.math_op(a, b, a.interface(), bigi.modulo)
}
pub fn remainder(a: RangedInt(overflow_mode), b: RangedInt(overflow_mode)) {
pub fn remainder(a: RangedInt(overflow_mode), b: BigInt) {
interface.math_op(a, b, a.interface(), bigi.remainder)
}
pub fn power(a: RangedInt(overflow_mode), b: RangedInt(overflow_mode)) {
pub fn power(a: RangedInt(overflow_mode), b: BigInt) {
interface.math_op(a, b, a.interface(), bigi.power)
}

View file

@ -23,43 +23,43 @@ pub fn compare(a: Int128, b: Int128) {
interface.compare(a, b, iface)
}
pub fn add(a: Int128, b: Int128) {
pub fn add(a: Int128, b: BigInt) {
interface.math_op(a, b, iface, bigi.add)
}
pub fn subtract(a: Int128, b: Int128) {
pub fn subtract(a: Int128, b: BigInt) {
interface.math_op(a, b, iface, bigi.subtract)
}
pub fn multiply(a: Int128, b: Int128) {
pub fn multiply(a: Int128, b: BigInt) {
interface.math_op(a, b, iface, bigi.multiply)
}
pub fn divide(a: Int128, b: Int128) {
pub fn divide(a: Int128, b: BigInt) {
interface.math_op(a, b, iface, bigi.divide)
}
pub fn divide_no_zero(a: Int128, b: Int128) {
pub fn divide_no_zero(a: Int128, b: BigInt) {
interface.fallible_op(a, b, iface, bigi.divide_no_zero)
}
pub fn modulo(a: Int128, b: Int128) {
pub fn modulo(a: Int128, b: BigInt) {
interface.math_op(a, b, iface, bigi.modulo)
}
pub fn modulo_no_zero(a: Int128, b: Int128) {
pub fn modulo_no_zero(a: Int128, b: BigInt) {
interface.fallible_op(a, b, iface, bigi.modulo_no_zero)
}
pub fn remainder(a: Int128, b: Int128) {
pub fn remainder(a: Int128, b: BigInt) {
interface.math_op(a, b, iface, bigi.remainder)
}
pub fn remainder_no_zero(a: Int128, b: Int128) {
pub fn remainder_no_zero(a: Int128, b: BigInt) {
interface.fallible_op(a, b, iface, bigi.remainder_no_zero)
}
pub fn power(a: Int128, b: Int128) {
pub fn power(a: Int128, b: BigInt) {
interface.math_op(a, b, iface, bigi.power)
}

View file

@ -27,43 +27,43 @@ pub fn compare(a: Int16, b: Int16) {
interface.compare(a, b, iface)
}
pub fn add(a: Int16, b: Int16) {
pub fn add(a: Int16, b: BigInt) {
interface.math_op(a, b, iface, bigi.add)
}
pub fn subtract(a: Int16, b: Int16) {
pub fn subtract(a: Int16, b: BigInt) {
interface.math_op(a, b, iface, bigi.subtract)
}
pub fn multiply(a: Int16, b: Int16) {
pub fn multiply(a: Int16, b: BigInt) {
interface.math_op(a, b, iface, bigi.multiply)
}
pub fn divide(a: Int16, b: Int16) {
pub fn divide(a: Int16, b: BigInt) {
interface.math_op(a, b, iface, bigi.divide)
}
pub fn divide_no_zero(a: Int16, b: Int16) {
pub fn divide_no_zero(a: Int16, b: BigInt) {
interface.fallible_op(a, b, iface, bigi.divide_no_zero)
}
pub fn modulo(a: Int16, b: Int16) {
pub fn modulo(a: Int16, b: BigInt) {
interface.math_op(a, b, iface, bigi.modulo)
}
pub fn modulo_no_zero(a: Int16, b: Int16) {
pub fn modulo_no_zero(a: Int16, b: BigInt) {
interface.fallible_op(a, b, iface, bigi.modulo_no_zero)
}
pub fn remainder(a: Int16, b: Int16) {
pub fn remainder(a: Int16, b: BigInt) {
interface.math_op(a, b, iface, bigi.remainder)
}
pub fn remainder_no_zero(a: Int16, b: Int16) {
pub fn remainder_no_zero(a: Int16, b: BigInt) {
interface.fallible_op(a, b, iface, bigi.remainder_no_zero)
}
pub fn power(a: Int16, b: Int16) {
pub fn power(a: Int16, b: BigInt) {
interface.math_op(a, b, iface, bigi.power)
}

View file

@ -27,43 +27,43 @@ pub fn compare(a: Int32, b: Int32) {
interface.compare(a, b, iface)
}
pub fn add(a: Int32, b: Int32) {
pub fn add(a: Int32, b: BigInt) {
interface.math_op(a, b, iface, bigi.add)
}
pub fn subtract(a: Int32, b: Int32) {
pub fn subtract(a: Int32, b: BigInt) {
interface.math_op(a, b, iface, bigi.subtract)
}
pub fn multiply(a: Int32, b: Int32) {
pub fn multiply(a: Int32, b: BigInt) {
interface.math_op(a, b, iface, bigi.multiply)
}
pub fn divide(a: Int32, b: Int32) {
pub fn divide(a: Int32, b: BigInt) {
interface.math_op(a, b, iface, bigi.divide)
}
pub fn divide_no_zero(a: Int32, b: Int32) {
pub fn divide_no_zero(a: Int32, b: BigInt) {
interface.fallible_op(a, b, iface, bigi.divide_no_zero)
}
pub fn modulo(a: Int32, b: Int32) {
pub fn modulo(a: Int32, b: BigInt) {
interface.math_op(a, b, iface, bigi.modulo)
}
pub fn modulo_no_zero(a: Int32, b: Int32) {
pub fn modulo_no_zero(a: Int32, b: BigInt) {
interface.fallible_op(a, b, iface, bigi.modulo_no_zero)
}
pub fn remainder(a: Int32, b: Int32) {
pub fn remainder(a: Int32, b: BigInt) {
interface.math_op(a, b, iface, bigi.remainder)
}
pub fn remainder_no_zero(a: Int32, b: Int32) {
pub fn remainder_no_zero(a: Int32, b: BigInt) {
interface.fallible_op(a, b, iface, bigi.remainder_no_zero)
}
pub fn power(a: Int32, b: Int32) {
pub fn power(a: Int32, b: BigInt) {
interface.math_op(a, b, iface, bigi.power)
}

View file

@ -23,43 +23,43 @@ pub fn compare(a: Int64, b: Int64) {
interface.compare(a, b, iface)
}
pub fn add(a: Int64, b: Int64) {
pub fn add(a: Int64, b: BigInt) {
interface.math_op(a, b, iface, bigi.add)
}
pub fn subtract(a: Int64, b: Int64) {
pub fn subtract(a: Int64, b: BigInt) {
interface.math_op(a, b, iface, bigi.subtract)
}
pub fn multiply(a: Int64, b: Int64) {
pub fn multiply(a: Int64, b: BigInt) {
interface.math_op(a, b, iface, bigi.multiply)
}
pub fn divide(a: Int64, b: Int64) {
pub fn divide(a: Int64, b: BigInt) {
interface.math_op(a, b, iface, bigi.divide)
}
pub fn divide_no_zero(a: Int64, b: Int64) {
pub fn divide_no_zero(a: Int64, b: BigInt) {
interface.fallible_op(a, b, iface, bigi.divide_no_zero)
}
pub fn modulo(a: Int64, b: Int64) {
pub fn modulo(a: Int64, b: BigInt) {
interface.math_op(a, b, iface, bigi.modulo)
}
pub fn modulo_no_zero(a: Int64, b: Int64) {
pub fn modulo_no_zero(a: Int64, b: BigInt) {
interface.fallible_op(a, b, iface, bigi.modulo_no_zero)
}
pub fn remainder(a: Int64, b: Int64) {
pub fn remainder(a: Int64, b: BigInt) {
interface.math_op(a, b, iface, bigi.remainder)
}
pub fn remainder_no_zero(a: Int64, b: Int64) {
pub fn remainder_no_zero(a: Int64, b: BigInt) {
interface.fallible_op(a, b, iface, bigi.remainder_no_zero)
}
pub fn power(a: Int64, b: Int64) {
pub fn power(a: Int64, b: BigInt) {
interface.math_op(a, b, iface, bigi.power)
}

View file

@ -27,43 +27,43 @@ pub fn compare(a: Int8, b: Int8) {
interface.compare(a, b, iface)
}
pub fn add(a: Int8, b: Int8) {
pub fn add(a: Int8, b: BigInt) {
interface.math_op(a, b, iface, bigi.add)
}
pub fn subtract(a: Int8, b: Int8) {
pub fn subtract(a: Int8, b: BigInt) {
interface.math_op(a, b, iface, bigi.subtract)
}
pub fn multiply(a: Int8, b: Int8) {
pub fn multiply(a: Int8, b: BigInt) {
interface.math_op(a, b, iface, bigi.multiply)
}
pub fn divide(a: Int8, b: Int8) {
pub fn divide(a: Int8, b: BigInt) {
interface.math_op(a, b, iface, bigi.divide)
}
pub fn divide_no_zero(a: Int8, b: Int8) {
pub fn divide_no_zero(a: Int8, b: BigInt) {
interface.fallible_op(a, b, iface, bigi.divide_no_zero)
}
pub fn modulo(a: Int8, b: Int8) {
pub fn modulo(a: Int8, b: BigInt) {
interface.math_op(a, b, iface, bigi.modulo)
}
pub fn modulo_no_zero(a: Int8, b: Int8) {
pub fn modulo_no_zero(a: Int8, b: BigInt) {
interface.fallible_op(a, b, iface, bigi.modulo_no_zero)
}
pub fn remainder(a: Int8, b: Int8) {
pub fn remainder(a: Int8, b: BigInt) {
interface.math_op(a, b, iface, bigi.remainder)
}
pub fn remainder_no_zero(a: Int8, b: Int8) {
pub fn remainder_no_zero(a: Int8, b: BigInt) {
interface.fallible_op(a, b, iface, bigi.remainder_no_zero)
}
pub fn power(a: Int8, b: Int8) {
pub fn power(a: Int8, b: BigInt) {
interface.math_op(a, b, iface, bigi.power)
}

View file

@ -1,3 +1,5 @@
//// An unsigned integer ranging from 0 and upwards.
import bigi.{type BigInt}
import ranged_int/interface.{type Interface, Interface}
@ -25,31 +27,31 @@ pub fn compare(a: Uint, b: Uint) {
interface.compare(a, b, iface)
}
pub fn add(a: Uint, b: Uint) {
pub fn add(a: Uint, b: BigInt) {
interface.math_op(a, b, iface, bigi.add)
}
pub fn subtract(a: Uint, b: Uint) {
pub fn subtract(a: Uint, b: BigInt) {
interface.math_op(a, b, iface, bigi.subtract)
}
pub fn multiply(a: Uint, b: Uint) {
pub fn multiply(a: Uint, b: BigInt) {
interface.math_op(a, b, iface, bigi.multiply)
}
pub fn divide(a: Uint, b: Uint) {
pub fn divide(a: Uint, b: BigInt) {
interface.math_op(a, b, iface, bigi.divide)
}
pub fn modulo(a: Uint, b: Uint) {
pub fn modulo(a: Uint, b: BigInt) {
interface.math_op(a, b, iface, bigi.modulo)
}
pub fn remainder(a: Uint, b: Uint) {
pub fn remainder(a: Uint, b: BigInt) {
interface.math_op(a, b, iface, bigi.remainder)
}
pub fn power(a: Uint, b: Uint) {
pub fn power(a: Uint, b: BigInt) {
interface.math_op(a, b, iface, bigi.power)
}

View file

@ -23,43 +23,43 @@ pub fn compare(a: Uint128, b: Uint128) {
interface.compare(a, b, iface)
}
pub fn add(a: Uint128, b: Uint128) {
pub fn add(a: Uint128, b: BigInt) {
interface.math_op(a, b, iface, bigi.add)
}
pub fn subtract(a: Uint128, b: Uint128) {
pub fn subtract(a: Uint128, b: BigInt) {
interface.math_op(a, b, iface, bigi.subtract)
}
pub fn multiply(a: Uint128, b: Uint128) {
pub fn multiply(a: Uint128, b: BigInt) {
interface.math_op(a, b, iface, bigi.multiply)
}
pub fn divide(a: Uint128, b: Uint128) {
pub fn divide(a: Uint128, b: BigInt) {
interface.math_op(a, b, iface, bigi.divide)
}
pub fn divide_no_zero(a: Uint128, b: Uint128) {
pub fn divide_no_zero(a: Uint128, b: BigInt) {
interface.fallible_op(a, b, iface, bigi.divide_no_zero)
}
pub fn modulo(a: Uint128, b: Uint128) {
pub fn modulo(a: Uint128, b: BigInt) {
interface.math_op(a, b, iface, bigi.modulo)
}
pub fn modulo_no_zero(a: Uint128, b: Uint128) {
pub fn modulo_no_zero(a: Uint128, b: BigInt) {
interface.fallible_op(a, b, iface, bigi.modulo_no_zero)
}
pub fn remainder(a: Uint128, b: Uint128) {
pub fn remainder(a: Uint128, b: BigInt) {
interface.math_op(a, b, iface, bigi.remainder)
}
pub fn remainder_no_zero(a: Uint128, b: Uint128) {
pub fn remainder_no_zero(a: Uint128, b: BigInt) {
interface.fallible_op(a, b, iface, bigi.remainder_no_zero)
}
pub fn power(a: Uint128, b: Uint128) {
pub fn power(a: Uint128, b: BigInt) {
interface.math_op(a, b, iface, bigi.power)
}

View file

@ -27,43 +27,43 @@ pub fn compare(a: Uint16, b: Uint16) {
interface.compare(a, b, iface)
}
pub fn add(a: Uint16, b: Uint16) {
pub fn add(a: Uint16, b: BigInt) {
interface.math_op(a, b, iface, bigi.add)
}
pub fn subtract(a: Uint16, b: Uint16) {
pub fn subtract(a: Uint16, b: BigInt) {
interface.math_op(a, b, iface, bigi.subtract)
}
pub fn multiply(a: Uint16, b: Uint16) {
pub fn multiply(a: Uint16, b: BigInt) {
interface.math_op(a, b, iface, bigi.multiply)
}
pub fn divide(a: Uint16, b: Uint16) {
pub fn divide(a: Uint16, b: BigInt) {
interface.math_op(a, b, iface, bigi.divide)
}
pub fn divide_no_zero(a: Uint16, b: Uint16) {
pub fn divide_no_zero(a: Uint16, b: BigInt) {
interface.fallible_op(a, b, iface, bigi.divide_no_zero)
}
pub fn modulo(a: Uint16, b: Uint16) {
pub fn modulo(a: Uint16, b: BigInt) {
interface.math_op(a, b, iface, bigi.modulo)
}
pub fn modulo_no_zero(a: Uint16, b: Uint16) {
pub fn modulo_no_zero(a: Uint16, b: BigInt) {
interface.fallible_op(a, b, iface, bigi.modulo_no_zero)
}
pub fn remainder(a: Uint16, b: Uint16) {
pub fn remainder(a: Uint16, b: BigInt) {
interface.math_op(a, b, iface, bigi.remainder)
}
pub fn remainder_no_zero(a: Uint16, b: Uint16) {
pub fn remainder_no_zero(a: Uint16, b: BigInt) {
interface.fallible_op(a, b, iface, bigi.remainder_no_zero)
}
pub fn power(a: Uint16, b: Uint16) {
pub fn power(a: Uint16, b: BigInt) {
interface.math_op(a, b, iface, bigi.power)
}

View file

@ -27,43 +27,43 @@ pub fn compare(a: Uint32, b: Uint32) {
interface.compare(a, b, iface)
}
pub fn add(a: Uint32, b: Uint32) {
pub fn add(a: Uint32, b: BigInt) {
interface.math_op(a, b, iface, bigi.add)
}
pub fn subtract(a: Uint32, b: Uint32) {
pub fn subtract(a: Uint32, b: BigInt) {
interface.math_op(a, b, iface, bigi.subtract)
}
pub fn multiply(a: Uint32, b: Uint32) {
pub fn multiply(a: Uint32, b: BigInt) {
interface.math_op(a, b, iface, bigi.multiply)
}
pub fn divide(a: Uint32, b: Uint32) {
pub fn divide(a: Uint32, b: BigInt) {
interface.math_op(a, b, iface, bigi.divide)
}
pub fn divide_no_zero(a: Uint32, b: Uint32) {
pub fn divide_no_zero(a: Uint32, b: BigInt) {
interface.fallible_op(a, b, iface, bigi.divide_no_zero)
}
pub fn modulo(a: Uint32, b: Uint32) {
pub fn modulo(a: Uint32, b: BigInt) {
interface.math_op(a, b, iface, bigi.modulo)
}
pub fn modulo_no_zero(a: Uint32, b: Uint32) {
pub fn modulo_no_zero(a: Uint32, b: BigInt) {
interface.fallible_op(a, b, iface, bigi.modulo_no_zero)
}
pub fn remainder(a: Uint32, b: Uint32) {
pub fn remainder(a: Uint32, b: BigInt) {
interface.math_op(a, b, iface, bigi.remainder)
}
pub fn remainder_no_zero(a: Uint32, b: Uint32) {
pub fn remainder_no_zero(a: Uint32, b: BigInt) {
interface.fallible_op(a, b, iface, bigi.remainder_no_zero)
}
pub fn power(a: Uint32, b: Uint32) {
pub fn power(a: Uint32, b: BigInt) {
interface.math_op(a, b, iface, bigi.power)
}

View file

@ -23,43 +23,43 @@ pub fn compare(a: Uint64, b: Uint64) {
interface.compare(a, b, iface)
}
pub fn add(a: Uint64, b: Uint64) {
pub fn add(a: Uint64, b: BigInt) {
interface.math_op(a, b, iface, bigi.add)
}
pub fn subtract(a: Uint64, b: Uint64) {
pub fn subtract(a: Uint64, b: BigInt) {
interface.math_op(a, b, iface, bigi.subtract)
}
pub fn multiply(a: Uint64, b: Uint64) {
pub fn multiply(a: Uint64, b: BigInt) {
interface.math_op(a, b, iface, bigi.multiply)
}
pub fn divide(a: Uint64, b: Uint64) {
pub fn divide(a: Uint64, b: BigInt) {
interface.math_op(a, b, iface, bigi.divide)
}
pub fn divide_no_zero(a: Uint64, b: Uint64) {
pub fn divide_no_zero(a: Uint64, b: BigInt) {
interface.fallible_op(a, b, iface, bigi.divide_no_zero)
}
pub fn modulo(a: Uint64, b: Uint64) {
pub fn modulo(a: Uint64, b: BigInt) {
interface.math_op(a, b, iface, bigi.modulo)
}
pub fn modulo_no_zero(a: Uint64, b: Uint64) {
pub fn modulo_no_zero(a: Uint64, b: BigInt) {
interface.fallible_op(a, b, iface, bigi.modulo_no_zero)
}
pub fn remainder(a: Uint64, b: Uint64) {
pub fn remainder(a: Uint64, b: BigInt) {
interface.math_op(a, b, iface, bigi.remainder)
}
pub fn remainder_no_zero(a: Uint64, b: Uint64) {
pub fn remainder_no_zero(a: Uint64, b: BigInt) {
interface.fallible_op(a, b, iface, bigi.remainder_no_zero)
}
pub fn power(a: Uint64, b: Uint64) {
pub fn power(a: Uint64, b: BigInt) {
interface.math_op(a, b, iface, bigi.power)
}

View file

@ -27,43 +27,43 @@ pub fn compare(a: Uint8, b: Uint8) {
interface.compare(a, b, iface)
}
pub fn add(a: Uint8, b: Uint8) {
pub fn add(a: Uint8, b: BigInt) {
interface.math_op(a, b, iface, bigi.add)
}
pub fn subtract(a: Uint8, b: Uint8) {
pub fn subtract(a: Uint8, b: BigInt) {
interface.math_op(a, b, iface, bigi.subtract)
}
pub fn multiply(a: Uint8, b: Uint8) {
pub fn multiply(a: Uint8, b: BigInt) {
interface.math_op(a, b, iface, bigi.multiply)
}
pub fn divide(a: Uint8, b: Uint8) {
pub fn divide(a: Uint8, b: BigInt) {
interface.math_op(a, b, iface, bigi.divide)
}
pub fn divide_no_zero(a: Uint8, b: Uint8) {
pub fn divide_no_zero(a: Uint8, b: BigInt) {
interface.fallible_op(a, b, iface, bigi.divide_no_zero)
}
pub fn modulo(a: Uint8, b: Uint8) {
pub fn modulo(a: Uint8, b: BigInt) {
interface.math_op(a, b, iface, bigi.modulo)
}
pub fn modulo_no_zero(a: Uint8, b: Uint8) {
pub fn modulo_no_zero(a: Uint8, b: BigInt) {
interface.fallible_op(a, b, iface, bigi.modulo_no_zero)
}
pub fn remainder(a: Uint8, b: Uint8) {
pub fn remainder(a: Uint8, b: BigInt) {
interface.math_op(a, b, iface, bigi.remainder)
}
pub fn remainder_no_zero(a: Uint8, b: Uint8) {
pub fn remainder_no_zero(a: Uint8, b: BigInt) {
interface.fallible_op(a, b, iface, bigi.remainder_no_zero)
}
pub fn power(a: Uint8, b: Uint8) {
pub fn power(a: Uint8, b: BigInt) {
interface.math_op(a, b, iface, bigi.power)
}

View file

@ -1,48 +1,83 @@
//// The ranged integer "interface".
////
//// This module contains types and functions to be implemented and used to
//// create custom ranged integer types.
////
//// See the README for instructions on how to create your own type with this
//// module.
import gleam/order
import gleam/result
import bigi.{type BigInt}
import ranged_int/limit.{type Limit, NoOverflow, Overflow, Underflow}
import ranged_int/internal/limit.{type Limit, NoOverflow, Overflow, Underflow}
import ranged_int/utils
/// Mark this ranged integer as overflowable. An overflowable integer must have
/// both a minimum and maximum limit.
pub type Overflowable {
Overflowable
}
/// Mark this ranged integer as non-overflowable. A non-overflowable integer
/// can only have a minimum or a maximum limit, but cannot use the `overflow`
/// function.
pub type NonOverflowable {
NonOverflowable
}
/// The minimum and maximum limits of a ranged integer.
pub opaque type Limits(overflow_mode) {
Limits(min: Limit, max: Limit)
}
// Helper type for limits when we know that the integer is overflowable.
type OverflowableLimits {
OverflowableLimits(min: BigInt, max: BigInt)
}
/// Result of math operations: a new ranged integer or an overflow.
///
/// An overflow result may occur even for non-overflowable integers. The
/// difference is that the `overflow` function cannot be called to calculate the
/// overflowed result for a non-overflowable integer.
pub type OpResult(a) =
Result(a, utils.Overflow)
/// The ranged integer interface that needs to be passed to math operations.
///
/// To allow for overflowing, the second type parameter must be `Overflowable`.
pub type Interface(a, overflow_mode) {
Interface(
/// A function to convert the ranged integer into a big integer.
to_bigint: fn(a) -> BigInt,
/// A function to convert a big integer to the ranged integer. No limit
/// checking is done, hence the name "unsafe". This is used internally when
/// the limits have already been checked.
from_bigint_unsafe: fn(BigInt) -> a,
/// A function to return the limits of the ranged integer.
limits: fn() -> Limits(overflow_mode),
)
}
/// Construct limits for an overflowable ranged integer. The given limits are
/// inclusive.
pub fn overflowable_limits(min: BigInt, max: BigInt) -> Limits(Overflowable) {
Limits(min: limit.Bounded(min), max: limit.Bounded(max))
}
/// Construct limits for a non-overflowable ranged integer that only has a
/// maximum limit. The given limit is inclusive.
pub fn max_limit(max: BigInt) -> Limits(NonOverflowable) {
Limits(min: limit.Unbounded, max: limit.Bounded(max))
}
/// Construct limits for a non-overflowable ranged integer that only has a
/// minimum limit. The given limit is inclusive.
pub fn min_limit(min: BigInt) -> Limits(NonOverflowable) {
Limits(min: limit.Bounded(min), max: limit.Unbounded)
}
/// Create a ranged integer from a big integer.
pub fn from_bigint(
value: BigInt,
interface: Interface(a, overflow_mode),
@ -55,6 +90,7 @@ pub fn from_bigint(
}
}
/// Compare two ranged integers.
pub fn compare(
int1: a,
int2: a,
@ -63,16 +99,16 @@ pub fn compare(
bigi.compare(interface.to_bigint(int1), interface.to_bigint(int2))
}
/// Run a math operation on a ranged integer and an unranged big integer.
pub fn math_op(
int1: a,
int2: a,
int2: BigInt,
interface: Interface(a, overflow_mode),
op: fn(BigInt, BigInt) -> BigInt,
) -> OpResult(a) {
let limits = interface.limits()
let bigint1 = interface.to_bigint(int1)
let bigint2 = interface.to_bigint(int2)
let result = op(bigint1, bigint2)
let int1 = interface.to_bigint(int1)
let result = op(int1, int2)
case limit.check_limits(result, min: limits.min, max: limits.max) {
NoOverflow -> Ok(interface.from_bigint_unsafe(result))
Overflow(amount) -> Error(utils.DidOverflow(amount))
@ -80,16 +116,20 @@ pub fn math_op(
}
}
/// Run a math operation that may fail on a ranged integer and an unranged big
/// integer.
///
/// Some functions such as `bigi.divide_no_zero` will return an error if the
/// arguments are invalid. This error is passed upwards by this function.
pub fn fallible_op(
int1: a,
int2: a,
int2: BigInt,
interface: Interface(a, overflow_mode),
op: fn(BigInt, BigInt) -> Result(BigInt, op_err),
) -> Result(OpResult(a), op_err) {
let limits = interface.limits()
let bigint1 = interface.to_bigint(int1)
let bigint2 = interface.to_bigint(int2)
use result <- result.try(op(bigint1, bigint2))
let int1 = interface.to_bigint(int1)
use result <- result.try(op(int1, int2))
Ok(case limit.check_limits(result, min: limits.min, max: limits.max) {
NoOverflow -> Ok(interface.from_bigint_unsafe(result))
Overflow(amount) -> Error(utils.DidOverflow(amount))
@ -97,6 +137,13 @@ pub fn fallible_op(
})
}
/// Map the overflow result of an operation to the integer's allowed range.
///
/// The overflow is calculated using a modulo against the amount of integers in
/// the allowed range. This matches the unsigned integer overflow logic of C/C++
/// but is also defined for signed (negative) values.
///
/// This can only be called for overflowable integers.
pub fn overflow(value: OpResult(a), interface: Interface(a, Overflowable)) {
case value {
Ok(new_value) -> new_value
@ -108,6 +155,8 @@ pub fn overflow(value: OpResult(a), interface: Interface(a, Overflowable)) {
}
}
/// Eject the result of an operation to big integer. The big integer may be
/// outside the range of the original ranged integers.
pub fn eject(value: OpResult(a), interface: Interface(a, overflow_mode)) {
case value {
Ok(new_value) -> interface.to_bigint(new_value)

View file

@ -1,10 +1,15 @@
import bigi.{type BigInt}
/// An overflow would have occurred, i.e. the result did not fit the allowed
/// range.
pub type Overflow {
/// The result was this much higher than the maximum allowed value.
DidOverflow(BigInt)
/// The result was this much lower than the minimum allowed value.
DidUnderflow(BigInt)
}
/// Calculate the overflowed value given an overflow result and the limits.
pub fn overflow(overflow: Overflow, min min: BigInt, max max: BigInt) -> BigInt {
let total_values =
bigi.subtract(max, min)

View file

@ -4,9 +4,21 @@ import ranged_int/builtin/generic
pub fn add_test() {
let assert Ok(a) = generic.from_bigint_min(bigi.zero(), min: bigi.zero())
let assert Ok(b) =
generic.from_bigint_max(bigi.from_int(12), max: bigi.from_int(12))
let interface = generic.get_interface(a)
let c = generic.eject(generic.add(a, b), interface)
let b = bigi.from_int(12)
let iface = generic.get_interface(a)
let c = generic.eject(generic.add(a, b), iface)
should.equal(c, bigi.from_int(12))
}
pub fn overflow_test() {
let assert Ok(a) =
generic.from_bigint_overflowable(
bigi.from_int(-15),
min: bigi.from_int(-284),
max: bigi.from_int(-7),
)
let b = bigi.from_int(12)
let iface = generic.get_interface(a)
let c = generic.overflow(generic.add(a, b), iface)
should.equal(generic.to_bigint(c), bigi.from_int(-281))
}

View file

@ -4,7 +4,7 @@ import ranged_int/builtin/uint16
pub fn add_test() {
let assert Ok(a) = uint16.from_bigint(bigi.zero())
let assert Ok(b) = uint16.from_bigint(bigi.from_int(12))
let b = bigi.from_int(12)
let c = uint16.eject(uint16.add(a, b))
should.equal(c, bigi.from_int(12))
}

View file

@ -4,14 +4,28 @@ import ranged_int/builtin/uint8
pub fn add_test() {
let assert Ok(a) = uint8.from_bigint(bigi.zero())
let assert Ok(b) = uint8.from_bigint(bigi.from_int(12))
let b = bigi.from_int(12)
let c = uint8.eject(uint8.add(a, b))
should.equal(c, bigi.from_int(12))
}
pub fn divide_test() {
let assert Ok(a) = uint8.from_bigint(bigi.from_int(120))
let assert Ok(b) = uint8.from_bigint(bigi.from_int(12))
let b = bigi.from_int(12)
let assert Ok(c) = uint8.divide_no_zero(a, b)
should.equal(uint8.eject(c), bigi.from_int(10))
}
pub fn overflow_test() {
let assert Ok(a) = uint8.from_bigint(bigi.from_int(120))
let b = bigi.from_int(160)
let c = uint8.overflow(uint8.add(a, b))
should.equal(uint8.to_bigint(c), bigi.from_int(24))
}
pub fn underflow_test() {
let assert Ok(a) = uint8.from_bigint(bigi.from_int(120))
let b = bigi.from_int(160)
let c = uint8.overflow(uint8.subtract(a, b))
should.equal(uint8.to_bigint(c), bigi.from_int(216))
}

31
test/readme/mvp.gleam Normal file
View file

@ -0,0 +1,31 @@
import bigi.{type BigInt}
import ranged_int/interface.{type Interface, Interface}
const max_limit = 2015
const min_limit = 2007
pub opaque type Katzen {
Katzen(data: BigInt)
}
pub fn to_bigint(value: Katzen) {
value.data
}
pub fn limits() {
interface.overflowable_limits(
bigi.from_int(min_limit),
bigi.from_int(max_limit),
)
}
pub fn from_bigint_unsafe(value: BigInt) {
Katzen(data: value)
}
pub const iface: Interface(Katzen, interface.Overflowable) = Interface(
from_bigint_unsafe: from_bigint_unsafe,
to_bigint: to_bigint,
limits: limits,
)

43
test/readme/mvp2.gleam Normal file
View file

@ -0,0 +1,43 @@
import bigi.{type BigInt}
import ranged_int/interface.{type Interface, Interface}
const max_limit = 2015
const min_limit = 2007
pub opaque type Katzen {
Katzen(data: BigInt)
}
pub fn to_bigint(value: Katzen) {
value.data
}
fn limits() {
interface.overflowable_limits(
bigi.from_int(min_limit),
bigi.from_int(max_limit),
)
}
fn from_bigint_unsafe(value: BigInt) {
Katzen(data: value)
}
const iface: Interface(Katzen, interface.Overflowable) = Interface(
from_bigint_unsafe: from_bigint_unsafe,
to_bigint: to_bigint,
limits: limits,
)
pub fn add(a: Katzen, b: BigInt) {
interface.math_op(a, b, iface, bigi.add)
}
pub fn from_bigint(value: BigInt) {
interface.from_bigint(value, iface)
}
pub fn compare(a: Katzen, b: Katzen) {
interface.compare(a, b, iface)
}

View file

@ -0,0 +1,19 @@
import gleam/order
import bigi
import gleeunit/should
import ranged_int/utils
import readme/mvp2
pub fn mvp2_test() {
let assert Ok(a) = mvp2.from_bigint(bigi.from_int(2007))
let assert Ok(b) = mvp2.from_bigint(bigi.from_int(2009))
let c = mvp2.compare(a, b)
should.equal(c, order.Gt)
}
pub fn mvp2_add_test() {
let assert Ok(a) = mvp2.from_bigint(bigi.from_int(2007))
let b = bigi.from_int(9)
let c = mvp2.add(a, b)
should.equal(c, Error(utils.DidOverflow(bigi.from_int(1))))
}

View file

@ -0,0 +1,20 @@
import gleam/order
import bigi
import gleeunit/should
import ranged_int/interface
import ranged_int/utils
import readme/mvp
pub fn mvp_test() {
let assert Ok(a) = interface.from_bigint(bigi.from_int(2007), mvp.iface)
let assert Ok(b) = interface.from_bigint(bigi.from_int(2009), mvp.iface)
let c = interface.compare(a, b, mvp.iface)
should.equal(c, order.Gt)
}
pub fn mvp_add_test() {
let assert Ok(a) = interface.from_bigint(bigi.from_int(2007), mvp.iface)
let b = bigi.from_int(9)
let c = interface.math_op(a, b, mvp.iface, bigi.add)
should.equal(c, Error(utils.DidOverflow(bigi.from_int(1))))
}