1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
open Base
open Stdint
type rounding =
| Up
| Down
| Nearest
| NearestHalfToEven
| TowardsZero
| WithDecimalPrecision
| Truncate
module Separator = struct
type t = string * string option
let make_exn (c1, c2) =
if Char.is_digit c1 || Char.is_whitespace c1 then
failwith "C1 separator type not allowed"
else
match c2 with
| Some c2 ->
if Char.compare c1 c2 = 0 then
failwith "Separator must be different as per convention"
else if Char.is_digit c2 || Char.is_whitespace c2 then
failwith "C2 separator type not allowed"
else (String.of_char c1, Some (String.of_char c2))
| None -> (String.of_char c1, None)
let fst x = fst x
let snd x = snd x
end
type printing_conf =
{ separator: Separator.t
; plus_sign: bool
; num_of_digits: Uint8.t
; rounding: rounding }
let make_printing_conf ~sep ~plus_sign ~num_of_digits ~rounding =
{ separator= sep
; plus_sign
; num_of_digits= Uint8.of_int num_of_digits
; rounding }
let sep_comma () = Separator.make_exn (',', None)
let sep_comma_dot () = Separator.make_exn (',', Some '.')
let sep_comma_space () = Separator.make_exn (',', Some '_')
let sep_dot () = Separator.make_exn ('.', None)
let sep_dot_comma () = Separator.make_exn ('.', Some ',')
let sep_dot_space () = Separator.make_exn ('.', Some '_')
let default_printing_conf () =
make_printing_conf ~sep:(sep_dot ()) ~plus_sign:false ~num_of_digits:2
~rounding:Up
(** Mark a positive integer value with thousand separator configuration
e.g. [mark_thousands ~v: 1234567 ~sep: ","]
would print "1,234,567" *)
let mark_thousands ~v ~sep =
let k = 1000 in
if Int.compare v k < 0 then Int.to_string v
else
let divmod num den = (Int.( / ) num den, Int.rem num den) in
let aux ~sep ~i =
match divmod i k with
| 0, 0 -> None
| 0, rem -> Some (Int.to_string rem, 0)
| quo, rem ->
if Int.compare rem 10 < 0 then
Some (sep ^ "0" ^ "0" ^ Int.to_string rem, quo)
else if Int.compare rem 100 < 0 then
Some (sep ^ "0" ^ Int.to_string rem, quo)
else Some (sep ^ Int.to_string rem, quo)
in
Sequence.fold ~init:"" ~f:(Fn.flip ( ^ ))
(Sequence.unfold ~init:v ~f:(fun i -> aux ~sep ~i))
let q_to_decimal ~printing_conf ~q =
let qv = Qv.S.to_float q in
let sep1 = Separator.fst printing_conf.separator in
let sep2 =
Option.value (Separator.snd printing_conf.separator) ~default:""
in
let num_of_digits = Uint8.to_int printing_conf.num_of_digits in
let modf_aux v =
( Float.Parts.integral @@ Float.modf v
, Float.abs @@ Float.Parts.fractional @@ Float.modf v )
in
let sign_aux v =
match Float.sign_exn @@ fst v with
| Sign.Pos -> if printing_conf.plus_sign then "+" else ""
| Sign.Neg -> "-"
| _ -> ""
in
let print_aux v =
let parts = modf_aux v in
let sign = sign_aux parts in
sign
^ mark_thousands ~v:(Int.abs @@ Float.to_int (fst parts)) ~sep:sep2
^ sep1 ^ Int.to_string
@@ Float.to_int (snd parts)
in
match printing_conf.rounding with
| Up -> print_aux @@ Float.round_up qv
| Down -> print_aux @@ Float.round_down qv
| Nearest -> print_aux @@ Float.round_nearest qv
| NearestHalfToEven -> print_aux @@ Float.round_nearest_half_to_even qv
| TowardsZero -> print_aux @@ Float.round_towards_zero qv
| WithDecimalPrecision ->
let round_decimal ?(decimal_digits = 2) qv =
Float.round_decimal ~decimal_digits qv
in
print_aux @@ round_decimal ~decimal_digits:num_of_digits qv
| Truncate -> (
let fpair = modf_aux qv in
let sign = sign_aux fpair in
let rec aux acc n s : string =
if n = 0 then acc else aux (s ^ acc) (n - 1) s
in
let pad = aux "" num_of_digits "0" in
let f_part v =
Option.value
(List.nth (String.split (Float.to_string v) ~on:'.') 1)
~default:""
in
let take_digits len v =
let open Angstrom in
match parse_string ~consume:Prefix (take len) v with
| Ok r -> r
| Error _ -> failwith "error parsing digits"
in
match fpair with
| 0., 0. -> sign ^ "0" ^ sep1 ^ pad
| i, 0. ->
let ipart =
mark_thousands ~v:(Int.abs @@ Float.to_int i) ~sep:sep2
in
sign ^ ipart ^ sep1 ^ pad
| i, f ->
let fpart = f_part f in
let ipart =
mark_thousands ~v:(Int.abs @@ Float.to_int i) ~sep:sep2
in
let len = num_of_digits - String.length fpart in
let dig = take_digits num_of_digits fpart in
if len > 0 then sign ^ ipart ^ sep1 ^ dig ^ aux "" len "0"
else sign ^ ipart ^ sep1 ^ dig )
(** Unsafely convert a float/decimal value of string rep to integer value.
It is unsafe in a sense that the origin of float/decimal is deemed to
be unverified by default and might be as a result of lossy operations *)
let unsafe_integer_to_z integer = Z.to_string @@ Z.of_int integer
let seal_quotient ~printing_conf ~(qv : Quotient.t) =
q_to_decimal ~printing_conf ~q:qv.value_
let seal_discrete ~printing_conf ~(dv : Discrete.t) =
let z_to_q =
Qv.S.div (Qv.S.make (Zv.S.to_str dv.value_ ^ "/1")) dv.scale_.value_
in
q_to_decimal ~printing_conf ~q:z_to_q
let seal_exchange ~printing_conf ~(xchg : Exchange.t) =
q_to_decimal ~printing_conf ~q:xchg.value_
let seal_scale ~printing_conf ~(scale : Discrete.Scale.t) =
q_to_decimal ~printing_conf ~q:scale.value_
let unsafe_decimal_to_q ~decimal ~sep =
let sep1 = Separator.fst sep in
let is_eol = function '\r' | '\n' -> true | _ -> false in
let open Angstrom in
let parser =
lift3
(fun sign ipart fpart ->
Q.to_string @@ Q.of_float
@@ Float.of_string (sign ^ ipart ^ sep1 ^ fpart) )
( peek_char
>>= function
| Some '-' -> advance 1 >>| fun () -> "-"
| Some '+' -> advance 1 >>| fun () -> ""
| Some c when Char.is_digit c -> return ""
| _ -> fail "failure parsing sign" )
( match Separator.snd sep with
| None -> take_while1 Char.is_digit
| Some sep2 ->
lift2
(fun start rest -> start ^ rest)
( count 3 (satisfy Char.is_digit)
<|> count 2 (satisfy Char.is_digit)
<|> count 1 (satisfy Char.is_digit)
>>| String.of_char_list )
( many
@@ char (Char.of_string sep2)
*> count 3 (satisfy Char.is_digit)
>>| List.concat >>| String.of_char_list ) )
( (char (Char.of_string sep1) >>| String.of_char)
*> ( take_while1 (fun c -> if Char.is_digit c then true else false)
<* take_till is_eol
<|> (at_end_of_input >>| fun _ -> "") ) )
in
parse_string ~consume:All parser decimal
let unsafe_float_to_quotient ~symbol ~decimal ~sep =
match unsafe_decimal_to_q ~decimal ~sep with
| Ok qv -> Quotient.make_qv (symbol, Qv.S.make qv)
| Error msg -> failwith msg
let unsafe_float_to_discrete ~scale ~integer =
let r = unsafe_integer_to_z integer in
Discrete.make_dv (scale, Zv.S.make r)
let unsafe_float_to_exchange ~src ~dst ~decimal ~sep =
match unsafe_decimal_to_q ~decimal ~sep with
| Ok qv -> Exchange.make_xchg ~src ~dst (Qv.S.make qv)
| Error msg -> failwith msg
let unsafe_float_to_scale ~sym ~sub_unit ~decimal ~sep =
match unsafe_decimal_to_q ~decimal ~sep with
| Ok qv -> Discrete.Scale.make_scale ~sym ~sub_unit (Qv.S.make qv)
| Error msg -> failwith msg