initial commit

This commit is contained in:
Hans Fast 2025-11-10 12:58:12 +01:00
commit 2283893f25
28 changed files with 857 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
process/
output/
.shastore

265
Sakefile.yaml Normal file
View File

@ -0,0 +1,265 @@
#using sake instead of just because need file outputs for dependencies ...
#! P=process
#! I=inputs
#! O=output
#! C=csvsql --no-inference --snifflimit 0 --query
#
#skip for demo: demo dataset already has normalized names.
# txns-names:
# help: normalize account names and fix missing names
# (ignore) doc: |-
# a case statement with a when for each variant present in the input data,
# becauase the payee field varies (e.g. 'Bob Fischer', 'B. Fischer').
# Then also add missing names (some bank transactions don't have a payee)
# (ignore) WARNING: |-
# might need to check the input data over time, as new variants occur... Is there some kind of warning I can build in to see if there are any UNKNOWNS left over?
# also, the second query, 'fix missing names', sets all remaining UNKNOWNs to Bank. Is it possible to do that so there's remaining UNKNOWNs to detect?
# dependencies:
# - "$I/bank-transactions.csv"
# - "fix_missing_names.sql"
# - "query_normalize_names.sql" #not present in this demo dataset
# formula: >-
# $C "normalize_names.sql"
# --query "fix_missing_names.sql"
# < "$I/bank-transactions.csv"
# > $P/txns-names.csv
# output:
# - $P/txns-names.csv
txns-fromto:
help: convert transactions to format 'from — amount — to'
dependencies:
# - $P/txns-names.csv
- $I/bank-transactions.csv
formula: >-
$C 'select id, date, name as "from",
amount,
"shared" as "to", description
from stdin where cast(amount as real) > 0
union select id, date, "shared" as "from",
-amount,
name as "to", description
from stdin where cast(amount as real) < 0
order by id, date;'
< $I/bank-transactions.csv
> $P/txns-fromto.csv
output:
- $P/txns-fromto.csv
#skip for demo. In the real dataset, kWh-meters:users is many:one so the meter usages have to be grouped and summed.
#months added to output file manually.
# kwh-usage:
# help: everyone's usage per period, including kWh used and months active in period.
# (ignore) doc: |
# in the real dataset, some households user multiple kWh meters. We need one kWh usage figure per account,
# so we have to sum the amount, grouped by account.
# dependencies:
# - $I/kwh-meters.csv
# - $I/months.csv
# formula: >-
# $C 'with vbwsum as
# (select "end" - "start" as usage, period, account from meters),
# vb as(select period, account, cast(sum(usage) as int) as usage
# from vbwsum
# group by period, account)
# select vb.*, months.months from vb
# inner join months on vb.period = months.period
# and vb.account = months.account;'
# --tables meters,months
# $I/kwh-meters.csv
# $I/months.csv
# > $P/kwh-usage.csv
# output:
# - $P/kwh-usage.csv
bank-costs:
help: extract totals for each supplier from initial transaction table.
(ignore) doc: |
This gets calculated so that it gets updated when bank transactions input file changes. Only Bank is actually used for now...
dependencies:
- $P/txns-fromto.csv
formula: >-
$C 'select "to" as account, cast(round(sum(amount), 2) as string) as amount from stdin
where "to" = "Bank"
group by "to";'
< $P/txns-fromto.csv
> $P/bank-costs.csv
output:
- $P/bank-costs.csv
total-val-txns:
help: reverse value transactions for electricity usage variable and fixed, and banking costs
(ignore) doc: |
Doing this calculation is the main premise of this whole pipeline.
To figure out how much all the users owe or are owed by the shared account,
we allocate the final costs of energy delivered (from the invoices) to each account
based on their proportion of kWh used (that gets done in the next step, indiv-val-txns).
And then in the step txns-with-generated, we add these as 'virtual' transactions in reverse, from the Energy company to the shared account,
and then from the shared account to the individual users.
If everyone's account is settled correctly, then in the final balance every account should be at zero.
dependencies:
- $I/collective-usage-expenses.csv
- $P/bank-costs.csv
formula: >-
$C '
with bank as (
select account || "total" as id, "2025-09-01" as date, account, amount as amount, "value of banking costs" as description
from sup where account = "Bank"
)
select period || account || "valvar" as id,
enddate as date,
account as "from",
exp_var as amount,
"shared" as "to",
"value of variable electricity expenses" as description
from elec
union all select period || account || "valfixed" as id,
enddate as date,
account as "from",
exp_fixed as amount,
"shared" as "to",
"value of fixed electricity expenses" as description
from elec
union all select id, date, account, amount, "shared", description from bank
order by id, date, account;'
--tables elec,sup
$I/collective-usage-expenses.csv
$P/bank-costs.csv
> $P/total-val-txns.csv
output:
- $P/total-val-txns.csv
#Javascript version: uses percent-divide.js
indiv-val-txns:
help: create txns for value consumed by each household
(ignore) doc: |
this step gets done in Javascript because sqlite can't do accurate floating point division.
We need to allocate a proportion of the total expenses to each user:
- collective energy usage * proportion of kWh per user
- banking costs * months the user was participating (Sarah joined in the last month of period 2).
based on
The math is still not 100% accurate, but it's good enough for the current purposes.
(the errors are to the order of 10e-10).
Possible improvement: use Big.js to do the arithmetic, though there might still be a final rounding error at the end.
dependencies:
- $I/collective-usage-expenses.csv
- $P/bank-costs.csv
- $I/kwh-usage.csv
- percent-divide.js
formula: >-
deno run -A percent-divide.js $I/collective-usage-expenses.csv $P/bank-costs.csv $I/kwh-usage.csv
> $P/indiv-val-txns.csv
output:
- $P/indiv-val-txns.csv
#Web Origami version: uses data.ori
#indiv-val-txns:
# help: create txns for value consumed by each household
# (ignore) doc: |
# this step gets done in Javascript because sqlite can't do accurate floating point division.
# We need to allocate a proportion of the total expenses to each user:
# - collective energy usage * proportion of kWh per user
# - banking costs * months the user was participating (Sarah joined in the last month of period 2).
# based on
# The math is still not 100% accurate, but it's good enough for the current purposes.
# (the errors are to the order of 10e-10).
# Possible improvement: use Big.js to do the arithmetic, though there might still be a final rounding error at the end.
# dependencies:
# - $I/collective-usage-expenses.csv
# - $P/bank-costs.csv
# - $I/kwh-usage.csv
# - data.ori
##there are more dependencies -- all the JS scripts used in data.ori -- but it doesn't make sense to include those all here.
# formula: >-
# ori 'Origami.csv data.ori/txns' > $P/indiv-val-txns.csv
# output:
# - $P/indiv-val-txns.csv
txns-with-other:
help: manually add txns which were not in input bank transaction csv
(ignore) doc: |
we add this in the 'fromto' step.
dependencies:
- $P/txns-fromto.csv
- $I/txns-other.csv
formula: >-
$C 'select * from "txns-fromto" union all
select * from "txns-other"
order by date, id;'
$P/txns-fromto.csv
$I/txns-other.csv
> $P/txns-with-other.csv
output:
- $P/txns-with-other.csv
txns-with-generated:
help: add the generated value transactions.
dependencies:
- $P/txns-with-other.csv
- $P/total-val-txns.csv
- $P/indiv-val-txns.csv
formula: >-
$C 'select * from "txns-with-other"
union all select * from "total-val-txns"
union all select * from "indiv-val-txns"
order by date, id;'
$P/txns-with-other.csv
$P/total-val-txns.csv
$P/indiv-val-txns.csv
> $P/txns-with-generated.csv
output:
- $P/txns-with-generated.csv
txns:
help: expand transactions to two rows per transaction.
(ignore) doc: By expanding the transactions to two entries, one for the 'from' account and one for the 'to' account, we can do double-entry accounting (lite)
dependencies:
- $P/txns-with-generated.csv
formula: >-
$C 'select id, date,
"from" as account,
-amount as amount,
description
from stdin union all
select id, date,
"to" as account,
amount,
description
from stdin
order by date, id;'
< $P/txns-with-generated.csv
> $P/txns.csv
output:
- $P/txns.csv
balance:
help: get total balance
dependencies:
- $P/txns.csv
formula: >-
$C 'with total as (
select "total" as account, sum(amount) as amount
from stdin
), pre as (
select account,
round(sum(amount),2) as amount
from stdin
where id is not null
group by account
)
select * from pre union all select * from total;'
< $P/txns.csv
>$O/balance.csv
output:
- $O/balance.csv

