summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRutger Broekhoff2026-01-07 22:37:44 +0100
committerRutger Broekhoff2026-01-07 22:37:44 +0100
commit76cc9ce576e830a3ee7615d0f617a7ce24316c44 (patch)
tree0dc996a0e6a7dc944c8eb0bdc0ef9707d154e8fb
parent46169ec3eb38e177cafd7faf6338d36c6a9e3971 (diff)
downloadrdcapsis-76cc9ce576e830a3ee7615d0f617a7ce24316c44.tar.gz
rdcapsis-76cc9ce576e830a3ee7615d0f617a7ce24316c44.zip
pre-destruction commit
-rw-r--r--lib/ledger.ml454
-rw-r--r--lib/prelude.ml23
2 files changed, 364 insertions, 113 deletions
diff --git a/lib/ledger.ml b/lib/ledger.ml
index 7805179..058cc65 100644
--- a/lib/ledger.ml
+++ b/lib/ledger.ml
@@ -96,72 +96,251 @@ module Labels = struct
96 | Sexp.Atom _ -> of_sexp_error "Labels.t_of_sexp: list needed" sexp 96 | Sexp.Atom _ -> of_sexp_error "Labels.t_of_sexp: list needed" sexp
97end 97end
98 98
99module Money : sig 99module Debit_credit = struct
100 type t 100 type t = Debit | Credit [@@deriving string, sexp_of]
101 101
102 val equal : t -> t -> bool 102 (* let opposite = function Debit -> Credit | Credit -> Debit *)
103 val compare : t -> t -> int
104 val of_bigint : Bigint.t -> t
105 val to_bigint : t -> Bigint.t
106 val ( + ) : t -> t -> t
107 val ( - ) : t -> t -> t
108 val ( = ) : t -> t -> bool
109 val ( ~$ ) : int -> t
110 val sexp_of_t : t -> Sexp.t
111end = struct
112 type t = Bigint.t [@@deriving sexp_of]
113
114 let equal = Bigint.equal
115 let compare = Bigint.compare
116 let of_bigint = Fn.id
117 let to_bigint = Fn.id
118 let ( + ) x y = Bigint.(x + y)
119 let ( - ) x y = Bigint.(x - y)
120 let ( = ) = equal
121 let ( ~$ ) = Fn.compose of_bigint Bigint.of_int
122end 103end
123 104
124type commodity_id = string 105module Money = struct
125(* TODO: consider making this UUID *) [@@deriving equal, compare, sexp] 106 module Amount : sig
107 type t
108
109 val equal : t -> t -> bool
110 val compare : t -> t -> int
111 val of_bigint : Bigint.t -> t option
112 val to_bigint : t -> Bigint.t
113 val ( + ) : t -> t -> t
114 val ( = ) : t -> t -> bool
115 val sexp_of_t : t -> Sexp.t
116 val zero : t
117 end = struct
118 type t = Bigint.t [@@deriving sexp_of]
119
120 let equal = Bigint.equal
121 let compare = Bigint.compare
122 let of_bigint x = if Bigint.(zero <= x) then Some x else None
123 let to_bigint x = x
124 let ( + ) x y = Bigint.(x + y)
125 let ( = ) = equal
126 let zero = Bigint.zero
127 end
128
129 module Diff : sig
130 type t
131
132 val equal : t -> t -> bool
133 val compare : t -> t -> int
134 val of_bigint : Bigint.t -> t
135 val to_bigint : t -> Bigint.t
136 val ( + ) : t -> t -> t
137 val ( +% ) : t -> Amount.t -> t
138 val ( - ) : t -> t -> t
139 val ( -% ) : t -> Amount.t -> t
140 val ( = ) : t -> t -> bool
141 val neg : t -> t
142 val ( ~$ ) : int -> t
143 val sexp_of_t : t -> Sexp.t
144
145 val of_amount :
146 Amount.t -> Debit_credit.t -> on_debit:[ `Incr | `Decr ] -> t
147 end = struct
148 type t = Bigint.t [@@deriving sexp_of]
149
150 let equal = Bigint.equal
151 let compare = Bigint.compare
152 let of_bigint x = x
153 let to_bigint x = x
154 let ( + ) x y = Bigint.(x + y)
155 let ( +% ) x y = x + of_bigint (Amount.to_bigint y)
156 let ( - ) x y = Bigint.(x - y)
157 let ( -% ) x y = x - of_bigint (Amount.to_bigint y)
158 let ( = ) = equal
159 let neg = Bigint.neg
160 let ( ~$ ) = Fn.compose of_bigint Bigint.of_int
161
162 let of_amount x (dc : Debit_credit.t) ~on_debit =
163 match (dc, on_debit) with
164 | Debit, `Incr -> of_bigint (Amount.to_bigint x)
165 | Credit, `Incr -> neg (of_bigint (Amount.to_bigint x))
166 | Debit, `Decr -> neg (of_bigint (Amount.to_bigint x))
167 | Credit, `Decr -> of_bigint (Amount.to_bigint x)
168 end
169end
126 170
127type scalar = 171module Commodity_id = struct
128 | Amount of Money.t 172 type t = string [@@deriving equal, compare, sexp]
129 | Rate of { in_primary_commodity : Money.t; rate : Bigdecimal.t }
130[@@deriving equal, compare, sexp_of]
131 173
132module Account_id = struct 174 module Map = Map.Make (struct
133 type t = string list [@@deriving sexp, compare] 175 type nonrec t = t [@@deriving equal, compare, sexp]
176 end)
134end 177end
135 178
136type account = { 179(*
137 id : Account_id.t; 180type scalar =
138 description : string list; 181 | Amount of Money.Amount.t
139 commodity_id : commodity_id; 182 | Rate of { in_primary_commodity : Money.Amount.t; rate : Bigdecimal.t }
140 balance : Money.t; 183[@@deriving equal, compare, sexp_of] *)
141} 184
142[@@deriving sexp_of] 185module Account = struct
186 (* The contents of an account of category 'a *)
187 type 'a core =
188 (* Comprises of subaccounts of its subcategories *)
189 | Node of 'a String.Map.t
190 (* Comprises of subaccounts of its own category *)
191 | Ind of 'a t String.Map.t
192 (* Has no subaccounts, has a balance in a certain commodity *)
193 | Leaf of Commodity_id.t * Money.Diff.t
194
195 and extra = { description : String.t }
196 and 'a t = extra * 'a core
197
198 (* The category of the five top-level categories *)
199 type global
200
201 (* The five top-level categories *)
202 type asset
203 type equity
204 type expense
205 type income
206 type liability
207
208 (* No subcategories *)
209 type final
210
211 (* Subaccounts under the five top-level categories *)
212 type 'a f =
213 | Accounts_payable : final f t -> liability f
214 | Accounts_receivable : final f t -> asset f
215 | Bank : final f t -> asset f
216 | Cash : final f t -> asset f
217 | Credit : final f t -> liability f
218 | Mutual_fund : final f t -> asset f
219 | Stock : final f t -> asset f
220
221 module Ft_mapper = struct
222 type nonrec 'b t = { car : 'a. 'a f t -> ('b * 'a f t) option }
223
224 let map (type b c) (f : c t) : b f -> (c * b f) option =
225 let open Option.Let_syntax in
226 function
227 | Accounts_payable v ->
228 let%map c, v' = f.car v in
229 (c, Accounts_payable v')
230 | Accounts_receivable v ->
231 let%map c, v' = f.car v in
232 (c, Accounts_receivable v')
233 | Bank v ->
234 let%map c, v' = f.car v in
235 (c, Bank v')
236 | Cash v ->
237 let%map c, v' = f.car v in
238 (c, Cash v')
239 | Credit v ->
240 let%map c, v' = f.car v in
241 (c, Credit v')
242 | Mutual_fund v ->
243 let%map c, v' = f.car v in
244 (c, Mutual_fund v')
245 | Stock v ->
246 let%map c, v' = f.car v in
247 (c, Stock v')
248 end
249
250 module Top_level (F : sig
251 type 'a t
252 end) =
253 struct
254 type t =
255 | Asset of asset F.t
256 | Equity of equity F.t
257 | Expense of expense F.t
258 | Income of income F.t
259 | Liability of liability F.t
260 end
261
262 module Top_level_type = Top_level (struct
263 type nonrec 'a t = unit
264 end)
265
266 (* I swear the bullshit stops here *)
267 module F0 = struct
268 include Top_level (struct
269 type nonrec 'a t = 'a f t
270 end)
271
272 let type_ : t -> Top_level_type.t = function
273 | Asset _ -> Asset ()
274 | Equity _ -> Equity ()
275 | Expense _ -> Expense ()
276 | Income _ -> Income ()
277 | Liability _ -> Liability ()
278 end
279
280 (* All accounts *)
281 type world = F0.t String.Map.t
282
283 module Path = struct
284 type t = string list [@@deriving compare, sexp]
285
286 module Map = Map.Make (struct
287 type nonrec t = t [@@deriving compare, sexp]
288 end)
289 end
290
291 let world_inst : world =
292 String.Map.of_alist_exn
293 [
294 ( "Assets",
295 F0.Asset
296 ( { description = "assets" },
297 Ind
298 (String.Map.of_alist_exn
299 [
300 ( "Current",
301 ( { description = "current" },
302 Node
303 (String.Map.of_alist_exn
304 [
305 ( "Checking",
306 Bank
307 ( { description = "bnak accounts" },
308 Ind
309 (String.Map.of_alist_exn
310 [
311 ( "ING",
312 ( { description = "ING bank" },
313 Leaf ("EUC", Money.Diff.(~$0))
314 ) );
315 ( "N26",
316 ( { description = "ING bank" },
317 Leaf ("EUC", Money.Diff.(~$0))
318 ) );
319 ]) ) );
320 ]) ) );
321 ]) ) );
322 ]
323end
143 324
144type bal_assert = { 325type bal_assert = {
145 account : Account_id.t; 326 account : Account.Path.t;
146 amount : Money.t;
147 labels : Labels.t; 327 labels : Labels.t;
328 bal : Money.Diff.t;
148} 329}
149[@@deriving sexp_of] 330[@@deriving sexp_of]
150 331
151module Account_id_map = Map.Make (Account_id)
152
153module Debit_credit = struct
154 type t = Debit | Credit [@@deriving string, sexp_of]
155
156 let opposite = function Debit -> Credit | Credit -> Debit
157end
158
159module Tx : sig 332module Tx : sig
333 type entry = {
334 dc : Debit_credit.t;
335 commodity : Commodity_id.t;
336 amount : Money.Amount.t;
337 assertion : Money.Diff.t option;
338 }
339
160 (* Private because we only want to allow constructing balanced transactions. *) 340 (* Private because we only want to allow constructing balanced transactions. *)
161 type t = private { 341 type t = private {
162 cleared : Date.t option; 342 cleared : Date.t option;
163 commodity_id : commodity_id; 343 entries : entry Account.Path.Map.t;
164 entries : (Debit_credit.t * scalar * Money.t option) Account_id_map.t;
165 labels : Labels.t; 344 labels : Labels.t;
166 } 345 }
167 346
@@ -169,17 +348,23 @@ module Tx : sig
169 348
170 val make : 349 val make :
171 cleared:Date.t option -> 350 cleared:Date.t option ->
172 commodity_id:commodity_id -> 351 entries:entry Account.Path.Map.t ->
173 entries:(Debit_credit.t * scalar * Money.t option) Account_id_map.t ->
174 labels:Labels.t -> 352 labels:Labels.t ->
175 (t, error) result 353 (t, error) result
176 354
177 val sexp_of_t : t -> Sexp.t 355 val sexp_of_t : t -> Sexp.t
178end = struct 356end = struct
357 type entry = {
358 dc : Debit_credit.t;
359 commodity : Commodity_id.t;
360 amount : Money.Amount.t;
361 assertion : Money.Diff.t option;
362 }
363 [@@deriving sexp_of]
364
179 type t = { 365 type t = {
180 cleared : Date.t option; 366 cleared : Date.t option;
181 commodity_id : commodity_id; 367 entries : entry Account.Path.Map.t;
182 entries : (Debit_credit.t * scalar * Money.t option) Account_id_map.t;
183 labels : Labels.t; 368 labels : Labels.t;
184 } 369 }
185 [@@deriving sexp_of] 370 [@@deriving sexp_of]
@@ -187,22 +372,18 @@ end = struct
187 type error = Unbalanced 372 type error = Unbalanced
188 373
189 let is_balanced entries = 374 let is_balanced entries =
190 Map.fold entries 375 Map.fold entries ~init:Commodity_id.Map.empty
191 ~init:Money.(~$0, ~$0) 376 ~f:(fun ~key:_ ~data comm_balances ->
192 ~f:(fun ~key:_ ~data:(type_, scalar, _oassert) (ds, cs) -> 377 Map.update comm_balances data.commodity ~f:(fun ocomm_bal ->
193 let m = 378 let comm_bal = Option.value ocomm_bal ~default:Money.Diff.(~$0) in
194 match scalar with 379 match data.dc with
195 | Amount m -> m 380 | Debit_credit.Debit -> Money.Diff.(comm_bal +% data.amount)
196 | Rate { in_primary_commodity = m; _ } -> m 381 | Debit_credit.Credit -> Money.Diff.(comm_bal -% data.amount)))
197 in 382 |> Map.for_all ~f:(fun comm_bal -> Money.Diff.(comm_bal = ~$0))
198 match type_ with 383
199 | Debit_credit.Debit -> Money.(ds + m, cs) 384 let make ~cleared ~entries ~labels =
200 | Debit_credit.Credit -> Money.(ds, cs + m))
201 |> fun (ds, cs) -> Money.(ds = cs)
202
203 let make ~cleared ~commodity_id ~entries ~labels =
204 if not (is_balanced entries) then Error Unbalanced 385 if not (is_balanced entries) then Error Unbalanced
205 else Ok { cleared; commodity_id; entries; labels } 386 else Ok { cleared; entries; labels }
206end 387end
207 388
208type item = 389type item =
@@ -212,53 +393,100 @@ type item =
212 393
213type t = item list [@@deriving sexp_of] 394type t = item list [@@deriving sexp_of]
214 395
215module Account = struct
216 type global_type = Asset | Equity | Liability | Expense | Income
217 [@@deriving compare, sexp]
218
219 type asset
220 type global
221
222 type 'a subcategory =
223 | Asset : asset subcategory option -> global subcategory
224 | Checking : asset subcategory
225
226 type 'a t = Sub of ('a, 'a t) category String.Map.t
227
228 let world : global t =
229 Sub
230 (String.Map.of_alist_exn [ ("Assets", Asset (Some (
231 String.Map.of_alist_exn [
232 ("Checking", Checking)
233 ]
234 ))) ])
235end
236
237(*
238module World = struct 396module World = struct
239 type t = (commodity_id * Money.t) Account_id_map.t 397 type t = Account.world
240 398
241 let empty : t = Account_id_map.empty 399 let empty : t = String.Map.empty
242 400
243 let apply_tx_entry_base aid primary_commodity debit_credit scalar = 401 let update_bal_fn = (f :
244 let amount = Scalar.to_amount ~commodity:primary_commodity scalar in 402 Account.Top_level_type.t ->
245 Map.update aid ~f:(function 403 Commodity_id.t ->
246 | None -> 404 Money.Diff.t ->
247 405 ('a * Money.Diff.t) option)
248 (* 406
249 let assert_bal aid sc world = 407 (* Stretching the type system a little :) *)
250 408 let rec update_bal_aux ttype subaid
251 let apply_tx_entry aid (dc, sc, oassert) world = *) 409 (f :
252 410 Account.Top_level_type.t ->
253 let apply_tx (tx : Tx.t) world = 411 Commodity_id.t ->
254 Map.fold tx.entries ~init:world ~f:(fun ~key:account_id ~data:(type_, scalar, _oassert) world -> 412 Money.Diff.t ->
255 413 ('a * Money.Diff.t) option) : 'a Account.Ft_mapper.t =
256 414 {
257 ) 415 car =
416 (fun in_acc ->
417 let open Option.Let_syntax in
418 match (subaid, in_acc) with
419 | [], (extra, Account.Leaf (acc_comm, acc_bal)) ->
420 let%bind x, acc_bal' = f ttype acc_comm acc_bal in
421 Some (x, (extra, Account.Leaf (acc_comm, acc_bal')))
422 | [], _ -> None
423 | subaid0 :: subaid, (extra, Node subaccs) ->
424 let open Option.Let_syntax in
425 let%bind subacc = Map.find subaccs subaid0 in
426 let%map x, subacc' =
427 Account.Ft_mapper.map (update_bal_aux ttype subaid f) subacc
428 in
429 ( x,
430 ( extra,
431 Account.Node (Map.set subaccs ~key:subaid0 ~data:subacc') ) )
432 | subaid0 :: subaid, (extra, Ind subaccs) ->
433 let open Option.Let_syntax in
434 let%bind subacc = Map.find subaccs subaid0 in
435 let%map x, subacc' = (update_bal_aux ttype subaid f).car subacc in
436 ( x,
437 (extra, Account.Ind (Map.set subaccs ~key:subaid0 ~data:subacc'))
438 )
439 | _ :: _, (_, Leaf _) -> None);
440 }
441
442 (** Update the balance (debit/credit ([dc])) of account [aid] [by_amount]
443 (commodity: [in_comm]) in [world], giving the updated world and the pre
444 and post balances for [aid] iff the account exists in [world]. *)
445 let update_bal aid dc by_amount in_comm (world : t) :
446 ((Money.Diff.t * Money.Diff.t) * t) option =
447 match aid with
448 | [] -> None
449 | aid0 :: subaid -> (
450 let open Option.Let_syntax in
451 match%bind Map.find world aid0 with
452 | Asset acc ->
453 let diff = Money.Diff.of_amount by_amount dc ~on_debit:`Incr in
454 let%map bals, acc' = (update_bal_aux subaid diff in_comm).car acc in
455 (bals, Map.set world ~key:aid0 ~data:(Asset acc'))
456 | Expense acc ->
457 let diff = Money.Diff.of_amount by_amount dc ~on_debit:`Incr in
458 let%map bals, acc' = (update_bal_aux subaid diff in_comm).car acc in
459 (bals, Map.set world ~key:aid0 ~data:(Expense acc'))
460 | Income acc ->
461 let diff = Money.Diff.of_amount by_amount dc ~on_debit:`Decr in
462 let%map bals, acc' = (update_bal_aux subaid diff in_comm).car acc in
463 (bals, Map.set world ~key:aid0 ~data:(Income acc'))
464 | Liability acc ->
465 let diff = Money.Diff.of_amount by_amount dc ~on_debit:`Decr in
466 let%map bals, acc' = (update_bal_aux subaid diff in_comm).car acc in
467 (bals, Map.set world ~key:aid0 ~data:(Liability acc'))
468 | Equity acc ->
469 let diff = Money.Diff.of_amount by_amount dc ~on_debit:`Decr in
470 let%map bals, acc' = (update_bal_aux subaid diff in_comm).car acc in
471 (bals, Map.set world ~key:aid0 ~data:(Equity acc')))
472
473 let apply_tx (tx : Tx.t) world : t option =
474 Map.fold_option tx.entries ~init:world
475 ~f:(fun ~key:aid ~(data : Tx.entry) world ->
476 let open Option.Let_syntax in
477 let%bind (_old_bal, new_bal), world =
478 update_bal aid data.dc data.amount data.commodity world
479 in
480 match data.assertion with
481 | None -> Some world
482 | Some bal_ass ->
483 if Money.Diff.(bal_ass = new_bal) then Some world else None)
258 484
259 let apply : item -> t -> t = function 485 let apply : item -> t -> t option = function
260 | Tx_item tx -> apply_tx tx 486 | Tx_item tx -> apply_tx tx
261 | Bal_assert_item ba -> apply_ba ba 487 | Bal_assert_item ba -> apply_ba ba
262end *) 488end
489
490module Ctxd_item = struct end
263 491
264let make = Fn.id 492let make = Fn.id
diff --git a/lib/prelude.ml b/lib/prelude.ml
index 57f7af3..f571a4d 100644
--- a/lib/prelude.ml
+++ b/lib/prelude.ml
@@ -16,6 +16,29 @@ module List = struct
16 go 16 go
17end 17end
18 18
19module Map = struct
20 include Map
21
22 let fold_result (m : ('k, 'v, _) t) ~(init : 'acc)
23 ~(f : key:'k -> data:'v -> 'acc -> ('acc, 'err) result) :
24 ('acc, 'err) result =
25 fold_until m ~init
26 ~f:(fun ~key ~data acc ->
27 match f ~key ~data acc with
28 | Ok acc' -> Continue acc'
29 | Error _ as res -> Stop res)
30 ~finish:(fun v -> Ok v)
31
32 let fold_option (m : ('k, 'v, _) t) ~(init : 'acc)
33 ~(f : key:'k -> data:'v -> 'acc -> 'acc option) : 'acc option =
34 fold_until m ~init
35 ~f:(fun ~key ~data acc ->
36 match f ~key ~data acc with
37 | Some acc' -> Continue acc'
38 | None -> Stop None)
39 ~finish:(fun v -> Some v)
40end
41
19module Z = struct 42module Z = struct
20 include Z 43 include Z
21 44