diff options
| author | Rutger Broekhoff | 2026-01-08 02:16:11 +0100 |
|---|---|---|
| committer | Rutger Broekhoff | 2026-01-08 02:16:11 +0100 |
| commit | 40ed2624e13bc519bebe4332a217fd539b76e5f4 (patch) | |
| tree | 30a2e3ef58bbb49633ecf703f6b606f523398116 /lib/ledger.ml | |
| parent | 76cc9ce576e830a3ee7615d0f617a7ce24316c44 (diff) | |
| download | rdcapsis-40ed2624e13bc519bebe4332a217fd539b76e5f4.tar.gz rdcapsis-40ed2624e13bc519bebe4332a217fd539b76e5f4.zip | |
Type system crimes have been committed
Diffstat (limited to 'lib/ledger.ml')
| -rw-r--r-- | lib/ledger.ml | 334 |
1 files changed, 194 insertions, 140 deletions
diff --git a/lib/ledger.ml b/lib/ledger.ml index 058cc65..3e1d177 100644 --- a/lib/ledger.ml +++ b/lib/ledger.ml | |||
| @@ -182,19 +182,7 @@ type scalar = | |||
| 182 | | Rate of { in_primary_commodity : Money.Amount.t; rate : Bigdecimal.t } | 182 | | Rate of { in_primary_commodity : Money.Amount.t; rate : Bigdecimal.t } |
| 183 | [@@deriving equal, compare, sexp_of] *) | 183 | [@@deriving equal, compare, sexp_of] *) |
| 184 | 184 | ||
| 185 | module Account = struct | 185 | module Gh = 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 *) | 186 | (* The category of the five top-level categories *) |
| 199 | type global | 187 | type global |
| 200 | 188 | ||
| @@ -207,92 +195,188 @@ module Account = struct | |||
| 207 | 195 | ||
| 208 | (* No subcategories *) | 196 | (* No subcategories *) |
| 209 | type final | 197 | type final |
| 198 | end | ||
| 210 | 199 | ||
| 211 | (* Subaccounts under the five top-level categories *) | 200 | module Account_structure0 (F : sig |
| 201 | type 'a t | ||
| 202 | end) = | ||
| 203 | struct | ||
| 212 | type 'a f = | 204 | type 'a f = |
| 213 | | Accounts_payable : final f t -> liability f | 205 | | Accounts_payable : Gh.final f F.t -> Gh.liability f |
| 214 | | Accounts_receivable : final f t -> asset f | 206 | | Accounts_receivable : Gh.final f F.t -> Gh.asset f |
| 215 | | Bank : final f t -> asset f | 207 | | Bank : Gh.final f F.t -> Gh.asset f |
| 216 | | Cash : final f t -> asset f | 208 | | Cash : Gh.final f F.t -> Gh.asset f |
| 217 | | Credit : final f t -> liability f | 209 | | Credit : Gh.final f F.t -> Gh.liability f |
| 218 | | Mutual_fund : final f t -> asset f | 210 | | Mutual_fund : Gh.final f F.t -> Gh.asset f |
| 219 | | Stock : final f t -> asset f | 211 | | Stock : Gh.final f F.t -> Gh.asset f |
| 220 | 212 | ||
| 221 | module Ft_mapper = struct | 213 | type t0 = |
| 222 | type nonrec 'b t = { car : 'a. 'a f t -> ('b * 'a f t) option } | 214 | | Asset of Gh.asset f F.t |
| 223 | 215 | | Equity of Gh.equity f F.t | |
| 224 | let map (type b c) (f : c t) : b f -> (c * b f) option = | 216 | | Expense of Gh.expense f F.t |
| 217 | | Income of Gh.income f F.t | ||
| 218 | | Liability of Gh.liability f F.t | ||
| 219 | end | ||
| 220 | |||
| 221 | module Account_type = struct | ||
| 222 | type 'a elem = Leaf | Node of 'a | ||
| 223 | |||
| 224 | include Account_structure0 (struct | ||
| 225 | type 'a t = 'a elem | ||
| 226 | end) | ||
| 227 | end | ||
| 228 | |||
| 229 | module Account_structure (F : sig | ||
| 230 | type 'a t | ||
| 231 | end) = | ||
| 232 | struct | ||
| 233 | include Account_structure0 (F) | ||
| 234 | |||
| 235 | module Mapper = struct | ||
| 236 | type nonrec 'b t = { | ||
| 237 | car : | ||
| 238 | 'a. | ||
| 239 | 'a f F.t -> | ||
| 240 | ('a Account_type.f Account_type.elem -> Account_type.t0) -> | ||
| 241 | ('b * 'a f F.t) option; | ||
| 242 | } | ||
| 243 | |||
| 244 | let map (type b c) (f : c t) (mkt : b Account_type.f -> Account_type.t0) : | ||
| 245 | b f -> (c * b f) option = | ||
| 225 | let open Option.Let_syntax in | 246 | let open Option.Let_syntax in |
| 226 | function | 247 | function |
| 227 | | Accounts_payable v -> | 248 | | Accounts_payable v -> |
| 228 | let%map c, v' = f.car v in | 249 | let%map c, v' = |
| 250 | f.car v (fun el -> mkt (Account_type.Accounts_payable el)) | ||
| 251 | in | ||
| 229 | (c, Accounts_payable v') | 252 | (c, Accounts_payable v') |
| 230 | | Accounts_receivable v -> | 253 | | Accounts_receivable v -> |
| 231 | let%map c, v' = f.car v in | 254 | let%map c, v' = |
| 255 | f.car v (fun el -> mkt (Account_type.Accounts_receivable el)) | ||
| 256 | in | ||
| 232 | (c, Accounts_receivable v') | 257 | (c, Accounts_receivable v') |
| 233 | | Bank v -> | 258 | | Bank v -> |
| 234 | let%map c, v' = f.car v in | 259 | let%map c, v' = f.car v (fun el -> mkt (Account_type.Bank el)) in |
| 235 | (c, Bank v') | 260 | (c, Bank v') |
| 236 | | Cash v -> | 261 | | Cash v -> |
| 237 | let%map c, v' = f.car v in | 262 | let%map c, v' = f.car v (fun el -> mkt (Account_type.Cash el)) in |
| 238 | (c, Cash v') | 263 | (c, Cash v') |
| 239 | | Credit v -> | 264 | | Credit v -> |
| 240 | let%map c, v' = f.car v in | 265 | let%map c, v' = f.car v (fun el -> mkt (Account_type.Credit el)) in |
| 241 | (c, Credit v') | 266 | (c, Credit v') |
| 242 | | Mutual_fund v -> | 267 | | Mutual_fund v -> |
| 243 | let%map c, v' = f.car v in | 268 | let%map c, v' = |
| 269 | f.car v (fun el -> mkt (Account_type.Mutual_fund el)) | ||
| 270 | in | ||
| 244 | (c, Mutual_fund v') | 271 | (c, Mutual_fund v') |
| 245 | | Stock v -> | 272 | | Stock v -> |
| 246 | let%map c, v' = f.car v in | 273 | let%map c, v' = f.car v (fun el -> mkt (Account_type.Stock el)) in |
| 247 | (c, Stock v') | 274 | (c, Stock v') |
| 248 | end | 275 | end |
| 276 | end | ||
| 249 | 277 | ||
| 250 | module Top_level (F : sig | 278 | module Typed_account_path = struct |
| 251 | type 'a t | 279 | type 'a elem = Leaf | Node of string * 'a | Ind of string * 'a elem |
| 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 | 280 | ||
| 262 | module Top_level_type = Top_level (struct | 281 | include Account_structure (struct |
| 263 | type nonrec 'a t = unit | 282 | type 'a t = 'a elem |
| 264 | end) | 283 | end) |
| 284 | end | ||
| 265 | 285 | ||
| 266 | (* I swear the bullshit stops here *) | 286 | module Account_path = struct |
| 267 | module F0 = struct | 287 | type t = string list [@@deriving compare, sexp] |
| 268 | include Top_level (struct | 288 | |
| 269 | type nonrec 'a t = 'a f t | 289 | module Map = Map.Make (struct |
| 270 | end) | 290 | type nonrec t = t [@@deriving compare, sexp] |
| 271 | 291 | end) | |
| 272 | let type_ : t -> Top_level_type.t = function | 292 | end |
| 273 | | Asset _ -> Asset () | 293 | |
| 274 | | Equity _ -> Equity () | 294 | module Account_hierarchy = struct |
| 275 | | Expense _ -> Expense () | 295 | (* The contents of an account of category 'a *) |
| 276 | | Income _ -> Income () | 296 | type 'a core = |
| 277 | | Liability _ -> Liability () | 297 | (* Comprises of subaccounts of its subcategories *) |
| 278 | end | 298 | | Node of 'a String.Map.t |
| 299 | (* Comprises of subaccounts of its own category *) | ||
| 300 | | Ind of 'a account String.Map.t | ||
| 301 | (* Has no subaccounts, has a balance in a certain commodity *) | ||
| 302 | | Leaf of Commodity_id.t * Money.Diff.t | ||
| 303 | |||
| 304 | and extra = { description : String.t } | ||
| 305 | and 'a account = extra * 'a core | ||
| 306 | |||
| 307 | include Account_structure (struct | ||
| 308 | type 'a t = 'a account | ||
| 309 | end) | ||
| 279 | 310 | ||
| 280 | (* All accounts *) | 311 | (* All accounts *) |
| 281 | type world = F0.t String.Map.t | 312 | type world = t0 String.Map.t |
| 282 | 313 | ||
| 283 | module Path = struct | 314 | let rec alter_aux (subaid : Account_path.t) |
| 284 | type t = string list [@@deriving compare, sexp] | 315 | (f : |
| 316 | Account_type.t0 -> Commodity_id.t -> Money.Diff.t -> 'a * Money.Diff.t) | ||
| 317 | : 'a Mapper.t = | ||
| 318 | { | ||
| 319 | car = | ||
| 320 | (fun in_acc mkt -> | ||
| 321 | let open Option.Let_syntax in | ||
| 322 | match (subaid, in_acc) with | ||
| 323 | | [], (extra, Leaf (acc_comm, acc_bal)) -> | ||
| 324 | let x, acc_bal' = f (mkt Account_type.Leaf) acc_comm acc_bal in | ||
| 325 | Some (x, (extra, Leaf (acc_comm, acc_bal'))) | ||
| 326 | | [], _ -> None | ||
| 327 | | subaid0 :: subaid, (extra, Node subaccs) -> | ||
| 328 | let open Option.Let_syntax in | ||
| 329 | let%bind subacc = Map.find subaccs subaid0 in | ||
| 330 | let%map x, subacc' = | ||
| 331 | Mapper.map (alter_aux subaid f) (fun k -> mkt (Node k)) subacc | ||
| 332 | in | ||
| 333 | (x, (extra, Node (Map.set subaccs ~key:subaid0 ~data:subacc'))) | ||
| 334 | | subaid0 :: subaid, (extra, Ind subaccs) -> | ||
| 335 | let open Option.Let_syntax in | ||
| 336 | let%bind subacc = Map.find subaccs subaid0 in | ||
| 337 | let%map x, subacc' = (alter_aux subaid f).car subacc mkt in | ||
| 338 | (x, (extra, Ind (Map.set subaccs ~key:subaid0 ~data:subacc'))) | ||
| 339 | | _ :: _, (_, Leaf _) -> None); | ||
| 340 | } | ||
| 285 | 341 | ||
| 286 | module Map = Map.Make (struct | 342 | let alter (aid : Account_path.t) |
| 287 | type nonrec t = t [@@deriving compare, sexp] | 343 | (f : |
| 288 | end) | 344 | Account_type.t0 -> Commodity_id.t -> Money.Diff.t -> 'a * Money.Diff.t) |
| 289 | end | 345 | (w : world) : ('a * world) option = |
| 346 | match aid with | ||
| 347 | | [] -> None | ||
| 348 | | aid0 :: subaid -> ( | ||
| 349 | let open Option.Let_syntax in | ||
| 350 | match%bind Map.find w aid0 with | ||
| 351 | | Asset acc -> | ||
| 352 | let%map x, acc' = (alter_aux subaid f).car acc (fun k -> Asset k) in | ||
| 353 | (x, Map.set w ~key:aid0 ~data:(Asset acc')) | ||
| 354 | | Expense acc -> | ||
| 355 | let%map x, acc' = | ||
| 356 | (alter_aux subaid f).car acc (fun k -> Expense k) | ||
| 357 | in | ||
| 358 | (x, Map.set w ~key:aid0 ~data:(Expense acc')) | ||
| 359 | | Income acc -> | ||
| 360 | let%map x, acc' = | ||
| 361 | (alter_aux subaid f).car acc (fun k -> Income k) | ||
| 362 | in | ||
| 363 | (x, Map.set w ~key:aid0 ~data:(Income acc')) | ||
| 364 | | Liability acc -> | ||
| 365 | let%map x, acc' = | ||
| 366 | (alter_aux subaid f).car acc (fun k -> Liability k) | ||
| 367 | in | ||
| 368 | (x, Map.set w ~key:aid0 ~data:(Liability acc')) | ||
| 369 | | Equity acc -> | ||
| 370 | let%map x, acc' = | ||
| 371 | (alter_aux subaid f).car acc (fun k -> Equity k) | ||
| 372 | in | ||
| 373 | (x, Map.set w ~key:aid0 ~data:(Equity acc'))) | ||
| 290 | 374 | ||
| 291 | let world_inst : world = | 375 | let world_inst : world = |
| 292 | String.Map.of_alist_exn | 376 | String.Map.of_alist_exn |
| 293 | [ | 377 | [ |
| 294 | ( "Assets", | 378 | ( "Assets", |
| 295 | F0.Asset | 379 | Asset |
| 296 | ( { description = "assets" }, | 380 | ( { description = "assets" }, |
| 297 | Ind | 381 | Ind |
| 298 | (String.Map.of_alist_exn | 382 | (String.Map.of_alist_exn |
| @@ -323,7 +407,7 @@ module Account = struct | |||
| 323 | end | 407 | end |
| 324 | 408 | ||
| 325 | type bal_assert = { | 409 | type bal_assert = { |
| 326 | account : Account.Path.t; | 410 | account : Account_path.t; |
| 327 | labels : Labels.t; | 411 | labels : Labels.t; |
| 328 | bal : Money.Diff.t; | 412 | bal : Money.Diff.t; |
| 329 | } | 413 | } |
| @@ -340,7 +424,7 @@ module Tx : sig | |||
| 340 | (* Private because we only want to allow constructing balanced transactions. *) | 424 | (* Private because we only want to allow constructing balanced transactions. *) |
| 341 | type t = private { | 425 | type t = private { |
| 342 | cleared : Date.t option; | 426 | cleared : Date.t option; |
| 343 | entries : entry Account.Path.Map.t; | 427 | entries : entry Account_path.Map.t; |
| 344 | labels : Labels.t; | 428 | labels : Labels.t; |
| 345 | } | 429 | } |
| 346 | 430 | ||
| @@ -348,7 +432,7 @@ module Tx : sig | |||
| 348 | 432 | ||
| 349 | val make : | 433 | val make : |
| 350 | cleared:Date.t option -> | 434 | cleared:Date.t option -> |
| 351 | entries:entry Account.Path.Map.t -> | 435 | entries:entry Account_path.Map.t -> |
| 352 | labels:Labels.t -> | 436 | labels:Labels.t -> |
| 353 | (t, error) result | 437 | (t, error) result |
| 354 | 438 | ||
| @@ -364,7 +448,7 @@ end = struct | |||
| 364 | 448 | ||
| 365 | type t = { | 449 | type t = { |
| 366 | cleared : Date.t option; | 450 | cleared : Date.t option; |
| 367 | entries : entry Account.Path.Map.t; | 451 | entries : entry Account_path.Map.t; |
| 368 | labels : Labels.t; | 452 | labels : Labels.t; |
| 369 | } | 453 | } |
| 370 | [@@deriving sexp_of] | 454 | [@@deriving sexp_of] |
| @@ -394,87 +478,57 @@ type item = | |||
| 394 | type t = item list [@@deriving sexp_of] | 478 | type t = item list [@@deriving sexp_of] |
| 395 | 479 | ||
| 396 | module World = struct | 480 | module World = struct |
| 397 | type t = Account.world | 481 | type t = Account_hierarchy.world |
| 398 | 482 | ||
| 399 | let empty : t = String.Map.empty | 483 | let empty : t = String.Map.empty |
| 400 | 484 | ||
| 401 | let update_bal_fn = (f : | ||
| 402 | Account.Top_level_type.t -> | ||
| 403 | Commodity_id.t -> | ||
| 404 | Money.Diff.t -> | ||
| 405 | ('a * Money.Diff.t) option) | ||
| 406 | |||
| 407 | (* Stretching the type system a little :) *) | ||
| 408 | let rec update_bal_aux ttype subaid | ||
| 409 | (f : | ||
| 410 | Account.Top_level_type.t -> | ||
| 411 | Commodity_id.t -> | ||
| 412 | Money.Diff.t -> | ||
| 413 | ('a * Money.Diff.t) option) : 'a Account.Ft_mapper.t = | ||
| 414 | { | ||
| 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] | 485 | (** Update the balance (debit/credit ([dc])) of account [aid] [by_amount] |
| 443 | (commodity: [in_comm]) in [world], giving the updated world and the pre | 486 | (commodity: [in_comm]) in [world], giving the updated world and the pre |
| 444 | and post balances for [aid] iff the account exists in [world]. *) | 487 | and post balances for [aid] iff the account exists in [world]. *) |
| 445 | let update_bal aid dc by_amount in_comm (world : t) : | 488 | let update_bal aid dc by_amount in_comm (world : t) : |
| 446 | ((Money.Diff.t * Money.Diff.t) * t) option = | 489 | (Money.Diff.t * Money.Diff.t * t) option = |
| 447 | match aid with | 490 | let open Option.Let_syntax in |
| 448 | | [] -> None | 491 | let%bind mres, world' = |
| 449 | | aid0 :: subaid -> ( | 492 | Account_hierarchy.alter aid |
| 450 | let open Option.Let_syntax in | 493 | (fun acc_type acc_comm acc_bal -> |
| 451 | match%bind Map.find world aid0 with | 494 | if not ([%equal: Commodity_id.t] acc_comm in_comm) then (None, acc_bal) |
| 452 | | Asset acc -> | 495 | else |
| 453 | let diff = Money.Diff.of_amount by_amount dc ~on_debit:`Incr in | 496 | match acc_type with |
| 454 | let%map bals, acc' = (update_bal_aux subaid diff in_comm).car acc in | 497 | | Asset _ -> |
| 455 | (bals, Map.set world ~key:aid0 ~data:(Asset acc')) | 498 | let acc_bal' = |
| 456 | | Expense acc -> | 499 | Money.Diff.(acc_bal + of_amount by_amount dc ~on_debit:`Incr) |
| 457 | let diff = Money.Diff.of_amount by_amount dc ~on_debit:`Incr in | 500 | in |
| 458 | let%map bals, acc' = (update_bal_aux subaid diff in_comm).car acc in | 501 | (Some (acc_bal, acc_bal'), acc_bal') |
| 459 | (bals, Map.set world ~key:aid0 ~data:(Expense acc')) | 502 | | Expense _ -> |
| 460 | | Income acc -> | 503 | let acc_bal' = |
| 461 | let diff = Money.Diff.of_amount by_amount dc ~on_debit:`Decr in | 504 | Money.Diff.(acc_bal + of_amount by_amount dc ~on_debit:`Incr) |
| 462 | let%map bals, acc' = (update_bal_aux subaid diff in_comm).car acc in | 505 | in |
| 463 | (bals, Map.set world ~key:aid0 ~data:(Income acc')) | 506 | (Some (acc_bal, acc_bal'), acc_bal') |
| 464 | | Liability acc -> | 507 | | Income _ -> |
| 465 | let diff = Money.Diff.of_amount by_amount dc ~on_debit:`Decr in | 508 | let acc_bal' = |
| 466 | let%map bals, acc' = (update_bal_aux subaid diff in_comm).car acc in | 509 | Money.Diff.(acc_bal + of_amount by_amount dc ~on_debit:`Decr) |
| 467 | (bals, Map.set world ~key:aid0 ~data:(Liability acc')) | 510 | in |
| 468 | | Equity acc -> | 511 | (Some (acc_bal, acc_bal'), acc_bal') |
| 469 | let diff = Money.Diff.of_amount by_amount dc ~on_debit:`Decr in | 512 | | Liability _ -> |
| 470 | let%map bals, acc' = (update_bal_aux subaid diff in_comm).car acc in | 513 | let acc_bal' = |
| 471 | (bals, Map.set world ~key:aid0 ~data:(Equity acc'))) | 514 | Money.Diff.(acc_bal + of_amount by_amount dc ~on_debit:`Decr) |
| 515 | in | ||
| 516 | (Some (acc_bal, acc_bal'), acc_bal') | ||
| 517 | | Equity _ -> | ||
| 518 | let acc_bal' = | ||
| 519 | Money.Diff.(acc_bal + of_amount by_amount dc ~on_debit:`Decr) | ||
| 520 | in | ||
| 521 | (Some (acc_bal, acc_bal'), acc_bal')) | ||
| 522 | world | ||
| 523 | in | ||
| 524 | let%map pre_bal, post_bal = mres in | ||
| 525 | (pre_bal, post_bal, world') | ||
| 472 | 526 | ||
| 473 | let apply_tx (tx : Tx.t) world : t option = | 527 | let apply_tx (tx : Tx.t) world : t option = |
| 474 | Map.fold_option tx.entries ~init:world | 528 | Map.fold_option tx.entries ~init:world |
| 475 | ~f:(fun ~key:aid ~(data : Tx.entry) world -> | 529 | ~f:(fun ~key:aid ~(data : Tx.entry) world -> |
| 476 | let open Option.Let_syntax in | 530 | let open Option.Let_syntax in |
| 477 | let%bind (_old_bal, new_bal), world = | 531 | let%bind _old_bal, new_bal, world = |
| 478 | update_bal aid data.dc data.amount data.commodity world | 532 | update_bal aid data.dc data.amount data.commodity world |
| 479 | in | 533 | in |
| 480 | match data.assertion with | 534 | match data.assertion with |