1
addUsageAmounts.js Normal file
View File

@ -0,0 +1 @@

15
bankersRound.js Normal file
View File

@ -0,0 +1,15 @@
//from https://stackoverflow.com/a/49080858
function bankersRound(n, d=2) {
var x = n * Math.pow(10, d);
var r = Math.round(x);
var br = Math.abs(x) % 1 === 0.5 ? (r % 2 === 0 ? r : r-1) : r;
return br / Math.pow(10, d);
}
//this function divides obj.valuekey by totalamt, putting the result rounded to two positions in obj.resultkeyname.
export default async function(obj, valuekeyname, resultkeyname, totalamt) {
obj[resultkeyname] = bankersRound(obj[valuekeyname] / 100 * parseFloat(totalamt));
return obj;
}

74
data.ori Normal file
View File

@ -0,0 +1,74 @@
{
//load input data: individual usage, banktotal and collective usage.
indivusage = ./inputs/kwh-usage.csv/
banktotal = Tree.filter(./process/bank-costs.csv/, entry => entry.account === 'ASN')[0].amount
collective_usage = ./inputs/collective-usage-expenses.csv/
//group collective usage by period
collective_usage_by_period = Tree.map(collective_usage, {key: (value, key) => value.period})
//group individual usage per account under period
indiv_by_period = Tree.groupBy(indivusage, line => line.period)
//group individual usage per period under account.
indiv_by_account = Tree.groupBy(indivusage, line => line.account)
//sum all users' months for all periods (one total sum)
totalmonths = Tree.mapReduce(indivusage, null, (lines) => lines.reduce((a,b) => a + parseInt(b.months),0))
//calculate percent for months and usage.
//roundUsage.js calculates each account's percentage of the total usage,
//roundMonths.js does the same for how many months each was active in the entire period
//since some people came and left partway through.
percent_months = Tree.map(indiv_by_period, roundMonths.js)
percent_usage = Tree.map(indiv_by_period, roundUsage.js)
//add percents to each entry and flatten (reverse the Tree.groupBy)
//the output of the roundX algorithm above is a bare array of percents.
//they need to be mapped back to the individual's entries using the array index,
//which is what withPercents.js does.
with_percents = Tree.map(
indiv_by_period,
(values, key) => withPercents.js(values, key, percent_usage, percent_months)
) →
Tree.deepValues → //object of arrays → array of arrays
(values) => values.flat() //array of arrays → flat array
//now calculate usage for fixed and variable electricity expenses using the percents
//For each record, add two new properties 'amount_fixed' and 'amount_var'
//bankersRound.js multiplies the percents by the total from the collective usage table,
//using the banker's rounding rule.
with_usage = with_percents/ → (withPercents) =>
Tree.map(withPercents, (record) => bankersRound.js(record, 'percent_months', 'amount_fixed', collective_usage_by_period[record.period].exp_fixed)) →
(withFixed) => Tree.map(withFixed, (record) => bankersRound.js(record, 'percent_usage', 'amount_var', collective_usage_by_period[record.period].exp_var))
//need to calculate a single banking costs amount for the whole period.
//It's not quite accurate, because banking costs have gone up over time, but it will do.
//this pipeline is akin to the with_percents one above, but this is not subdivided per period.
//hence the use of a difference script, withBankPercents.js, with a slightly different structure.
user_months = Tree.map(
indiv_by_account,
withMonths.js
)
//the array is placed under a key 'bank' for compatibility:
//in the original dataset, there were multiple suppliers that had to be accounted for,
//and the scripts expected that structure.
user_months_flat = Tree.deepValues(user_months) → (vals) => {'bank': vals.flat()}
percent_bank = Tree.map(user_months_flat, roundMonths.js)
with_bank_percents = Tree.map(
user_months_flat,
(values, key) => withBankPercents.js(values, key, percent_bank)
) → Tree.deepValues → (values) => values.flat()
with_bank_usage = Tree.map(
with_bank_percents, (record) => bankersRound.js(record, 'percent_bank', 'amount_bank', banktotal))
//the output records: convert with_usage and with_bank_usage to a csv representing transactions.
txns_elec = Tree.map(with_usage, outputFormat.js).flat()
txns_bank = Tree.map(with_bank_usage, outputFormatBank.js)
txns = Tree.deepMerge(txns_elec, txns_bank) → Tree.deepValues //→ (values) => values.flat()
}

