diff --git a/README.md b/README.md index b00b8c7..267e2da 100644 --- a/README.md +++ b/README.md @@ -145,9 +145,14 @@ alipay: - item: 相互宝 targetAccount: Expenses:Insurance - - method: 余额 # 余额/余额宝 + - method: 余额 + fullMatch: true + methodAccount: Assets:Alipay + - method: 余额宝 + fullMatch: true methodAccount: Assets:Alipay - method: 招商银行(9876) + fullMatch: true methodAccount: Assets:Bank:CN:CMB-9876:Savings - type: 收入 # 其他转账收款 @@ -199,11 +204,11 @@ alipay: `alipay` is the provider-specific configuration. Alipay provider has rules matching mechanism. `alipay` 蚂蚁账单相关的配置。它提供基于规则的匹配。可以指定: -- `peer`(交易对方)的包含匹配。 -- `item`(商品说明)的包含匹配。 -- `type`(收/支)的包含匹配。 -- `method`(收/付款方式)的包含匹配。 -- `category`(交易分类)的包含匹配。 +- `peer`(交易对方)的完全/包含匹配。 +- `item`(商品说明)的完全/包含匹配。 +- `type`(收/支)的完全/包含匹配。 +- `method`(收/付款方式)的完全/包含匹配。 +- `category`(交易分类)的完全/包含匹配。 - `time`(交易时间)的区间匹配。 > 交易时间可写为以下两种形式: > - `11:00-13:00` @@ -212,6 +217,8 @@ alipay: 在单条规则中可以使用分隔符(sep)填写多个关键字,在同一对象中,每个关键字之间是或的关系。 +在单条规则中可以使用 `fullMatch` 来设置字符匹配规则,`true` 表示使用完全匹配(full match),`false` 表示使用包含匹配(partial match),不设置该项则默认使用包含匹配。 + 匹配成功则使用规则中定义的 `targetAccount` 、 `methodAccount` 等账户覆盖默认定义账户。 规则匹配的顺序是:从 `rules` 配置中的第一条开始匹配,如果匹配成功仍继续匹配。也就是后面的规则优先级要**高于**前面的规则。 @@ -272,6 +279,12 @@ wechat: sep: ',' time: 16:30-21:30 targetAccount: Expenses:Food:Meal:Dinner + - peer: 餐厅 + time: 23:55-00:10 # test T+1 + targetAccount: Expenses:Food:Meal:MidNight + - peer: 餐厅 + time: 23:50-00:05 # test T-1 + targetAccount: Expenses:Food:Meal:MidNight - peer: 房东 type: 支出 @@ -291,7 +304,11 @@ wechat: - method: / # 一般为收入,存入零钱 methodAccount: Assets:Digital:Wechat:Cash - - method: 零钱 # 零钱/零钱通 + - method: 零钱 + fullMatch: true + methodAccount: Assets:Digital:Wechat:Cash + - method: 零钱通 + fullMatch: true methodAccount: Assets:Digital:Wechat:Cash - method: 工商银行 methodAccount: Assets:Bank:CN:ICBC:Savings @@ -309,11 +326,11 @@ wechat: `wechat` is the provider-specific configuration. WeChat provider has rules matching mechanism. `wechat` 是微信相关的配置。它提供基于规则的匹配。可以指定: -- `peer`(交易对方)的包含匹配。 -- `item`(商品名称)的包含匹配。 -- `type`(收/支)的包含匹配。 -- `txType`(交易类型)的包含匹配。 -- `method`(支付方式)的包含匹配。 +- `peer`(交易对方)的完全/包含匹配。 +- `item`(商品名称)的完全/包含匹配。 +- `type`(收/支)的完全/包含匹配。 +- `txType`(交易类型)的完全/包含匹配。 +- `method`(支付方式)的完全/包含匹配。 - `time`(交易时间)的区间匹配。 > 交易时间可写为以下两种形式: > - `11:00-13:00` @@ -322,6 +339,8 @@ wechat: 在单条规则中可以使用分隔符(sep)填写多个关键字,在同一对象中,每个关键字之间是或的关系。 +在单条规则中可以使用 `fullMatch` 来设置字符匹配规则,`true` 表示使用完全匹配(full match),`false` 表示使用包含匹配(partial match),不设置该项则默认使用包含匹配。 + 匹配成功则使用规则中定义的 `targetAccount` 、 `methodAccount` 等账户覆盖默认定义账户。 规则匹配的顺序是:从 `rules` 配置中的第一条开始匹配,如果匹配成功仍继续匹配。也就是后面的规则优先级要**高于**前面的规则。 @@ -354,6 +373,7 @@ huobi: - item: BTC/USDT,BTC1S/USDT # multiple keywords with separator type: 币币交易 txType: 买入 + fullMatch: true sep: ',' # define separator as a comma cashAccount: Assets:Rule1:Cash positionAccount: Assets:Rule1:Positions @@ -368,9 +388,9 @@ huobi: `huobi` is the provider-specific configuration. Huobi provider has rules matching mechanism. `huobi` 是火币相关的配置。它提供基于规则的匹配。可以指定: -- `item`(交易对)的包含匹配。 -- `type`(交易类型)的包含匹配。 -- `txType`(交易方向)的包含匹配。 +- `item`(交易对)的完全/包含匹配。 +- `type`(交易类型)的完全/包含匹配。 +- `txType`(交易方向)的完全/包含匹配。 - `time`(交易时间)的区间匹配。 > 交易时间可写为以下两种形式: > - `11:00-13:00` @@ -379,6 +399,8 @@ huobi: 在单条规则中可以使用分隔符(sep)填写多个关键字,在同一对象中,每个关键字之间是或的关系。 +在单条规则中可以使用 `fullMatch` 来设置字符匹配规则,`true` 表示使用完全匹配(full match),`false` 表示使用包含匹配(partial match),不设置该项则默认使用包含匹配。 + 匹配成功则使用规则中定义的 `cashAccount`, `positionAccount`, `commissionAccount` 和 `pnlAccount` 覆盖默认定义。 规则匹配的顺序是:从 `rules` 配置中的第一条开始匹配,如果匹配成功仍继续匹配。也就是后面的规则优先级要**高于**前面的规则。 diff --git a/example/alipay/config.yaml b/example/alipay/config.yaml index 765ac91..26f17ce 100644 --- a/example/alipay/config.yaml +++ b/example/alipay/config.yaml @@ -20,9 +20,14 @@ alipay: - item: 相互宝 targetAccount: Expenses:Insurance - - method: 余额 # 余额/余额宝 + - method: 余额 + fullMatch: true + methodAccount: Assets:Alipay + - method: 余额宝 + fullMatch: true methodAccount: Assets:Alipay - method: 招商银行(9876) + fullMatch: true methodAccount: Assets:Bank:CN:CMB-9876:Savings - type: 收入 # 其他转账收款 diff --git a/example/huobi/config.yaml b/example/huobi/config.yaml index 12000c2..31b40f1 100644 --- a/example/huobi/config.yaml +++ b/example/huobi/config.yaml @@ -9,6 +9,7 @@ huobi: - item: BTC/USDT,BTC1S/USDT type: 币币交易 txType: 买入 + fullMatch: true sep: ',' cashAccount: Assets:Rule1:Cash positionAccount: Assets:Rule1:Positions diff --git a/example/wechat/config.yaml b/example/wechat/config.yaml index 37d2cdf..0690e83 100644 --- a/example/wechat/config.yaml +++ b/example/wechat/config.yaml @@ -59,7 +59,11 @@ wechat: - method: / # 一般为收入,存入零钱 methodAccount: Assets:Digital:Wechat:Cash - - method: 零钱 # 零钱/零钱通 + - method: 零钱 + fullMatch: true + methodAccount: Assets:Digital:Wechat:Cash + - method: 零钱通 + fullMatch: true methodAccount: Assets:Digital:Wechat:Cash - method: 工商银行 methodAccount: Assets:Bank:CN:ICBC:Savings diff --git a/pkg/analyser/alipay/alipay.go b/pkg/analyser/alipay/alipay.go index 3c40103..026dfc8 100644 --- a/pkg/analyser/alipay/alipay.go +++ b/pkg/analyser/alipay/alipay.go @@ -55,20 +55,26 @@ func (a Alipay) GetAccounts(o *ir.Order, cfg *config.Config, target, provider st if r.Separator != nil { sep = *r.Separator } + + matchFunc := util.SplitFindContains + if r.FullMatch { + matchFunc = util.SplitFindEquals + } + if r.Peer != nil { - match = util.SplitFindContains(*r.Peer, o.Peer, sep, match) + match = matchFunc(*r.Peer, o.Peer, sep, match) } if r.Type != nil { - match = util.SplitFindContains(*r.Type, o.TxTypeOriginal, sep, match) + match = matchFunc(*r.Type, o.TxTypeOriginal, sep, match) } if r.Item != nil { - match = util.SplitFindContains(*r.Item, o.Item, sep, match) + match = matchFunc(*r.Item, o.Item, sep, match) } if r.Method != nil { - match = util.SplitFindContains(*r.Method, o.Method, sep, match) + match = matchFunc(*r.Method, o.Method, sep, match) } if r.Category != nil { - match = util.SplitFindContains(*r.Category, o.Category, sep, match) + match = matchFunc(*r.Category, o.Category, sep, match) } if r.Time != nil { match, err = util.SplitFindTimeInterval(*r.Time, o.PayTime, match) diff --git a/pkg/analyser/huobi/huobi.go b/pkg/analyser/huobi/huobi.go index 2f5b2e8..5d4cf38 100644 --- a/pkg/analyser/huobi/huobi.go +++ b/pkg/analyser/huobi/huobi.go @@ -63,14 +63,20 @@ func (h Huobi) GetAccounts(o *ir.Order, cfg *config.Config, target, provider str if r.Seperator != nil { sep = *r.Seperator } + + matchFunc := util.SplitFindContains + if r.FullMatch { + matchFunc = util.SplitFindEquals + } + if r.Type != nil { - match = util.SplitFindContains(*r.Type, o.TypeOriginal, sep, match) + match = matchFunc(*r.Type, o.TypeOriginal, sep, match) } if r.TxType != nil { - match = util.SplitFindContains(*r.TxType, o.TxTypeOriginal, sep, match) + match = matchFunc(*r.TxType, o.TxTypeOriginal, sep, match) } if r.Item != nil { - match = util.SplitFindContains(*r.Item, o.Item, sep, match) + match = matchFunc(*r.Item, o.Item, sep, match) } if r.Time != nil { match, err = util.SplitFindTimeInterval(*r.Time, o.PayTime, match) diff --git a/pkg/analyser/wechat/wechat.go b/pkg/analyser/wechat/wechat.go index 51b3da9..32f104b 100644 --- a/pkg/analyser/wechat/wechat.go +++ b/pkg/analyser/wechat/wechat.go @@ -65,20 +65,26 @@ func (w Wechat) GetAccounts(o *ir.Order, cfg *config.Config, target, provider st if r.Seperator != nil { sep = *r.Seperator } + + matchFunc := util.SplitFindContains + if r.FullMatch { + matchFunc = util.SplitFindEquals + } + if r.Peer != nil { - match = util.SplitFindContains(*r.Peer, o.Peer, sep, match) + match = matchFunc(*r.Peer, o.Peer, sep, match) } if r.Type != nil { - match = util.SplitFindContains(*r.Type, o.TxTypeOriginal, sep, match) + match = matchFunc(*r.Type, o.TxTypeOriginal, sep, match) } if r.TxType != nil { - match = util.SplitFindContains(*r.TxType, o.TypeOriginal, sep, match) + match = matchFunc(*r.TxType, o.TypeOriginal, sep, match) } if r.Method != nil { - match = util.SplitFindContains(*r.Method, o.Method, sep, match) + match = matchFunc(*r.Method, o.Method, sep, match) } if r.Item != nil { - match = util.SplitFindContains(*r.Item, o.Item, sep, match) + match = matchFunc(*r.Item, o.Item, sep, match) } if r.Time != nil { match, err = util.SplitFindTimeInterval(*r.Time, o.PayTime, match) diff --git a/pkg/provider/alipay/config.go b/pkg/provider/alipay/config.go index 741563c..2b3b08c 100644 --- a/pkg/provider/alipay/config.go +++ b/pkg/provider/alipay/config.go @@ -33,4 +33,5 @@ type Rule struct { MethodAccount *string `mapstructure:"methodAccount,omitempty"` TargetAccount *string `mapstructure:"targetAccount,omitempty"` PnlAccount *string `mapstructure:"pnlAccount,omitempty"` + FullMatch bool `mapstructure:"fullMatch,omitempty"` } diff --git a/pkg/provider/huobi/config.go b/pkg/provider/huobi/config.go index f90ef9b..e6d2d3d 100644 --- a/pkg/provider/huobi/config.go +++ b/pkg/provider/huobi/config.go @@ -15,4 +15,5 @@ type Rule struct { PositionAccount *string `mapstructure:"positionAccount,omitempty"` CommissionAccount *string `mapstructure:"commissionAccount,omitempty"` PnlAccount *string `mapstructure:"pnlAccount,omitempty"` + FullMatch bool `mapstructure:"fullMatch,omitempty"` } diff --git a/pkg/provider/wechat/config.go b/pkg/provider/wechat/config.go index 375b0de..8b31dde 100644 --- a/pkg/provider/wechat/config.go +++ b/pkg/provider/wechat/config.go @@ -33,4 +33,5 @@ type Rule struct { MethodAccount *string `mapstructure:"methodAccount,omitempty"` TargetAccount *string `mapstructure:"targetAccount,omitempty"` CommissionAccount *string `mapstructure:"commissionAccount,omitempty"` + FullMatch bool `mapstructure:"fullMatch,omitempty"` } diff --git a/pkg/util/util.go b/pkg/util/util.go index 538c9db..a76efda 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -20,6 +20,19 @@ func SplitFindContains(str, target, sep string, match bool) bool { return isContain && match } +func SplitFindEquals(str, target, sep string, match bool) bool { + ss := strings.Split(str, sep) + isEqual := false + for _, s := range ss { + if target == s { + isEqual = true + break + } + } + + return isEqual && match +} + func SplitFindTimeInterval(timeStr string, targetTime time.Time, match bool) (bool, error) { isContain := false diff --git a/test/huobi-test.sh b/test/huobi-test.sh new file mode 100755 index 0000000..18df41a --- /dev/null +++ b/test/huobi-test.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# +# E2E test for huobi provider. + +# set -x # debug +set -eo errexit + +TEST_DIR=`dirname "$(realpath $0)"` +ROOT_DIR="$TEST_DIR/.." + +make -f "$ROOT_DIR/Makefile" build +mkdir -p "$ROOT_DIR/test/output" + +# generate huobi bills output in beancount format +"$ROOT_DIR/bin/double-entry-generator" translate \ + --provider huobi \ + --config "$ROOT_DIR/example/huobi/config.yaml" \ + --output "$ROOT_DIR/test/output/test-huobi-output.beancount" \ + "$ROOT_DIR/example/huobi/example-huobi-records.csv" + +diff -u --color \ + "$ROOT_DIR/example/huobi/example-huobi-output.beancount" \ + "$ROOT_DIR/test/output/test-huobi-output.beancount" + +if [ $? -ne 0 ]; then + echo "[FAIL] Huobi provider output is different from expected output." + exit 1 +fi + +echo "[PASS] All Huobi provider tests!"