diff options
Diffstat (limited to 'lib/account/account.ml')
| -rw-r--r-- | lib/account/account.ml | 121 |
1 files changed, 121 insertions, 0 deletions
diff --git a/lib/account/account.ml b/lib/account/account.ml new file mode 100644 index 0000000..0ef3d28 --- /dev/null +++ b/lib/account/account.ml | |||
| @@ -0,0 +1,121 @@ | |||
| 1 | open Prelude | ||
| 2 | |||
| 3 | (* TODO: Decide on public interface. Probably should not include | ||
| 4 | functions such as [unsafe_update_bal], but having [Balanced_batch] | ||
| 5 | under [Account] also feels a bit awkward. *) | ||
| 6 | |||
| 7 | module Path = struct | ||
| 8 | type t = string list [@@deriving compare, sexp] | ||
| 9 | end | ||
| 10 | |||
| 11 | (** Ensures that only accounts with valid type hierarchies can be constructed. | ||
| 12 | *) | ||
| 13 | module Kernel : sig | ||
| 14 | type extra = { description : string } | ||
| 15 | |||
| 16 | type t = private { type_ : Type.t; extra : extra; core : core } | ||
| 17 | |||
| 18 | and core = | ||
| 19 | (* Balance in some commodity *) | ||
| 20 | | Leaf of Money.Commodity_id.t * Money.Diff.t | ||
| 21 | | Subtree of t String.Map.t | ||
| 22 | |||
| 23 | val make : Type.t -> extra -> core -> t option | ||
| 24 | end = struct | ||
| 25 | type extra = { description : string } | ||
| 26 | |||
| 27 | type t = { type_ : Type.t; extra : extra; core : core } | ||
| 28 | |||
| 29 | and core = | ||
| 30 | (* Balance in some commodity *) | ||
| 31 | | Leaf of Money.Commodity_id.t * Money.Diff.t | ||
| 32 | | Subtree of t String.Map.t | ||
| 33 | |||
| 34 | let make type_ extra : core -> t option = function | ||
| 35 | | Leaf (comm, bal) -> Some { type_; extra; core = Leaf (comm, bal) } | ||
| 36 | | Subtree children -> | ||
| 37 | if | ||
| 38 | Map.for_all children ~f:(fun subacc -> | ||
| 39 | Type.is_super subacc.type_ ~super:type_ ~strict:false) | ||
| 40 | then Some { type_; extra; core = Subtree children } | ||
| 41 | else None | ||
| 42 | end | ||
| 43 | |||
| 44 | type t = Kernel.t | ||
| 45 | |||
| 46 | type update_bal_error = | ||
| 47 | | Empty_path | ||
| 48 | | Unmatching_commodity_id of { in_account : Money.Commodity_id.t } | ||
| 49 | | Not_a_leaf_account | ||
| 50 | | Not_a_subtree_account | ||
| 51 | | Not_found (* TODO: report at which level *) | ||
| 52 | |||
| 53 | (* We do not necessarily expect [aid] to be a valid path, as we | ||
| 54 | always do for paths in the type hierarchy. The difference is that | ||
| 55 | the type hierarchy is fixed, while the account hierarchy can | ||
| 56 | change over the course of a year. *) | ||
| 57 | let rec unsafe_update_bal_aux (aid : Path.t) (dc : Money.Debit_credit.t) | ||
| 58 | (by_amount : Money.Amount.t) (in_comm : Money.Commodity_id.t) (w : t) : | ||
| 59 | (t, update_bal_error) result = | ||
| 60 | match (aid, w.core) with | ||
| 61 | | [], Leaf (comm, bal) -> | ||
| 62 | if [%equal: Money.Commodity_id.t] in_comm comm then (* slay! *) | ||
| 63 | let core' = | ||
| 64 | Kernel.Leaf | ||
| 65 | ( comm, | ||
| 66 | Money.Diff.(bal + of_amount by_amount dc (Type.polarity w.type_)) | ||
| 67 | ) | ||
| 68 | in | ||
| 69 | Ok (Option.value_exn (Kernel.make w.type_ w.extra core')) | ||
| 70 | else (* bruh *) | ||
| 71 | Error (Unmatching_commodity_id { in_account = comm }) | ||
| 72 | | [], Subtree _ -> Error Not_a_leaf_account | ||
| 73 | | _ :: _, Leaf _ -> Error Not_a_subtree_account | ||
| 74 | | aid0 :: aid', Subtree subaccs -> ( | ||
| 75 | match Map.find subaccs aid0 with | ||
| 76 | | None -> Error Not_found | ||
| 77 | | Some subacc -> | ||
| 78 | let open Result.Let_syntax in | ||
| 79 | let%bind subacc' = | ||
| 80 | (* TODO: when reporting at which level Not_found fails, | ||
| 81 | we want to make sure that we extend the information | ||
| 82 | in the error with the current aid0 (so we recover a | ||
| 83 | full path to where the account is missing *) | ||
| 84 | unsafe_update_bal_aux aid' dc by_amount in_comm subacc | ||
| 85 | in | ||
| 86 | let core' = | ||
| 87 | Kernel.Subtree (Map.set subaccs ~key:aid0 ~data:subacc') | ||
| 88 | in | ||
| 89 | Ok (Option.value_exn (Kernel.make w.type_ w.extra core'))) | ||
| 90 | |||
| 91 | (* Unfortunate but true, there has to be some kind of a root account | ||
| 92 | with no type :) *) | ||
| 93 | type root = t String.Map.t | ||
| 94 | |||
| 95 | let unsafe_update_bal (aid : Path.t) (dc : Money.Debit_credit.t) | ||
| 96 | (by_amount : Money.Amount.t) (in_comm : Money.Commodity_id.t) (w : root) : | ||
| 97 | (root, update_bal_error) result = | ||
| 98 | match aid with | ||
| 99 | | [] -> Error Empty_path | ||
| 100 | | aid0 :: aid' -> ( | ||
| 101 | match Map.find w aid0 with | ||
| 102 | | None -> Error Not_found | ||
| 103 | | Some subacc -> | ||
| 104 | let open Result.Let_syntax in | ||
| 105 | (* TODO: when reporting at which level Not_found fails, | ||
| 106 | we want to make sure that we extend the information | ||
| 107 | in the error with the current aid0 (so we recover a | ||
| 108 | full path to where the account is missing *) | ||
| 109 | let%bind subacc' = | ||
| 110 | unsafe_update_bal_aux aid' dc by_amount in_comm subacc | ||
| 111 | in | ||
| 112 | Ok (Map.set w ~key:aid0 ~data:subacc')) | ||
| 113 | |||
| 114 | module Balanced_batch_acc_paths = Balanced_batch.Make (Path) | ||
| 115 | |||
| 116 | let apply_balanced_batch (b : Balanced_batch_acc_paths.t) (w : root) = | ||
| 117 | Map.fold_result (Balanced_batch_acc_paths.entries b) ~init:w | ||
| 118 | ~f:(fun ~key:aid ~(data : Balanced_batch_acc_paths.entry) w -> | ||
| 119 | let open Result.Let_syntax in | ||
| 120 | let%bind w = unsafe_update_bal aid data.dc data.amount data.commodity w in | ||
| 121 | Ok w) | ||