115
dependencies.svg Normal file
View File

@ -0,0 +1,115 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 2.43.0 (0)
-->
<!-- Title: DependencyDiagram Pages: 1 -->
<svg width="425pt" height="404pt"
viewBox="0.00 0.00 425.34 404.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 400)">
<title>DependencyDiagram</title>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-400 421.34,-400 421.34,4 -4,4"/>
<!-- bank&#45;costs -->
<g id="node1" class="node">
<title>bank&#45;costs</title>
<ellipse fill="none" stroke="black" cx="175.44" cy="-306" rx="59.59" ry="18"/>
<text text-anchor="middle" x="175.44" y="-302.3" font-family="Times,serif" font-size="14.00">bank&#45;costs</text>
</g>
<!-- indiv&#45;val&#45;txns -->
<g id="node2" class="node">
<title>indiv&#45;val&#45;txns</title>
<ellipse fill="none" stroke="black" cx="73.44" cy="-234" rx="73.39" ry="18"/>
<text text-anchor="middle" x="73.44" y="-230.3" font-family="Times,serif" font-size="14.00">indiv&#45;val&#45;txns</text>
</g>
<!-- bank&#45;costs&#45;&gt;indiv&#45;val&#45;txns -->
<g id="edge1" class="edge">
<title>bank&#45;costs&#45;&gt;indiv&#45;val&#45;txns</title>
<path fill="none" stroke="black" d="M152.55,-289.29C138.71,-279.79 120.84,-267.53 105.59,-257.06"/>
<polygon fill="black" stroke="black" points="107.19,-253.92 96.97,-251.14 103.23,-259.69 107.19,-253.92"/>
</g>
<!-- total&#45;val&#45;txns -->
<g id="node3" class="node">
<title>total&#45;val&#45;txns</title>
<ellipse fill="none" stroke="black" cx="236.44" cy="-234" rx="71.49" ry="18"/>
<text text-anchor="middle" x="236.44" y="-230.3" font-family="Times,serif" font-size="14.00">total&#45;val&#45;txns</text>
</g>
<!-- bank&#45;costs&#45;&gt;total&#45;val&#45;txns -->
<g id="edge2" class="edge">
<title>bank&#45;costs&#45;&gt;total&#45;val&#45;txns</title>
<path fill="none" stroke="black" d="M189.9,-288.41C197.39,-279.82 206.68,-269.16 214.98,-259.63"/>
<polygon fill="black" stroke="black" points="217.74,-261.79 221.67,-251.96 212.46,-257.2 217.74,-261.79"/>
</g>
<!-- txns&#45;with&#45;generated -->
<g id="node4" class="node">
<title>txns&#45;with&#45;generated</title>
<ellipse fill="none" stroke="black" cx="236.44" cy="-162" rx="104.78" ry="18"/>
<text text-anchor="middle" x="236.44" y="-158.3" font-family="Times,serif" font-size="14.00">txns&#45;with&#45;generated</text>
</g>
<!-- indiv&#45;val&#45;txns&#45;&gt;txns&#45;with&#45;generated -->
<g id="edge3" class="edge">
<title>indiv&#45;val&#45;txns&#45;&gt;txns&#45;with&#45;generated</title>
<path fill="none" stroke="black" d="M108.41,-217.98C132.24,-207.75 164.05,-194.09 190,-182.95"/>
<polygon fill="black" stroke="black" points="191.46,-186.13 199.27,-178.97 188.7,-179.7 191.46,-186.13"/>
</g>
<!-- total&#45;val&#45;txns&#45;&gt;txns&#45;with&#45;generated -->
<g id="edge4" class="edge">
<title>total&#45;val&#45;txns&#45;&gt;txns&#45;with&#45;generated</title>
<path fill="none" stroke="black" d="M236.44,-215.7C236.44,-207.98 236.44,-198.71 236.44,-190.11"/>
<polygon fill="black" stroke="black" points="239.94,-190.1 236.44,-180.1 232.94,-190.1 239.94,-190.1"/>
</g>
<!-- txns -->
<g id="node5" class="node">
<title>txns</title>
<ellipse fill="none" stroke="black" cx="236.44" cy="-90" rx="30.59" ry="18"/>
<text text-anchor="middle" x="236.44" y="-86.3" font-family="Times,serif" font-size="14.00">txns</text>
</g>
<!-- txns&#45;with&#45;generated&#45;&gt;txns -->
<g id="edge8" class="edge">
<title>txns&#45;with&#45;generated&#45;&gt;txns</title>
<path fill="none" stroke="black" d="M236.44,-143.7C236.44,-135.98 236.44,-126.71 236.44,-118.11"/>
<polygon fill="black" stroke="black" points="239.94,-118.1 236.44,-108.1 232.94,-118.1 239.94,-118.1"/>
</g>
<!-- balance -->
<g id="node6" class="node">
<title>balance</title>
<ellipse fill="none" stroke="black" cx="236.44" cy="-18" rx="46.29" ry="18"/>
<text text-anchor="middle" x="236.44" y="-14.3" font-family="Times,serif" font-size="14.00">balance</text>
</g>
<!-- txns&#45;&gt;balance -->
<g id="edge5" class="edge">
<title>txns&#45;&gt;balance</title>
<path fill="none" stroke="black" d="M236.44,-71.7C236.44,-63.98 236.44,-54.71 236.44,-46.11"/>
<polygon fill="black" stroke="black" points="239.94,-46.1 236.44,-36.1 232.94,-46.1 239.94,-46.1"/>
</g>
<!-- txns&#45;fromto -->
<g id="node7" class="node">
<title>txns&#45;fromto</title>
<ellipse fill="none" stroke="black" cx="255.44" cy="-378" rx="64.99" ry="18"/>
<text text-anchor="middle" x="255.44" y="-374.3" font-family="Times,serif" font-size="14.00">txns&#45;fromto</text>
</g>
<!-- txns&#45;fromto&#45;&gt;bank&#45;costs -->
<g id="edge6" class="edge">
<title>txns&#45;fromto&#45;&gt;bank&#45;costs</title>
<path fill="none" stroke="black" d="M236.48,-360.41C226.05,-351.28 212.94,-339.81 201.55,-329.84"/>
<polygon fill="black" stroke="black" points="203.82,-327.18 193.99,-323.23 199.21,-332.45 203.82,-327.18"/>
</g>
<!-- txns&#45;with&#45;other -->
<g id="node8" class="node">
<title>txns&#45;with&#45;other</title>
<ellipse fill="none" stroke="black" cx="335.44" cy="-306" rx="81.79" ry="18"/>
<text text-anchor="middle" x="335.44" y="-302.3" font-family="Times,serif" font-size="14.00">txns&#45;with&#45;other</text>
</g>
<!-- txns&#45;fromto&#45;&gt;txns&#45;with&#45;other -->
<g id="edge7" class="edge">
<title>txns&#45;fromto&#45;&gt;txns&#45;with&#45;other</title>
<path fill="none" stroke="black" d="M274.4,-360.41C284.59,-351.5 297.32,-340.36 308.52,-330.56"/>
<polygon fill="black" stroke="black" points="311.12,-332.93 316.34,-323.71 306.51,-327.66 311.12,-332.93"/>
</g>
<!-- txns&#45;with&#45;other&#45;&gt;txns&#45;with&#45;generated -->
<g id="edge9" class="edge">
<title>txns&#45;with&#45;other&#45;&gt;txns&#45;with&#45;generated</title>
<path fill="none" stroke="black" d="M335.52,-287.77C334.81,-268.66 331.21,-237.68 316.44,-216 307.68,-203.13 294.66,-192.6 281.65,-184.41"/>
<polygon fill="black" stroke="black" points="283.09,-181.19 272.7,-179.11 279.52,-187.22 283.09,-181.19"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.0 KiB

