Files
ezbookkeeping/pkg/converters/iif/iif_data_reader.go
T
2024-10-27 01:22:07 +08:00

236 lines
7.9 KiB
Go

package iif
import (
"bytes"
"encoding/csv"
"io"
"strings"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/log"
)
const iifAccountSampleLineSignColumnName = "!ACCNT"
const iifTransactionSampleLineSignColumnName = "!TRNS"
const iifTransactionSplitSampleLineSignColumnName = "!SPL"
const iifTransactionEndSampleLineSignColumnName = "!ENDTRNS"
const iifAccountLineSignColumnName = "ACCNT"
const iifTransactionLineSignColumnName = "TRNS"
const iifTransactionSplitLineSignColumnName = "SPL"
const iifTransactionEndLineSignColumnName = "ENDTRNS"
// iifDataReader defines the structure of intuit interchange format (iif) data reader
type iifDataReader struct {
reader *csv.Reader
}
// read returns the iif transaction dataset
func (r *iifDataReader) read(ctx core.Context) ([]*iifAccountDataset, []*iifTransactionDataset, error) {
allAccountDatasets := make([]*iifAccountDataset, 0)
allTransactionDatasets := make([]*iifTransactionDataset, 0)
currentDatasetType := ""
lastLineSign := ""
var currentAccountDataset *iifAccountDataset
var currentTransactionDataset *iifTransactionDataset
var currentTransactionData *iifTransactionData
for {
items, err := r.reader.Read()
if err == io.EOF {
break
}
if err != nil {
log.Errorf(ctx, "[iif_data_reader.read] cannot parse tsv data, because %s", err.Error())
return nil, nil, errs.ErrInvalidIIFFile
}
if len(items) == 1 && items[0] == "" {
continue
}
if len(items[0]) < 1 {
log.Errorf(ctx, "[iif_data_reader.read] line first column is empty")
return nil, nil, errs.ErrInvalidIIFFile
}
if items[0][0] == '!' { // sample line
if lastLineSign != "" {
log.Errorf(ctx, "[iif_data_reader.read] iif missing transaction end line")
return nil, nil, errs.ErrInvalidIIFFile
}
if currentAccountDataset != nil {
allAccountDatasets = append(allAccountDatasets, currentAccountDataset)
currentAccountDataset = nil
}
if currentTransactionDataset != nil {
allTransactionDatasets = append(allTransactionDatasets, currentTransactionDataset)
currentTransactionDataset = nil
}
if items[0] == iifTransactionSplitSampleLineSignColumnName || items[0] == iifTransactionEndSampleLineSignColumnName {
log.Errorf(ctx, "[iif_data_reader.read] read transaction split sample line or transaction end sample line sign before transaction sample line sign")
return nil, nil, errs.ErrInvalidIIFFile
} else {
currentDatasetType = items[0]
lastLineSign = ""
}
}
if currentDatasetType == "" {
log.Errorf(ctx, "[iif_data_reader.read] cannot read data line before sample line")
return nil, nil, errs.ErrInvalidIIFFile
} else if currentDatasetType == iifAccountSampleLineSignColumnName {
if currentAccountDataset == nil {
currentAccountDataset, err = r.readAccountSampleLine(ctx, items)
if err != nil {
return nil, nil, err
}
} else {
if items[0] == iifAccountLineSignColumnName {
accountData := &iifAccountData{
dataItems: items,
}
currentAccountDataset.accounts = append(currentAccountDataset.accounts, accountData)
} else {
log.Errorf(ctx, "[iif_data_reader.read] iif line expected reading account sign, but actual is \"%s\"", items[0])
return nil, nil, errs.ErrInvalidIIFFile
}
}
} else if currentDatasetType == iifTransactionSampleLineSignColumnName {
if currentTransactionDataset == nil {
currentTransactionDataset, err = r.readTransactionSampleLines(ctx, items)
if err != nil {
return nil, nil, err
}
} else {
if lastLineSign == "" {
if items[0] == iifTransactionLineSignColumnName {
currentTransactionData = &iifTransactionData{
dataItems: items,
splitData: make([]*iifTransactionSplitData, 0),
}
lastLineSign = items[0]
} else {
log.Errorf(ctx, "[iif_data_reader.read] iif line expected reading transaction sign, but actual is \"%s\"", items[0])
return nil, nil, errs.ErrInvalidIIFFile
}
} else if lastLineSign == iifTransactionLineSignColumnName || lastLineSign == iifTransactionSplitLineSignColumnName {
if items[0] == iifTransactionSplitLineSignColumnName {
currentTransactionData.splitData = append(currentTransactionData.splitData, &iifTransactionSplitData{
dataItems: items,
})
lastLineSign = items[0]
} else if items[0] == iifTransactionEndLineSignColumnName {
currentTransactionDataset.transactions = append(currentTransactionDataset.transactions, currentTransactionData)
lastLineSign = ""
} else {
log.Errorf(ctx, "[iif_data_reader.read] iif line expected reading split sign or transaction end sign, but actual is \"%s\"", items[0])
return nil, nil, errs.ErrInvalidIIFFile
}
} else {
log.Errorf(ctx, "[iif_data_reader.read] iif missing transaction sample end line")
return nil, nil, errs.ErrInvalidIIFFile
}
}
}
}
if lastLineSign != "" {
log.Errorf(ctx, "[iif_data_reader.read] iif missing transaction end line")
return nil, nil, errs.ErrInvalidIIFFile
}
if currentAccountDataset != nil {
allAccountDatasets = append(allAccountDatasets, currentAccountDataset)
}
if currentTransactionDataset != nil {
allTransactionDatasets = append(allTransactionDatasets, currentTransactionDataset)
}
return allAccountDatasets, allTransactionDatasets, nil
}
func (r *iifDataReader) readAccountSampleLine(ctx core.Context, items []string) (*iifAccountDataset, error) {
accountSampleItems := items
accountDataColumnIndexes := make(map[string]int, len(accountSampleItems))
for i := 1; i < len(accountSampleItems); i++ {
columnName := accountSampleItems[i]
accountDataColumnIndexes[columnName] = i
}
return &iifAccountDataset{
accountDataColumnIndexes: accountDataColumnIndexes,
accounts: make([]*iifAccountData, 0),
}, nil
}
func (r *iifDataReader) readTransactionSampleLines(ctx core.Context, items []string) (*iifTransactionDataset, error) {
transactionSampleItems := items
transactionDataColumnIndexes := make(map[string]int, len(transactionSampleItems))
for i := 1; i < len(transactionSampleItems); i++ {
columnName := transactionSampleItems[i]
transactionDataColumnIndexes[columnName] = i
}
splitSampleItems, err := r.reader.Read()
if err == io.EOF {
log.Errorf(ctx, "[iif_data_reader.readTransactionSampleLines] expected reading transaction split sample line, but read eof")
return nil, errs.ErrInvalidIIFFile
}
if len(splitSampleItems) < 1 || splitSampleItems[0] != iifTransactionSplitSampleLineSignColumnName {
log.Errorf(ctx, "[iif_data_reader.readTransactionSampleLines] expected reading transaction split sample line, but read \"%s\"", strings.Join(splitSampleItems, "\t"))
return nil, errs.ErrInvalidIIFFile
}
splitDataColumnIndexes := make(map[string]int, len(splitSampleItems))
for i := 1; i < len(splitSampleItems); i++ {
columnName := splitSampleItems[i]
splitDataColumnIndexes[columnName] = i
}
transactionEndSampleItems, err := r.reader.Read()
if err == io.EOF {
log.Errorf(ctx, "[iif_data_reader.readTransactionSampleLines] expected reading transaction end sample line, but read eof")
return nil, errs.ErrInvalidIIFFile
}
if len(transactionEndSampleItems) < 1 || transactionEndSampleItems[0] != iifTransactionEndSampleLineSignColumnName {
log.Errorf(ctx, "[iif_data_reader.readTransactionSampleLines] expected reading transaction end sample line, but read \"%s\"", strings.Join(splitSampleItems, "\t"))
return nil, errs.ErrInvalidIIFFile
}
return &iifTransactionDataset{
transactionDataColumnIndexes: transactionDataColumnIndexes,
splitDataColumnIndexes: splitDataColumnIndexes,
transactions: make([]*iifTransactionData, 0),
}, nil
}
func createNewIifDataReader(data []byte) *iifDataReader {
reader := bytes.NewReader(data)
csvReader := csv.NewReader(reader)
csvReader.Comma = '\t'
csvReader.FieldsPerRecord = -1
return &iifDataReader{
reader: csvReader,
}
}