Skip to content

Commit

Permalink
fix yojson derive (#6)
Browse files Browse the repository at this point in the history
  • Loading branch information
Geoffrey Borough authored Apr 26, 2024
1 parent aff2ee0 commit 2afff70
Show file tree
Hide file tree
Showing 25 changed files with 2,679 additions and 2,555 deletions.
7 changes: 5 additions & 2 deletions .ocamlformat
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
profile = janestreet
version = 0.26.1
profile = ocamlformat
break-cases = fit
margin = 77
wrap-comments = true
line-endings = lf
10 changes: 10 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
## 0.2.0 (2024-04-26)

* Removed transitive dependecies
* Removed unnecessary sexp derive
* Fixed Yojson derive on exceptions
* Fixed module scopes and exports
* Renamed library public name to Safemoney
* Renamed module Utils to Ops and added signatures
* Updated code examples

## 0.1.1 (2023-09-20)

* Minor syntax fix and updated doc location
Expand Down
43 changes: 21 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ API Documentation: https://gborough.github.io/safemoney/safemoney

# SAFEMONEY

(This library is currently experimental and still working towards production-ready, all contributions are welcome)

A type safe money manipulation library for ocaml, currently supporting user-defined currency types as well as all ISO4217 currency codes and major crypto currencies.

It is well known that using float numbers for money calculation is inherently lossy and should be avoided at all cost. A classic example shows that 0.21 + 0.32 could possibly lead to 0.530003 which extra money is created out of thin air. Money calculation without context is also considered error prone (e.g. the accidental USD $10. + AUD $10. = ? $20). Also there are a number of float representations and operations do not make sense for money calculation (e.g. accidentally operating on infinity values or using functions like Float.atanh).

On the other hand money calculations using checked rational(quotient) and discrete(integer) numbers with properly established context are considered to be safe. Some of the core types of this library are based on rational and discrete numbers. Back to the previous example of 0.21 + 0.32, we can substitute using rational numbers 21/100 + 32/100 = 53/100 and clearly nothing is lost or created arbitrarily. Alternatively if we were to operate on the scale of "cents" we can then write in integers 21 + 32 = 53 which achieves the same result. This library aims to establish and uphold safety contract by providing contexts in which the calculations take place, the following examples will not compile(see user manual for more examples):

```ocaml
let open Quotient in
let open Safemoney.Quotient in
let qv1 = make_qv ("AUD", make_q "30/100") in <--- AUD
+
Expand All @@ -21,7 +23,7 @@ qv1 + qv2 Error
```

```ocaml
let open Discrete in
let open Safemoney.Discrete in
let scale1 = Scale.make_scale "AUD" "dollar" (make_q "1/1") in <--- Unit in dollar
+
Expand Down Expand Up @@ -74,7 +76,7 @@ let zv = make_z "12345" in ...
The **Quotient** type provides a context for rational number operations, taking a **string** of currency name and a **Qv** value:

```ocaml
let open Quotient in
let open Safemoney.Quotient in
let qv1 = make_qv ("AUD", make_q "30/100") in
let qv2 = make_qv ("AUD", make_q "20/100") in
qv1 + qv2
Expand All @@ -85,7 +87,7 @@ qv1 + qv2
The **Discrete** type provides a context for integer number operations, taking a **Scale** type and a **Zv** value. The Scale type takes a currency name, an unit name and a scale ratio which denote a named division with respect to the unit account of the currency, e.g. for USD the subdivision of 100 "cent" make up to 1 "dollar" of unit account, hence the ratio is 100/1 which is represented by a Qv value:

```ocaml
let open Discrete in
let open Safemoney.Discrete in
let scale = Scale.make_scale "USD" "cent" (make_q "100/1") in
let dv1 = make_dv (scale, make_z "200") in
let dv2 = make_dv (scale, make_z "100") in
Expand All @@ -97,14 +99,14 @@ dv1 - dv2
The **Exchange** type provides a context for currency exchange mechanism, taking a source currency, destination currency and exchange rate of **Qv** value:

```ocaml
let open Exchange in
let open Safemoney.Exchange in
let aud_to_nzd = make_xchg ~src: "AUD" ~dst: "NZD" (make_q "4908/4503") in ...
```

To compose exchange rate, e.g. from GBP to NZD via AUD:

```ocaml
let open Exchange in
let open Safemoney.Exchange in
let gbp_to_aud = make_xchg ~src: "GBP" ~dst: "AUD" (make_q "8872/4503") in
let aud_to_nzd = make_xchg ~src: "AUD" ~dst: "NZD" (make_q "4908/4503") in
let gbp_to_nzd = gbp_to_aud **> aud_to_nzd in ...
Expand All @@ -115,6 +117,8 @@ let gbp_to_nzd = gbp_to_aud **> aud_to_nzd in ...
The **Custom** type provides a signature for user defined currency types, allowing items such as currency code, description of the currency, an optional hashmap of unit scales and Qv/Zv initialiser to be specified. Obviously this is an opinionated approach and the **Predefined** module relies on this signature, but users could optionally come up with their own signatures to cater for specific needs. Let's define a new module for a currency called CamelCoin™ by implementing Custom signature:

```ocaml
open Safemoney.Types
module CAMELCOIN : Custom = struct
let symbol = "CMC"
Expand All @@ -136,22 +140,22 @@ module CAMELCOIN : Custom = struct
| None -> None
end
let open Quotient in
let open Safemoney.Quotient in
let qv1 = CAMELCOIN.make_qv (make_q "30/100") in
let qv2 = CAMELCOIN.make_qv (make_q "20/100") in
let dv1 = Core.Option.value_exn (CAMELCOIN.make_dv "cent" (make_z "200")) in
let dv2 = Core.Option.value_exn (CAMELCOIN.make_dv "cent" (make_z "100")) in
let dv1 = Option.get (CAMELCOIN.make_dv "cent" (make_z "200")) in
let dv2 = Option.get (CAMELCOIN.make_dv "cent" (make_z "100")) in
qv1 - qv2;
dv1 + dv2;
```

### Predefined

The **Predefined** module includes readily made ISO4217 currencies and major crypto currencies. See usage in **Custom**.
The **Predefined** module includes readily made ISO4217 currencies and major crypto currencies. See usage in signature **Custom**.

### Sealing Operations

Practically in real life when calculations are **DONE** on these safe types we might want to convert them to the float representations along with a conversion strategies, effectively sealing the operations/presenting the final result, and promise not to use it further. The **Utils** module provides such sealing functions and their effective signatures are:
Practically in real life when calculations are **DONE** on these safe types we might want to convert them to the float representations along with a conversion strategies, effectively sealing the operations/presenting the final result, and promise not to use it further. The **Ops** module provides such sealing functions and their effective signatures are:

* seal_quotient -> (val seal_quotient: printing_conf:printing_conf -> qv:Safemoney.Quotient.t -> string)
* seal_discrete -> (val seal_discrete: printing_conf:printing_conf -> dv:Safemoney.Discrete.t -> string)
Expand All @@ -161,12 +165,14 @@ Practically in real life when calculations are **DONE** on these safe types we m
The **printing_conf** specifies how the the final float number should be presented, by providing a number **Separator**, whether to print a "+" sign, number of decimal places to show and a **rounding** strategy. e.g to seal a Quotient value:

```ocaml
let open Utils in
let open Safemoney in
open Safemoney
open Safemoney.Ops
(** We make a value on the fly but usually it is the result of a chain of safe Qv type calculations **)
let qv = make_qv ("USD", make_q "1234567/7") in
(** Using a premade separator "sep_dot_comma" in Utils module **)
let printing_conf = { separator = sep_dot_comma; plus_sign = true; num_of_digits = Uint8.of_int 4; rounding = Truncate } in
let printing_conf = make_printing_conf ~sep:(sep_dot_comma ()) ~plus_sign:true ~num_of_digits:4 ~rounding:Truncate
in
seal_quotient ~printing_conf: printing_conf ~qv: qv
```
would print "+176,366.7142"
Expand All @@ -175,7 +181,7 @@ Note the sealed result is a **String** to discourage further usage

### Unsafe

This library does provide escape hatches to allow float values from unsafe part of the codebase to be returned into the safe types, if you cannot establish the chain of proof for the float safety contract then use them at your own peril. The **Utils** module provides such functions:
This library does provide escape hatches to allow float values from unsafe part of the codebase to be returned into the safe types, if you cannot establish the chain of proof for the float safety contract then use them at your own peril. The **Ops** module provides such functions:

* unsafe_float_to_quotient
* unsafe_float_to_discrete
Expand All @@ -184,13 +190,6 @@ This library does provide escape hatches to allow float values from unsafe part

Note the float value is taken as **String** to encourage "finalisation" on previously unsafe float. To guide the functions on how to correctly recognise the separator the correct one should be provided or parser error is thrown.


## TODOs

- Toplevel Printer

- Preprocessor

## License

This project is licensed under the [MIT license].
Expand Down
3 changes: 1 addition & 2 deletions dune-project
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
(name safemoney)

(generate_opam_files true)
(implicit_transitive_deps false)

(source
(github gborough/safemoney))
Expand All @@ -21,4 +20,4 @@
(authors "Geoffrey Borough")
(synopsis "A type safe money manipulation library")
(description "A type safe money manipulation library")
(depends (ocaml (>= 4.14.0)) core dune (zarith (>= 1.6)) stdint (angstrom (>= 0.14.0)) re2 yojson (ppx_jane (>= v0.16.0)) ppx_deriving ppx_yojson_conv ppx_deriving_yojson))
(depends (ocaml (>= 4.14.0)) base dune zarith stdint angstrom re2 yojson ppx_deriving ppx_deriving_yojson ppx_jane (ppx_expect :with-test)))
95 changes: 40 additions & 55 deletions lib/discrete.ml
Original file line number Diff line number Diff line change
@@ -1,96 +1,81 @@
open Core