7
fix_missing_names.sql Normal file
View File

@ -0,0 +1,7 @@
--2nd query of 2, using temp defined in earlier step.
--(not used in demo)
UPDATE temp set account = 'Bob' where id in (221130, 222138,222146);
UPDATE temp set account = 'Alice' where id = 231248;
update temp set account = 'Bank' where account = 'UNKNOWN';
select * from temp;

37
inputs/bank-temp.csv Normal file
View File

@ -0,0 +1,37 @@
;transactions
25018,2025-04-25,James,105,electricity advance may
25019,2025-04-25,Alice,90,electricity advance may
25020,2025-04-26,Bob,95,electricity advance may
25021,2025-04-27,ElecCo,-280,electricity advance may
25021a,2025-05-05,ElecCo,-120,extra payment for actual usage period 1
25021b,2025-05-25,Bank,-2,banking costs may
25022,2025-05-25,James,105,electricity advance june
25023,2025-05-25,Alice,90,electricity advance june
25024,2025-05-26,Bob,95,electricity advance june
25025,2025-05-27,ElecCo,-280,electricity advance june
25025b,2025-06-25,Bank,-2,banking costs june
25026,2025-06-25,James,105,electricity advance july
25027,2025-06-25,Alice,90,electricity advance july
25028,2025-06-26,Bob,95,electricity advance july
25029,2025-06-27,ElecCo,-280,electricity advance july
25029b,2025-07-25,Bank,-2,banking costs july
25030,2025-07-25,James,105,electricity advance august
25031,2025-07-25,Alice,90,electricity advance august
25032,2025-07-26,Bob,95,electricity advance august
25033,2025-07-26,Sarah,85,electricity advance august
25034,2025-07-27,ElecCo,-280,electricity advance august
25034b,2025-08-25,Bank,-2,banking costs august
25035,2025-09-05,ElecCo,200,reimbursement electricity advance period 2
;collective usage
2025b,2025-04-01,2025-08-31,552,368,920,ElecCo,
;kwh-usage
2025b,James,320,4
2025b,Alice,300,4
2025b,Bob,250,4
2025b,Sarah,5,1
1 ;transactions
2 25018,2025-04-25,James,105,electricity advance may
3 25019,2025-04-25,Alice,90,electricity advance may
4 25020,2025-04-26,Bob,95,electricity advance may
5 25021,2025-04-27,ElecCo,-280,electricity advance may
6 25021a,2025-05-05,ElecCo,-120,extra payment for actual usage period 1
7 25021b,2025-05-25,Bank,-2,banking costs may
8 25022,2025-05-25,James,105,electricity advance june
9 25023,2025-05-25,Alice,90,electricity advance june
10 25024,2025-05-26,Bob,95,electricity advance june
11 25025,2025-05-27,ElecCo,-280,electricity advance june
12 25025b,2025-06-25,Bank,-2,banking costs june
13 25026,2025-06-25,James,105,electricity advance july
14 25027,2025-06-25,Alice,90,electricity advance july
15 25028,2025-06-26,Bob,95,electricity advance july
16 25029,2025-06-27,ElecCo,-280,electricity advance july
17 25029b,2025-07-25,Bank,-2,banking costs july
18 25030,2025-07-25,James,105,electricity advance august
19 25031,2025-07-25,Alice,90,electricity advance august
20 25032,2025-07-26,Bob,95,electricity advance august
21 25033,2025-07-26,Sarah,85,electricity advance august
22 25034,2025-07-27,ElecCo,-280,electricity advance august
23 25034b,2025-08-25,Bank,-2,banking costs august
24 25035,2025-09-05,ElecCo,200,reimbursement electricity advance period 2
25 ;collective usage
26 2025b,2025-04-01,2025-08-31,552,368,920,ElecCo,
27 ;kwh-usage
28 2025b,James,320,4
29 2025b,Alice,300,4
30 2025b,Bob,250,4
31 2025b,Sarah,5,1

