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, } }