module Make (Qv : Qv_intf.S) (Zv : Zv_intf.S) = struct
exception ScaleTypeMismatch of string [@@deriving sexp, yojson]
exception ScaleTypeMismatch of string

module Scale = struct
exception ValidScaleError of string [@@deriving sexp, yojson]
exception ValidScaleError of string

type t =
{ symbol : string
; unit : string
; value : Qv.t
}
{ symbol: string [@compare.ignore]
; unit: string [@compare.ignore]
; value: Qv.t }
[@@deriving compare]

type showable =
{ symbol_ : string [@key "symbol"]
; unit_ : string [@key "unit"]
; value_ : string [@key "value"]
}
[@@deriving show, sexp, yojson]
{ symbol_: string [@key "symbol"]
; unit_: string [@key "unit"]
; value_: string [@key "value"] }
[@@deriving show, yojson]

let check_scale scale =
if Z.gt (Qv.S.num_of_q scale) Z.zero && Z.gt (Qv.S.den_of_q scale) Z.zero
if
Z.gt (Qv.S.num_of_q scale) Z.zero
&& Z.gt (Qv.S.den_of_q scale) Z.zero
then true
else false
;;

let make_scale symbol unit value =
if check_scale value
then { symbol; unit; value }
if check_scale value then {symbol; unit; value}
else raise (ValidScaleError "Valid scale must be greater than 0")
;;