View File

@ -0,0 +1,51 @@
id,date,name,amount,description
25000a,2024-12-25,Bank,-2,banking costs december
25001,2024-12-25,James,105,electricity advance january
25002,2024-12-25,Alice,90,electricity advance january
25003,2424-12-26,Bob,-185,reimburse advance to ElecCo minus your advance to shared for this month
25003a,2025-01-25,Bank,-2,banking costs january
25005,2025-01-25,James,105,electricity advance february
25006,2025-01-25,Alice,90,electricity advance february
25007,2025-01-26,Bob,95,electricity advance february
25008,2025-01-27,ElecCo,-280,electricity advance february
25008a,2025-02-25,Bank,-2,banking costs february
25009,2025-02-25,James,105,electricity advance march
25010,2025-02-25,Alice,90,electricity advance march
25011,2025-02-26,Bob,95,electricity advance march
25012,2025-02-27,ElecCo,-280,electricity advance march
25012a,2025-03-25,Bank,-2,banking costs march
25013,2025-03-25,James,105,electricity advance april
25014,2025-03-25,Alice,90,electricity advance april
25015,2025-03-26,Bob,95,electricity advance april
25016,2025-03-27,ElecCo,-280,electricity advance april
25016a,2025-04-25,Bank,-2,banking costs april
25018,2025-04-25,James,105,electricity advance may
25019,2025-04-25,Alice,90,electricity advance may
25020,2025-04-26,Bob,95,electricity advance may
25021,2025-04-27,ElecCo,-280,electricity advance may
25016b,2025-05-01,James,53.25,extra payment for actual usage period 1
25016c,2025-05-01,Alice,39.33,extra payment for actual usage period 1
25016d,2025-05-01,Bob,77.41,extra payment for actual usage period 1
25021a,2025-05-05,ElecCo,-200,extra payment for actual usage period 1
25021b,2025-05-25,Bank,-2,banking costs may
25022,2025-05-25,James,105,electricity advance june
25023,2025-05-25,Alice,90,electricity advance june
25024,2025-05-26,Bob,95,electricity advance june
25025,2025-05-27,ElecCo,-280,electricity advance june
25025b,2025-06-25,Bank,-2,banking costs june
25026,2025-06-25,James,105,electricity advance july
25027,2025-06-25,Alice,90,electricity advance july
25028,2025-06-26,Bob,95,electricity advance july
25029,2025-06-27,ElecCo,-280,electricity advance july
25029b,2025-07-25,Bank,-2,banking costs july
25030,2025-07-25,James,105,electricity advance august
25031,2025-07-25,Alice,90,electricity advance august
25032,2025-07-26,Bob,95,electricity advance august
25033,2025-07-26,Sarah,85,electricity advance august
25034,2025-07-27,ElecCo,-280,electricity advance august
25034b,2025-08-25,Bank,-2,banking costs august
25035,2025-09-05,ElecCo,200,reimbursement too much advanced for period 2
25036,2025-09-06,James,-115.24,reimbursement advance period 2
25037,2025-09-06,Alice,-62.6,reimbursement advance period 2
25038,2025-09-06,Bob,-101,reimbursement advance period 2
25039,2025-09-06,Sarah,-38.14,reimbursement advance period 2
1 id date name amount description
2 25000a 2024-12-25 Bank -2 banking costs december
3 25001 2024-12-25 James 105 electricity advance january
4 25002 2024-12-25 Alice 90 electricity advance january
5 25003 2424-12-26 Bob -185 reimburse advance to ElecCo minus your advance to shared for this month
6 25003a 2025-01-25 Bank -2 banking costs january
7 25005 2025-01-25 James 105 electricity advance february
8 25006 2025-01-25 Alice 90 electricity advance february
9 25007 2025-01-26 Bob 95 electricity advance february
10 25008 2025-01-27 ElecCo -280 electricity advance february
11 25008a 2025-02-25 Bank -2 banking costs february
12 25009 2025-02-25 James 105 electricity advance march
13 25010 2025-02-25 Alice 90 electricity advance march
14 25011 2025-02-26 Bob 95 electricity advance march
15 25012 2025-02-27 ElecCo -280 electricity advance march
16 25012a 2025-03-25 Bank -2 banking costs march
17 25013 2025-03-25 James 105 electricity advance april
18 25014 2025-03-25 Alice 90 electricity advance april
19 25015 2025-03-26 Bob 95 electricity advance april
20 25016 2025-03-27 ElecCo -280 electricity advance april
21 25016a 2025-04-25 Bank -2 banking costs april
22 25018 2025-04-25 James 105 electricity advance may
23 25019 2025-04-25 Alice 90 electricity advance may
24 25020 2025-04-26 Bob 95 electricity advance may
25 25021 2025-04-27 ElecCo -280 electricity advance may
26 25016b 2025-05-01 James 53.25 extra payment for actual usage period 1
27 25016c 2025-05-01 Alice 39.33 extra payment for actual usage period 1
28 25016d 2025-05-01 Bob 77.41 extra payment for actual usage period 1
29 25021a 2025-05-05 ElecCo -200 extra payment for actual usage period 1
30 25021b 2025-05-25 Bank -2 banking costs may
31 25022 2025-05-25 James 105 electricity advance june
32 25023 2025-05-25 Alice 90 electricity advance june
33 25024 2025-05-26 Bob 95 electricity advance june
34 25025 2025-05-27 ElecCo -280 electricity advance june
35 25025b 2025-06-25 Bank -2 banking costs june
36 25026 2025-06-25 James 105 electricity advance july
37 25027 2025-06-25 Alice 90 electricity advance july
38 25028 2025-06-26 Bob 95 electricity advance july
39 25029 2025-06-27 ElecCo -280 electricity advance july
40 25029b 2025-07-25 Bank -2 banking costs july
41 25030 2025-07-25 James 105 electricity advance august
42 25031 2025-07-25 Alice 90 electricity advance august
43 25032 2025-07-26 Bob 95 electricity advance august
44 25033 2025-07-26 Sarah 85 electricity advance august
45 25034 2025-07-27 ElecCo -280 electricity advance august
46 25034b 2025-08-25 Bank -2 banking costs august
47 25035 2025-09-05 ElecCo 200 reimbursement too much advanced for period 2
48 25036 2025-09-06 James -115.24 reimbursement advance period 2
49 25037 2025-09-06 Alice -62.6 reimbursement advance period 2
50 25038 2025-09-06 Bob -101 reimbursement advance period 2
51 25039 2025-09-06 Sarah -38.14 reimbursement advance period 2

