我的 cli foo 的一个弱点是awk
。我可能可以通过精心编写的脚本来解决以下问题,但我很确定awk
这是完成这项工作的最佳工具,而且对于我的一生来说,我无法找出正确的方法。
假设我有一个像这样的数据文件(Ledger):
2019/05/31 (MMEX948) Gürmar
Assets:Cash:Marina ₺-28,14
Expenses:Food:Groceries:Meat ₺28,14
Assets:Cash:Marina ₺-28,14
Expenses:Food:Groceries:Meat ₺28,14
Assets:Cash:Marina ₺-3,45
Expenses:Food:Groceries:Basic ₺3,45
Assets:Cash:Marina ₺-15,00
Expenses:Food:Groceries:Produce ₺15,00
2019/06/01 (MMEX932) A101
Assets:Cash:Caleb $-3.00
Assets:Cash:Marina $-2.50
Expenses:Food:Groceries:Basic $5.50
2019/06/01 (MMEX931) Şemikler Pazar Yeri
Assets:Cash:Marina ₺-24,00
Expenses:Food:Groceries:Basic ₺24,00
Assets:Cash:Marina ₺-31,00
Expenses:Food:Groceries:Meat ₺31,00
Assets:Cash:Marina ₺-65,00
Expenses:Food:Groceries:Produce ₺65,00
每个空行分隔的段落是一个交易,每条缩进线是一个发帖,每个帖子都有一个帐户和数量(至少用 2 个空格分隔)。
我希望这些数据发生两件事。我不在乎这些是否发生在同一个命令中,根据工具的不同,一两遍可能会更容易完成......
所有负金额的过账应排列在正金额的过账之后。
任何负金额和重复帐户的过帐都应合并。理想情况下,将对金额进行求和,但由于货币格式的原因,这确实很复杂,并且没有必要,因为我可以重新生成金额行。只要每次传递合并的唯一帐户不超过一个,从合并的过帐中完全删除该金额就足够了。
结果应该是这样的:
2019/05/31 (MMEX948) Gürmar
Expenses:Food:Groceries:Meat ₺28,14
Expenses:Food:Groceries:Meat ₺28,14
Expenses:Food:Groceries:Basic ₺3,45
Expenses:Food:Groceries:Produce ₺15,00
Assets:Cash:Marina
2019/06/01 (MMEX932) A101
Expenses:Food:Groceries:Basic $5.50
Assets:Cash:Marina $-2.50
Assets:Cash:Caleb
2019/06/01 (MMEX931) Şemikler Pazar Yeri
Expenses:Food:Groceries:Basic ₺24,00
Expenses:Food:Groceries:Meat ₺31,00
Expenses:Food:Groceries:Produce ₺65,00
Assets:Cash:Marina
注释使得这比仅仅扫描重复项稍微复杂一些:
- 在第一笔交易中,有两个重复的不同账户。只有其中之一应该合并并清除(可以将两者合并,但每次只能合并一个,否则我将无法修复金额)。
- 在中间交易中没有什么可以合并的,但是盲目地清除所有负数交易中的金额将是错误的。由于没有合并,所以根本不需要清除,但是可以如果这样可以更容易处理。
我将如何解决这个问题awk
?或者,如果 Awk 不是最佳解决方案,那么什么才是?在大多数脚本语言(perl、python、zsh)中,我会解析所有内容,将其全部放入多维数组中,然后根据金额的正则表达式匹配进行排序,其次根据帐户的 alpha 进行排序,然后迭代它以输出它,始终删除最后的金额并仅合并最后的重复项(如果有)。
请注意,前几天我确实找到了一种在 Awk 中解析和合并重复事务的方法:
awk 'NF { if (/^20/) { if (last != $$0) print "\n" $$0; last = $$0 } else { print $$0 } }' |
但现在更复杂的 awk 逻辑正在挑战我。
答案1
这个 GNU awk 脚本对我有用:
#! /usr/local/bin/awk -f
BEGIN { FS = "[[:space:]][[:space:]]+" }
function dump() {
for (acct in post) { # dump unmerged postings of current transaction
if (post[acct])
print post[acct];
}
if (merged) { # dump merged posting, if any
printf " %s\n", merged
}
merged = ""; # clear variables for next round
delete post;
txn = "";
}
!NF && txn { # blank line, end of transaction
dump();
print;
next
}
END { # end-of-file, print merged postings of last txn
dump();
}
!txn { # new transaction
txn = $0;
print;
next
}
{
acct = $2;
amt = $3
}
amt ~ /-/ { # negative amounts, keep for later
if (acct in post) { # duplicate entry
if (!merged || merged == acct) { # only merge and clear one duplicate account
post[acct] = "";
merged = acct;
}
else # tack on to existing record without merging
post[acct] = post[acct] "\n" $0
}
else
post[acct] = $0
next
}
1
行动中:
~ ./foo.awk foo
2019/05/31 (MMEX948) Gürmar
Expenses:Food:Groceries:Meat ₺28,14
Expenses:Food:Groceries:Meat ₺28,14
Expenses:Food:Groceries:Basic ₺3,45
Expenses:Food:Groceries:Produce ₺15,00
Assets:Cash:Marina
2019/06/01 (MMEX932) A101
Expenses:Food:Groceries:Basic $5.50
Assets:Cash:Marina $-2.50
Assets:Cash:Caleb $-3.00
2019/06/01 (MMEX931) Şemikler Pazar Yeri
Expenses:Food:Groceries:Basic ₺24,00
Expenses:Food:Groceries:Meat ₺31,00
Expenses:Food:Groceries:Produce ₺65,00
Assets:Cash:Marina
答案2
使用 GNU awk 进行 gensub()、数组的数组和sorted_in:
$ cat tst.awk
BEGIN { RS=""; FS="\n"; localeDecPt="."; PROCINFO["sorted_in"]="@val_num_desc" }
{
delete sum
print $1
denom = gensub(/.*([^0-9.,-]).+$/,"\\1",1,$2)
for (i=2; i<=NF; i++) {
account = gensub(/[[:space:]]+[^[:space:]]+$/,"",1,$i)
amount = gensub(/.*[^0-9.,-](.+)$/,"\\1",1,$i)
inputDecPt = gensub(/[0-9-]+/,"","g",amount)
sum[account] += gensub("["inputDecPt"]",localeDecPt,"g",amount)
}
for (account in sum) {
amount = denom gensub("["localeDecPt"]",inputDecPt,"g",sprintf("%0.2f",sum[account]))
printf "%-*s%*s\n", 40, account, 10, amount
}
print ""
}
。
$ awk -f tst.awk file
2019/05/31 (MMEX948) Gürmar
Expenses:Food:Groceries:Meat ₺56,28
Expenses:Food:Groceries:Produce ₺15,00
Expenses:Food:Groceries:Basic ₺3,45
Assets:Cash:Marina ₺-74,73
2019/06/01 (MMEX932) A101
Expenses:Food:Groceries:Basic $5.50
Assets:Cash:Marina $-2.50
Assets:Cash:Caleb $-3.00
2019/06/01 (MMEX931) Şemikler Pazar Yeri
Expenses:Food:Groceries:Produce ₺65,00
Expenses:Food:Groceries:Meat ₺31,00
Expenses:Food:Groceries:Basic ₺24,00
Assets:Cash:Marina ₺-120,00
如果.
小数点不是您所在区域中的小数点,则只需更改localeDecPt="."
为任何小数点即可。如果您的输入金额包含逗号作为千位分隔符,那么我发布的代码将无法工作,您应该提供包含要测试的输入。我将输出字段宽度硬编码为 40 和 10 - 您可以相当轻松地计算每个字段的最大宽度并使用它,或者使用制表符作为 OFS 并将输出通过管道传输到,column
但它看起来并不像其中任何一个' d 有必要。
老实说,我不明白您对合并内容以及如何识别重复项的要求(例如为什么不在第一笔交易中合并所有重复项以及为什么在第二笔交易中清除一个非重复帐户中的金额?)所以我只是合并所有重复项的金额并保留非重复项的金额。如果这对您不起作用,请澄清您问题中的要求。