diff options
| author | Rutger Broekhoff | 2026-02-22 00:57:41 +0100 |
|---|---|---|
| committer | Rutger Broekhoff | 2026-02-23 21:12:22 +0100 |
| commit | 8bcc0a7c0dfae57824cb1e80db2835f652d3b4ab (patch) | |
| tree | 7d15d67f51b35bbabd28b9c0d1d10e780ecbfb82 | |
| parent | 2367d2caa83831992392069c21bd96cb91e113f0 (diff) | |
| download | rdcapsis-8bcc0a7c0dfae57824cb1e80db2835f652d3b4ab.tar.gz rdcapsis-8bcc0a7c0dfae57824cb1e80db2835f652d3b4ab.zip | |
helo
| -rw-r--r-- | lib/account.ml | 96 | ||||
| -rw-r--r-- | lib/ledger.ml | 112 |
2 files changed, 165 insertions, 43 deletions
diff --git a/lib/account.ml b/lib/account.ml new file mode 100644 index 0000000..988b55c --- /dev/null +++ b/lib/account.ml | |||
| @@ -0,0 +1,96 @@ | |||
| 1 | open Prelude | ||
| 2 | |||
| 3 | (** The 'kernel' of account types: a hierarchy of valid types. A valid type is a | ||
| 4 | path that leads to a node in the hierarchy. *) | ||
| 5 | module Type_hierarchy : sig | ||
| 6 | type path | ||
| 7 | |||
| 8 | val children : path -> path list | ||
| 9 | val sub : path -> string -> path option | ||
| 10 | val super : path -> path option | ||
| 11 | val equal_path : path -> path -> bool | ||
| 12 | val is_prefix : path -> prefix:path -> bool | ||
| 13 | val root : path | ||
| 14 | val asset : path | ||
| 15 | val equity : path | ||
| 16 | val expense : path | ||
| 17 | val income : path | ||
| 18 | val liability : path | ||
| 19 | end = struct | ||
| 20 | type tree = { car : tree String.Map.t } | ||
| 21 | type path = Root | Sub of string * path | ||
| 22 | |||
| 23 | let canonical : tree = | ||
| 24 | let mk alist = { car = String.Map.of_alist_exn alist } in | ||
| 25 | mk | ||
| 26 | [ | ||
| 27 | ( "Asset", | ||
| 28 | mk | ||
| 29 | [ | ||
| 30 | ("Accounts_receivable", mk []); | ||
| 31 | ("Bank", mk [ ("Savings", mk []); ("Checking", mk []) ]); | ||
| 32 | ("Cash", mk []); | ||
| 33 | ("Mutual_fund", mk []); | ||
| 34 | ("Stock", mk []); | ||
| 35 | ] ); | ||
| 36 | ("Equity", mk []); | ||
| 37 | ("Expense", mk []); | ||
| 38 | ("Income", mk []); | ||
| 39 | ("Liability", mk [ ("Accounts_payable", mk []); ("Credit", mk []) ]); | ||
| 40 | ] | ||
| 41 | |||
| 42 | let rec get_node : path -> tree option = function | ||
| 43 | | Root -> Some canonical | ||
| 44 | | Sub (t, p) -> | ||
| 45 | let open Option.Let_syntax in | ||
| 46 | let%bind super = get_node p in | ||
| 47 | Map.find super.car t | ||
| 48 | |||
| 49 | (** Always gives [Some] under valid paths, giving a list of valid paths *) | ||
| 50 | let children (p : path) : path list = | ||
| 51 | let node = Option.value_exn (get_node p) in | ||
| 52 | List.map (Map.keys node.car) ~f:(fun sub -> Sub (sub, p)) | ||
| 53 | |||
| 54 | let sub (p : path) (name : string) : path option = | ||
| 55 | let node = Option.value_exn (get_node p) in | ||
| 56 | if Map.mem node.car name then Some (Sub (name, p)) else None | ||
| 57 | |||
| 58 | let super : path -> path option = function | ||
| 59 | | Root -> None | ||
| 60 | | Sub (_, super) -> Some super | ||
| 61 | |||
| 62 | let rec equal_path p1 p2 = | ||
| 63 | match (p1, p2) with | ||
| 64 | | Root, Root -> true | ||
| 65 | | Sub (x1, p1'), Sub (x2, p2') -> String.(x1 = x2) && equal_path p1' p2' | ||
| 66 | | _, _ -> false | ||
| 67 | |||
| 68 | let rec is_prefix (p : path) ~(prefix : path) : bool = | ||
| 69 | match (prefix, p) with | ||
| 70 | | Root, Root | Root, Sub _ -> true | ||
| 71 | | Sub (x1, p'), Sub (x2, prefix') -> | ||
| 72 | String.(x1 = x2) && is_prefix p' ~prefix:prefix' | ||
| 73 | | _ -> false | ||
| 74 | |||
| 75 | let root = Root | ||
| 76 | let asset = sub root "Asset" |> Option.value_exn | ||
| 77 | let equity = sub root "Equity" |> Option.value_exn | ||
| 78 | let expense = sub root "Expense" |> Option.value_exn | ||
| 79 | let income = sub root "Income" |> Option.value_exn | ||
| 80 | let liability = sub root "Liability" |> Option.value_exn | ||
| 81 | end | ||
| 82 | |||
| 83 | module Type = struct | ||
| 84 | type t = Type_hierarchy.path [@@deriving equal] | ||
| 85 | |||
| 86 | let rec base (t : t) : t option = | ||
| 87 | match Type_hierarchy.super t with | ||
| 88 | | None -> (* [t] is the root type *) None | ||
| 89 | | Some t' -> | ||
| 90 | (* [t] is a base type if its supertype is the root type *) | ||
| 91 | Some (Option.value (base t') ~default:t) | ||
| 92 | |||
| 93 | (** [a] is a strict supertype of [b] *) | ||
| 94 | let is_strict_super a b = | ||
| 95 | Type_hierarchy.is_prefix b ~prefix:a && not ([%equal: t] a b) | ||
| 96 | end | ||
diff --git a/lib/ledger.ml b/lib/ledger.ml index 9d315ae..d63dbed 100644 --- a/lib/ledger.ml +++ b/lib/ledger.ml | |||
| @@ -395,21 +395,16 @@ module Account_hierarchy = struct | |||
| 395 | 395 | ||
| 396 | let rec unsafe_alter_aux (subaid : Account_path.t) | 396 | let rec unsafe_alter_aux (subaid : Account_path.t) |
| 397 | (f : | 397 | (f : |
| 398 | Account_type.t0 -> | 398 | Account_type.t0 -> Commodity_id.t -> Money.Diff.t -> 'a * Money.Diff.t) |
| 399 | extra -> | 399 | : 'a Mapper.t = |
| 400 | Commodity_id.t -> | ||
| 401 | Money.Diff.t -> | ||
| 402 | 'a * extra * Money.Diff.t) : 'a Mapper.t = | ||
| 403 | { | 400 | { |
| 404 | car = | 401 | car = |
| 405 | (fun in_acc mkt -> | 402 | (fun in_acc mkt -> |
| 406 | let open Option.Let_syntax in | 403 | let open Option.Let_syntax in |
| 407 | match (subaid, in_acc) with | 404 | match (subaid, in_acc) with |
| 408 | | [], (extra, Leaf (acc_comm, acc_bal)) -> | 405 | | [], (extra, Leaf (acc_comm, acc_bal)) -> |
| 409 | let x, extra', acc_bal' = | 406 | let x, acc_bal' = f (mkt Account_type.Leaf) acc_comm acc_bal in |
| 410 | f (mkt Account_type.Leaf) extra acc_comm acc_bal | 407 | Some (x, (extra, Leaf (acc_comm, acc_bal'))) |
| 411 | in | ||
| 412 | Some (x, (extra', Leaf (acc_comm, acc_bal'))) | ||
| 413 | | [], _ -> None | 408 | | [], _ -> None |
| 414 | | subaid0 :: subaid, (extra, Node subaccs) -> | 409 | | subaid0 :: subaid, (extra, Node subaccs) -> |
| 415 | let%bind subacc = Map.find subaccs subaid0 in | 410 | let%bind subacc = Map.find subaccs subaid0 in |
| @@ -429,11 +424,8 @@ module Account_hierarchy = struct | |||
| 429 | 424 | ||
| 430 | let unsafe_alter (aid : Account_path.t) | 425 | let unsafe_alter (aid : Account_path.t) |
| 431 | (f : | 426 | (f : |
| 432 | Account_type.t0 -> | 427 | Account_type.t0 -> Commodity_id.t -> Money.Diff.t -> 'a * Money.Diff.t) |
| 433 | extra -> | 428 | (w : world) : ('a * world) option = |
| 434 | Commodity_id.t -> | ||
| 435 | Money.Diff.t -> | ||
| 436 | 'a * extra * Money.Diff.t) (w : world) : ('a * world) option = | ||
| 437 | match aid with | 429 | match aid with |
| 438 | | [] -> None | 430 | | [] -> None |
| 439 | | aid0 :: subaid -> ( | 431 | | aid0 :: subaid -> ( |
| @@ -466,16 +458,18 @@ module Account_hierarchy = struct | |||
| 466 | (x, Map.set w ~key:aid0 ~data:(Equity acc'))) | 458 | (x, Map.set w ~key:aid0 ~data:(Equity acc'))) |
| 467 | 459 | ||
| 468 | (** Update the balance (debit/credit ([dc])) of account [aid] [by_amount] | 460 | (** Update the balance (debit/credit ([dc])) of account [aid] [by_amount] |
| 469 | (commodity: [in_comm]) in [world], giving the updated world and the pre | 461 | (commodity: [in_comm]) in [world], giving the updated world and the post |
| 470 | and post balances for [aid] iff the account exists in [world]. *) | 462 | balance for [aid] iff the account exists in [world]. |
| 471 | let update_bal aid dc by_amount in_comm (w : world) : | 463 | |
| 472 | (Money.Diff.t * Money.Diff.t * world) option = | 464 | Unsafe because [by_amount] must be balanced with updates to other |
| 465 | accounts. *) | ||
| 466 | let unsafe_update_bal aid dc by_amount in_comm (w : world) : | ||
| 467 | (Money.Diff.t * world) option = | ||
| 473 | let open Option.Let_syntax in | 468 | let open Option.Let_syntax in |
| 474 | let%bind mres, w' = | 469 | let%bind mres, w' = |
| 475 | unsafe_alter aid | 470 | unsafe_alter aid |
| 476 | (fun acc_type acc_extra acc_comm acc_bal -> | 471 | (fun acc_type acc_comm acc_bal -> |
| 477 | if not ([%equal: Commodity_id.t] acc_comm in_comm) then | 472 | if not ([%equal: Commodity_id.t] acc_comm in_comm) then (None, acc_bal) |
| 478 | (None, acc_extra, acc_bal) | ||
| 479 | else | 473 | else |
| 480 | let on_debit = | 474 | let on_debit = |
| 481 | match acc_type with | 475 | match acc_type with |
| @@ -488,41 +482,68 @@ module Account_hierarchy = struct | |||
| 488 | let acc_bal' = | 482 | let acc_bal' = |
| 489 | Money.Diff.(acc_bal + of_amount by_amount dc ~on_debit) | 483 | Money.Diff.(acc_bal + of_amount by_amount dc ~on_debit) |
| 490 | in | 484 | in |
| 491 | (Some (acc_bal, acc_bal'), acc_extra, acc_bal')) | 485 | (Some acc_bal', acc_bal')) |
| 492 | w | 486 | w |
| 493 | in | 487 | in |
| 494 | let%map pre_bal, post_bal = mres in | 488 | let%map post_bal = mres in |
| 495 | (pre_bal, post_bal, w') | 489 | (post_bal, w') |
| 496 | |||
| 497 | let get_bal aid (w : world) : (Commodity_id.t * Money.Diff.t) option = | ||
| 498 | let open Option.Let_syntax in | ||
| 499 | let%map cb, _world' = | ||
| 500 | unsafe_alter aid | ||
| 501 | (fun _acc_type acc_extra acc_comm acc_bal -> | ||
| 502 | ((acc_comm, acc_bal), acc_extra, acc_bal)) | ||
| 503 | w | ||
| 504 | in | ||
| 505 | cb | ||
| 506 | 490 | ||
| 507 | let add_balance_maps m1 m2 : Money.Diff.t Commodity_id.Map.t = | 491 | let add_balance_maps m1 m2 : Money.Diff.t Commodity_id.Map.t = |
| 508 | Map.merge m1 m2 ~f:(fun ~key:_comm -> function | 492 | Map.merge m1 m2 ~f:(fun ~key:_comm -> function |
| 509 | | `Both (b1, b2) -> Some Money.Diff.(b1 + b2) | 493 | | `Both (b1, b2) -> Some Money.Diff.(b1 + b2) |
| 510 | | `Left b | `Right b -> Some b) | 494 | | `Left b | `Right b -> Some b) |
| 511 | 495 | ||
| 512 | let rec collect_balances : type a. | 496 | module Account_visitor = struct |
| 497 | type 'res t = { car : 'a. 'a Structure.f account -> 'res } | ||
| 498 | |||
| 499 | let rec visit_aux : type a. | ||
| 500 | 'res t -> Account_path.t * a Structure.f account -> 'res option = | ||
| 501 | fun visitor -> function | ||
| 502 | | [], in_acc -> Some (visitor.car in_acc) | ||
| 503 | | _ :: _, (_, Leaf _) -> None | ||
| 504 | | subaid0 :: subaid, (_, Ind subaccs) -> | ||
| 505 | let open Option.Let_syntax in | ||
| 506 | let%bind subacc = Map.find subaccs subaid0 in | ||
| 507 | visit_aux visitor (subaid, subacc) | ||
| 508 | | subaid0 :: subaid, (_, Node subaccs) -> | ||
| 509 | let open Option.Let_syntax in | ||
| 510 | let module Visitor = Account_structure.Basic_visitor (Structure) in | ||
| 511 | let%bind subacc = Map.find subaccs subaid0 in | ||
| 512 | Visitor.visit | ||
| 513 | { car = (fun subacc -> (visit_aux visitor) (subaid, subacc)) } | ||
| 514 | subacc | ||
| 515 | |||
| 516 | let visit (visitor : 'res t) (w : world) : Account_path.t -> 'res option = | ||
| 517 | function | ||
| 518 | | [] -> None | ||
| 519 | | aid0 :: subaid -> ( | ||
| 520 | let open Option.Let_syntax in | ||
| 521 | match%bind Map.find w aid0 with | ||
| 522 | | Asset acc -> visit_aux visitor (subaid, acc) | ||
| 523 | | Expense acc -> visit_aux visitor (subaid, acc) | ||
| 524 | | Equity acc -> visit_aux visitor (subaid, acc) | ||
| 525 | | Liability acc -> visit_aux visitor (subaid, acc) | ||
| 526 | | Income acc -> visit_aux visitor (subaid, acc)) | ||
| 527 | end | ||
| 528 | |||
| 529 | let rec collect_balances_aux : type a. | ||
| 513 | a Structure.f account -> Money.Diff.t Commodity_id.Map.t = function | 530 | a Structure.f account -> Money.Diff.t Commodity_id.Map.t = function |
| 514 | | _extra, Leaf (acc_comm, acc_bal) -> | 531 | | _extra, Leaf (acc_comm, acc_bal) -> |
| 515 | Commodity_id.Map.singleton acc_comm acc_bal | 532 | Commodity_id.Map.singleton acc_comm acc_bal |
| 516 | | _extra, Ind subaccs -> | 533 | | _extra, Ind subaccs -> |
| 517 | Map.fold subaccs ~init:Commodity_id.Map.empty | 534 | Map.fold subaccs ~init:Commodity_id.Map.empty |
| 518 | ~f:(fun ~key:_ ~data:subacc comm_bal_sums -> | 535 | ~f:(fun ~key:_ ~data:subacc comm_bal_sums -> |
| 519 | add_balance_maps comm_bal_sums (collect_balances subacc)) | 536 | add_balance_maps comm_bal_sums (collect_balances_aux subacc)) |
| 520 | | _extra, Node subaccs -> | 537 | | _extra, Node subaccs -> |
| 521 | Map.fold subaccs ~init:Commodity_id.Map.empty | 538 | Map.fold subaccs ~init:Commodity_id.Map.empty |
| 522 | ~f:(fun ~key:_ ~data:subacc comm_bal_sums -> | 539 | ~f:(fun ~key:_ ~data:subacc comm_bal_sums -> |
| 523 | let module Visitor = Account_structure.Basic_visitor (Structure) in | 540 | let module Visitor = Account_structure.Basic_visitor (Structure) in |
| 524 | add_balance_maps comm_bal_sums | 541 | add_balance_maps comm_bal_sums |
| 525 | (Visitor.visit { car = collect_balances } subacc)) | 542 | (Visitor.visit { car = collect_balances_aux } subacc)) |
| 543 | |||
| 544 | let collect_balances : | ||
| 545 | world -> Account_path.t -> Money.Diff.t Commodity_id.Map.t option = | ||
| 546 | Account_visitor.visit { car = collect_balances_aux } | ||
| 526 | 547 | ||
| 527 | type delete_error = Not_found | Nonzero_balance | 548 | type delete_error = Not_found | Nonzero_balance |
| 528 | 549 | ||
| @@ -569,7 +590,11 @@ module Account_hierarchy = struct | |||
| 569 | end | 590 | end |
| 570 | 591 | ||
| 571 | module Bal_assert = struct | 592 | module Bal_assert = struct |
| 572 | type t = { account : Account_path.t; labels : Labels.t; bal : Money.Diff.t } | 593 | type t = { |
| 594 | account : Account_path.t; | ||
| 595 | labels : Labels.t; | ||
| 596 | bals : Money.Diff.t Commodity_id.Map.t; | ||
| 597 | } | ||
| 573 | [@@deriving sexp_of] | 598 | [@@deriving sexp_of] |
| 574 | end | 599 | end |
| 575 | 600 | ||
| @@ -658,9 +683,9 @@ module World = struct | |||
| 658 | Map.fold_option tx.entries ~init:world | 683 | Map.fold_option tx.entries ~init:world |
| 659 | ~f:(fun ~key:aid ~(data : Tx.entry) world -> | 684 | ~f:(fun ~key:aid ~(data : Tx.entry) world -> |
| 660 | let open Option.Let_syntax in | 685 | let open Option.Let_syntax in |
| 661 | let%bind _old_bal, new_bal, world = | 686 | let%bind new_bal, world = |
| 662 | Account_hierarchy.update_bal aid data.dc data.amount data.commodity | 687 | Account_hierarchy.unsafe_update_bal aid data.dc data.amount |
| 663 | world | 688 | data.commodity world |
| 664 | in | 689 | in |
| 665 | match data.assertion with | 690 | match data.assertion with |
| 666 | | None -> Some world | 691 | | None -> Some world |
| @@ -669,8 +694,9 @@ module World = struct | |||
| 669 | 694 | ||
| 670 | let apply_ba (ba : Bal_assert.t) world : t option = | 695 | let apply_ba (ba : Bal_assert.t) world : t option = |
| 671 | let open Option.Let_syntax in | 696 | let open Option.Let_syntax in |
| 672 | let%bind _comm, bal = Account_hierarchy.get_bal ba.account world in | 697 | let%bind bals = Account_hierarchy.collect_balances world ba.account in |
| 673 | if not Money.Diff.(bal = ba.bal) then None else Some world | 698 | if not ([%equal: Money.Diff.t Commodity_id.Map.t] bals ba.bals) then None |
| 699 | else Some world | ||
| 674 | 700 | ||
| 675 | let apply_ad (_ad : Account_decl.t) _world : t option = None | 701 | let apply_ad (_ad : Account_decl.t) _world : t option = None |
| 676 | 702 | ||