View File

@ -0,0 +1,3 @@
period,startdate,enddate,exp_fixed,exp_var,exp_total,account,notes
2025a,2025-01-01,2024-04-30,792,528,1320,ElecCo,
2025b,2025-04-01,2025-08-31,552,368,920,ElecCo,
1 period startdate enddate exp_fixed exp_var exp_total account notes
2 2025a 2025-01-01 2024-04-30 792 528 1320 ElecCo
3 2025b 2025-04-01 2025-08-31 552 368 920 ElecCo

2
inputs/kwh-meters.csv Normal file
View File

@ -0,0 +1,2 @@
period,meter,account,start,end
2025a,1,James,400,
1 period meter account start end
2 2025a 1 James 400

8
inputs/kwh-usage.csv Normal file
View File

@ -0,0 +1,8 @@
period,account,usage,months
2025a,James,500,4
2025a,Alice,320,4
2025a,Bob,470,4
2025b,James,320,4
2025b,Alice,300,4
2025b,Bob,250,4
2025b,Sarah,5,1
1 period account usage months
2 2025a James 500 4
3 2025a Alice 320 4
4 2025a Bob 470 4
5 2025b James 320 4
6 2025b Alice 300 4
7 2025b Bob 250 4
8 2025b Sarah 5 1

8
inputs/months.csv Normal file
View File