let to_showable_json x =
let to_json x =
Yojson.Safe.to_string
@@ showable_to_yojson
{ symbol_ = x.symbol; unit_ = x.unit; value_ = Qv.S.to_str x.value }
;;
{symbol_= x.symbol; unit_= x.unit; value_= Qv.S.to_str x.value}
end

type t =
{ scale : Scale.t
; value : Zv.t
}
[@@deriving compare]
type t = {scale: Scale.t; value: Zv.t} [@@deriving compare]

type showable =
{ scale_ : string [@key "discrete_scale"]
; value_ : string [@key "discrete_value"]
}
[@@deriving show, sexp, yojson]
{ scale_: string [@key "discrete_scale"]
; value_: string [@key "discrete_value"] }
[@@deriving show, yojson]

let check_scale t1 t2 =
if Scale.compare t1.scale t2.scale = 0 then true else false

let check_scale t1 t2 = if Scale.compare t1.scale t2.scale = 0 then true else false
let make_dv (scale, value) = { scale; value }
let make_dv (scale, value) = {scale; value}

let show_scale t =
Printf.printf "(%s %s %s)" t.scale.symbol t.scale.unit (Qv.S.to_str t.scale.value)
;;
Printf.printf "(%s %s %s)" t.scale.symbol t.scale.unit
(Qv.S.to_str t.scale.value)

let show_val t = Printf.printf "%s" (Zv.S.to_str t.value)

let show_t t =
Printf.printf
"(%s %s %s %s)"
t.scale.symbol
t.scale.unit
Printf.printf "(%s %s %s %s)" t.scale.symbol t.scale.unit
(Qv.S.to_str t.scale.value)
(Zv.S.to_str t.value)
;;

let neg t = { t with value = Zv.S.neg t.value }
let abs t = { t with value = Zv.S.abs t.value }
let neg t = {t with value= Zv.S.neg t.value}

let abs t = {t with value= Zv.S.abs t.value}

let ( + ) t1 t2 =
if check_scale t1 t2
then { t1 with value = Zv.S.add t1.value t2.value }
else raise (ScaleTypeMismatch "cannot operate on two different currency scales")
;;
if check_scale t1 t2 then {t1 with value= Zv.S.add t1.value t2.value}
else
raise
(ScaleTypeMismatch "cannot operate on two different currency scales")

let ( - ) t1 t2 =
if check_scale t1 t2
then { t1 with value = Zv.S.sub t1.value t2.value }
else raise (ScaleTypeMismatch "cannot operate on two different currency scales")
;;
if check_scale t1 t2 then {t1 with value= Zv.S.sub t1.value t2.value}
else
raise
(ScaleTypeMismatch "cannot operate on two different currency scales")

let ( * ) ~t ~value = { t with value = Zv.S.mul t.value value }
let ( * ) ~t ~value = {t with value= Zv.S.mul t.value value}

let to_showable_json x =
let to_json x =
Yojson.Safe.to_string
@@ showable_to_yojson
{ scale_ = Scale.to_showable_json x.scale
; value_ = Zv.S.to_showable_json x.value
}
;;
{scale_= Scale.to_json x.scale; value_= Zv.S.to_json x.value}
end
Loading

0 comments on commit 2afff70

Please sign in to comment.