@ -0,0 +1,8 @@
period,account,months
2025a,James,4
2025a,Alice,4
2025a,Bob,4
2025b,James,4
2025b,Alice,4
2025b,Bob,4
2025b,Sarah,1
1 period account months
2 2025a James 4
3 2025a Alice 4
4 2025a Bob 4
5 2025b James 4
6 2025b Alice 4
7 2025b Bob 4
8 2025b Sarah 1

2
inputs/txns-other.csv Normal file
View File

@ -0,0 +1,2 @@
id,date,from,amount,to,description
25000,2024-12-15,Bob,280,ElecCo,first advance for january before shared account was active
1 id date from amount to description
2 25000 2024-12-15 Bob 280 ElecCo first advance for january before shared account was active

3
insert-percents.js Normal file
View File

@ -0,0 +1,3 @@
export default function(value, key, tree) {
}

8
outputFormat.js Normal file
View File

@ -0,0 +1,8 @@
export default async function(record) {
const rows = [];
const {account, amount_fixed, amount_var, period} = record;
rows.push({id: `${account}_valfixed_${period}`, date: '2025-09-01', from: 'shared', amount: amount_fixed, to: account, description: 'value transaction for fixed energy expenses'})
rows.push({id: `${account}_valvar_${period}`, date: '2025-09-01', from: 'shared', amount: amount_var, to: account, description: 'value transaction for var energy expenses'})
return rows;
}

7
outputFormatBank.js Normal file
View File

@ -0,0 +1,7 @@
export default async function(record) {
const rows = [];
const {account, amount_fixed, amount_bank, period} = record;
rows.push({id: `${account}_valbank`, date: '2025-09-01', from: 'shared', amount: amount_bank, to: account, description: 'value transaction for fixed energy expenses'})
return rows;
}

105
percent-divide.js Normal file
View File

@ -0,0 +1,105 @@
import { parse as parseCSV, stringify as stringifyCSV } from "jsr:@std/csv";
import percentRound from './percent-round.js';
const [totalexpensefile, suppliertotals, indivusagefile] = Deno.args;
const totexpinput = await Deno.readTextFile(totalexpensefile);
const supptotinput = await Deno.readTextFile(suppliertotals);
const supptots = await parseCSV(supptotinput, {skipFirstRow: true});
const asntot = supptots.find(e => e.account === 'Bank').amount;
const ivbinput = await Deno.readTextFile(indivusagefile);
const txp = await parseCSV(totexpinput, {skipFirstRow: true});
const ivb = await parseCSV(ivbinput, {skipFirstRow: true});
const vbByPeriod = Object.groupBy(ivb, i => i.period);
const totalMonths = ivb.reduce((a,b) => a + parseInt(b.months),0)
const vbRowsByUser = Object.groupBy(ivb, i => i.account);
const entriesWithPercents = [];
//values is an array of objects with props 'months' and 'usages'.
for (const [key, values] of Object.entries(vbByPeriod)) {
const months = values.map(p => p.months);
const percents_mo = percentRound(months, 4);
const usages = values.map(p => p.usage);
const percents_us = percentRound(usages, 0);
for (let [index, value] of values.entries()) {
value.percent_mo = percents_mo[index];
value.percent_us = percents_us[index];
entriesWithPercents.push(value);
}
}
const userMonthsTotal = [];
for (const [key, value] of Object.entries(vbRowsByUser)) {
const monthssum = value.reduce((a,b) => a + parseInt(b.months),0);
userMonthsTotal.push({account: key, months: monthssum});
}
//oh, for ASN I need the total percentage for the whole dataset.
//create a single record for each user at the end and add it to the array created for var expenses.
const global_mo_perc = percentRound(userMonthsTotal.map(t => t.months), 2);
for (const [index, value] of userMonthsTotal.entries()) {
value.percent_mo = global_mo_perc[index];
}
const asnExpSrc = userMonthsTotal.map((entry) => {
entry.amount = bankersRound(entry.percent_mo / 100 * asntot);
return entry;
})
const totExpByPeriod = {};
for (const p of txp) {
totExpByPeriod[p.period] = p;
}
const withElecExp = entriesWithPercents.map((entry) => {
entry.fixed_exp = bankersRound(entry.percent_mo / 100 * parseFloat(totExpByPeriod[entry.period].exp_fixed));
entry.date = totExpByPeriod[entry.period].enddate;
entry.var_exp = bankersRound(entry.percent_us / 100 * parseFloat(totExpByPeriod[entry.period].exp_var));
return entry;
})
// console.log(withElecExp);
//
// I guess I could check whether these amounts add up ...
const output = [];
for (const {period, account, fixed_exp, date, var_exp} of withElecExp) {
const omschrijving_var = 'value of variable expenses';
const omschrijving_fixed = 'value of fixed expenses';
//fixed
output.push({id: `${period}_${account}_fixed`, datum: date, from: 'shared', amount: fixed_exp, to: account , omschrijving: omschrijving_fixed});
//var
output.push({id: `${period}_${account}_var`, datum: date, from: 'shared', amount: var_exp, to: account, omschrijving: omschrijving_var});
}
//emhans: var: 75537, fixed 33746
for (const {account, amount} of asnExpSrc) {
output.push({id: `${account}_asn`, datum: '2025-12-31', from: 'shared', amount, to: account , omschrijving: 'value of banking costs ASN'});
}
console.log(stringifyCSV(output, {columns: ['id', 'datum', 'from', 'amount', 'to', 'omschrijving']} ).replace(/\n$/, ""));
//from https://stackoverflow.com/a/49080858
function bankersRound(n, d=2) {
var x = n * Math.pow(10, d);
var r = Math.round(x);
var br = Math.abs(x) % 1 === 0.5 ? (r % 2 === 0 ? r : r-1) : r;
return br / Math.pow(10, d);
}
function getRandomInt(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
}

88
percent-round.js Normal file
View File

@ -0,0 +1,88 @@
//source: https://github.com/super-ienien/percent-round
/*Copyright 2020 Vivien Anglesio
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
export default function percentRound(ipt, precision) {
if (!precision) {
precision = 0;
}
if (!Array.isArray(ipt)) {
throw new Error('percentRound input should be an Array');
}
const iptPercents = ipt.slice();
const length = ipt.length;
const out = new Array(length);
let total = 0;
for (let i = length - 1; i >= 0; i--) {
if (typeof iptPercents[i] === "string") {
iptPercents[i] = Number.parseFloat(iptPercents[i]);
}
total += iptPercents[i] * 1;
}
if (isNaN(total)) {
throw new Error('percentRound invalid input');
}
if (total === 0) {
out.fill(0);
} else {
const powPrecision = Math.pow(10, precision);
const pow100 = 100 * powPrecision;
let check100 = 0;
for (let i = length - 1; i >= 0; i--) {
iptPercents[i] = 100 * iptPercents[i] / total;//hpf: insert bankersRound here? no, I think it's not necessary in this case!
check100 += out[i] = Math.round(iptPercents[i] * powPrecision); //or here? Or does this subsume the need for bankers round?
}
if (check100 !== pow100) {
const totalDiff = (check100 - pow100) ;
const roundGrain = 1;
let grainCount = Math.abs(totalDiff);
const diffs = new Array(length);
for (let i = 0; i < length; i++) {
diffs[i] = Math.abs(out[i] - iptPercents[i] * powPrecision);
}
while (grainCount > 0) {
let idx = 0;
let maxDiff = diffs[0];
for (let i = 1; i < length; i++) {
if (maxDiff < diffs[i]) {
// avoid negative result
if (check100 > pow100 && out[i] - roundGrain < 0) {
continue;
}
idx = i;
maxDiff = diffs[i];
}
}
if (check100 > pow100) {
out[idx] -= roundGrain;
} else {
out[idx] += roundGrain;
}
diffs[idx] -= roundGrain;
grainCount--;
}
}
if (powPrecision > 1) {
for (let i = 0; i < length; i++) {
out[i] = out[i] / powPrecision;
}
}
}
return out;
}
// For es import compatibility
percentRound.default = percentRound;

8
percentRound.js Normal file
View File

@ -0,0 +1,8 @@
//a function to use percent-round in origami
import percentRound from './percent-round.js';
export default function (key) {
return function(value) {
return percentRound(value.map(v => v[key]), 3);
}
}

2
roundMonths.js Normal file
View File

@ -0,0 +1,2 @@
import percentRound from './percentRound.js';
export default percentRound('months');

12
roundMonthsArray.js Normal file
View File

@ -0,0 +1,12 @@
import percentRound from './percent-round.js';
//use percentRound on an AsyncTree array: first convert it to a normal array.
export default async function (asyncVals) {
const records = [];
for (const key of await asyncVals.keys()) {
const record = await asyncVals.get(key);
records.push(record.months);
}
console.log(records);
const foobar = percentRound(records, 3)
console.log(foobar);
}

3
roundUsage.js Normal file
View File

@ -0,0 +1,3 @@
import percentRound from './percentRound.js';
export default percentRound('usage');

4
subtree.ori Normal file
View File

@ -0,0 +1,4 @@
{
foo = 'bar'
bananas = data.ori/boo + 'biz'
}

11
withBankPercents.js Normal file
View File

@ -0,0 +1,11 @@
//this is actually a more general function: can create the key name dynamically.
export default async function(values, key, percentarray) {
const percents = await percentarray.get(key);
// return {...value, percent_bank: bankp}
return values.map(async (value, index) => {;
value['percent_'+key.substring(0,key.length -1)] = await percents[index];
return value;
})
}

1
withMonthPercents.js Normal file
View File

@ -0,0 +1 @@
//calculat

5
withMonths.js Normal file
View File

@ -0,0 +1,5 @@
//calculate total months for each user (out of entire data period)
export default async function (records, account) {
const months = records.reduce((a,b) => a + parseInt(b.months),0);
return {account, months};
}

9
withPercents.js Normal file
View File

@ -0,0 +1,9 @@
export default async function(values, period, percent_usage, percent_months) {
const usagepercents = await percent_usage.get(period);
const monthpercents = await percent_months.get(period);
return values.map((value, index) => {
value.percent_months = monthpercents[index];
value.percent_usage = usagepercents[index];
return value;
})
}