From d15a862e5b97bd3ca36afb2b62c2ea8bb4b96ec3 Mon Sep 17 00:00:00 2001 From: MaysWind Date: Thu, 12 Sep 2024 00:00:22 +0800 Subject: [PATCH] support importing feidee mymoney web export data --- go.mod | 2 + go.sum | 6 + ..._transaction_data_excel_file_data_table.go | 229 ++++++++++++++++++ ...oney_transaction_data_xls_file_importer.go | 50 ++++ ...transaction_data_xls_file_importer_test.go | 102 ++++++++ pkg/converters/transaction_data_converters.go | 2 + pkg/errs/converter.go | 1 + pkg/utils/datetimes.go | 3 +- pkg/utils/datetimes_test.go | 2 +- pkg/utils/regexps.go | 24 +- pkg/utils/regexps_test.go | 117 +++++++++ src/consts/file.js | 5 + src/locales/en.json | 2 + src/locales/zh_Hans.json | 2 + testdata/feidee_mymoney_test_file.xls | Bin 0 -> 33280 bytes third-party-dependencies.json | 5 + 16 files changed, 547 insertions(+), 5 deletions(-) create mode 100644 pkg/converters/feidee_mymoney_transaction_data_excel_file_data_table.go create mode 100644 pkg/converters/feidee_mymoney_transaction_data_xls_file_importer.go create mode 100644 pkg/converters/feidee_mymoney_transaction_data_xls_file_importer_test.go create mode 100644 testdata/feidee_mymoney_test_file.xls diff --git a/go.mod b/go.mod index 2a12ae27..cd87488a 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/minio/minio-go/v7 v7.0.74 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/pquerna/otp v1.4.0 + github.com/shakinm/xlsReader v0.9.12 github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.9.0 github.com/urfave/cli/v2 v2.27.3 @@ -58,6 +59,7 @@ require ( github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/memcachier/mc/v3 v3.0.3 // indirect + github.com/metakeule/fmtdate v1.1.2 // indirect github.com/minio/md5-simd v1.1.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect diff --git a/go.sum b/go.sum index 3028dd3c..7bb8296b 100644 --- a/go.sum +++ b/go.sum @@ -93,6 +93,8 @@ github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/memcachier/mc/v3 v3.0.3 h1:qii+lDiPKi36O4Xg+HVKwHu6Oq+Gt17b+uEiA0Drwv4= github.com/memcachier/mc/v3 v3.0.3/go.mod h1:GzjocBahcXPxt2cmqzknrgqCOmMxiSzhVKPOe90Tpug= +github.com/metakeule/fmtdate v1.1.2 h1:n9M7H9HfAqp+6OA98wXGMdcAr6omshSNVct65Bks1lQ= +github.com/metakeule/fmtdate v1.1.2/go.mod h1:2JyMFlKxeoGy1qS6obQukT0AL0Y4iNANQL8scbSdT4E= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= github.com/minio/minio-go/v7 v7.0.74 h1:fTo/XlPBTSpo3BAMshlwKL5RspXRv9us5UeHEGYCFe0= @@ -121,6 +123,8 @@ github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shakinm/xlsReader v0.9.12 h1:F6GWYtCzfzQqdIuqZJ0MU3YJ7uwH1ofJtmTKyWmANQk= +github.com/shakinm/xlsReader v0.9.12/go.mod h1:ME9pqIGf+547L4aE4YTZzwmhsij+5K9dR+k84OO6WSs= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -165,8 +169,10 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= diff --git a/pkg/converters/feidee_mymoney_transaction_data_excel_file_data_table.go b/pkg/converters/feidee_mymoney_transaction_data_excel_file_data_table.go new file mode 100644 index 00000000..2a2d0e71 --- /dev/null +++ b/pkg/converters/feidee_mymoney_transaction_data_excel_file_data_table.go @@ -0,0 +1,229 @@ +package converters + +import ( + "bytes" + "time" + + "github.com/shakinm/xlsReader/xls" + + "github.com/mayswind/ezbookkeeping/pkg/errs" + "github.com/mayswind/ezbookkeeping/pkg/utils" +) + +// feideeMymoneyTransactionExcelFileDataTable defines the structure of feidee mymoney transaction plain text data table +type feideeMymoneyTransactionExcelFileDataTable struct { + workbook *xls.Workbook + headerLineColumnNames []string +} + +// feideeMymoneyTransactionExcelFileDataRow defines the structure of feidee mymoney transaction plain text data row +type feideeMymoneyTransactionExcelFileDataRow struct { + sheet *xls.Sheet + rowIndex int +} + +// feideeMymoneyTransactionExcelFileDataRowIterator defines the structure of feidee mymoney transaction plain text data row iterator +type feideeMymoneyTransactionExcelFileDataRowIterator struct { + dataTable *feideeMymoneyTransactionExcelFileDataTable + currentTableIndex int + currentRowIndexInTable int +} + +// DataRowCount returns the total count of data row +func (t *feideeMymoneyTransactionExcelFileDataTable) DataRowCount() int { + allSheets := t.workbook.GetSheets() + totalDataRowCount := 0 + + for i := 0; i < len(allSheets); i++ { + sheet := allSheets[i] + + if sheet.GetNumberRows() <= 1 { + continue + } + + totalDataRowCount += sheet.GetNumberRows() - 1 + } + + return totalDataRowCount +} + +// HeaderLineColumnNames returns the header column name list +func (t *feideeMymoneyTransactionExcelFileDataTable) HeaderLineColumnNames() []string { + return t.headerLineColumnNames +} + +// DataRowIterator returns the iterator of data row +func (t *feideeMymoneyTransactionExcelFileDataTable) DataRowIterator() ImportedDataRowIterator { + return &feideeMymoneyTransactionExcelFileDataRowIterator{ + dataTable: t, + currentTableIndex: 0, + currentRowIndexInTable: 0, + } +} + +// ColumnCount returns the total count of column in this data row +func (r *feideeMymoneyTransactionExcelFileDataRow) ColumnCount() int { + row, err := r.sheet.GetRow(r.rowIndex) + + if err != nil { + return 0 + } + + return len(row.GetCols()) +} + +// GetData returns the data in the specified column index +func (r *feideeMymoneyTransactionExcelFileDataRow) GetData(columnIndex int) string { + row, err := r.sheet.GetRow(r.rowIndex) + + if err != nil { + return "" + } + + cell, err := row.GetCol(columnIndex) + + if err != nil { + return "" + } + + return cell.GetString() +} + +// GetTime returns the time in the specified column index +func (r *feideeMymoneyTransactionExcelFileDataRow) GetTime(columnIndex int, timezoneOffset int16) (time.Time, error) { + str := r.GetData(columnIndex) + + if utils.IsValidLongDateTimeFormat(str) { + return utils.ParseFromLongDateTime(str, timezoneOffset) + } + + if utils.IsValidLongDateTimeWithoutSecondFormat(str) { + return utils.ParseFromLongDateTimeWithoutSecond(str, timezoneOffset) + } + + if utils.IsValidLongDateFormat(str) { + return utils.ParseFromLongDateTimeWithoutSecond(str+" 00:00", timezoneOffset) + } + + return time.Unix(0, 0), errs.ErrTransactionTimeInvalid +} + +// GetTimezoneOffset returns the time zone offset in the specified column index +func (r *feideeMymoneyTransactionExcelFileDataRow) GetTimezoneOffset(columnIndex int) (*time.Location, error) { + return nil, errs.ErrNotSupported +} + +// HasNext returns whether the iterator does not reach the end +func (t *feideeMymoneyTransactionExcelFileDataRowIterator) HasNext() bool { + allSheets := t.dataTable.workbook.GetSheets() + + if t.currentTableIndex >= len(allSheets) { + return false + } + + currentSheet := allSheets[t.currentTableIndex] + + if t.currentRowIndexInTable+1 < currentSheet.GetNumberRows() { + return true + } + + for i := t.currentTableIndex + 1; i < len(allSheets); i++ { + sheet := allSheets[i] + + if sheet.GetNumberRows() <= 1 { + continue + } + + return true + } + + return false +} + +// Next returns the next imported data row +func (t *feideeMymoneyTransactionExcelFileDataRowIterator) Next() ImportedDataRow { + allSheets := t.dataTable.workbook.GetSheets() + currentRowIndexInTable := t.currentRowIndexInTable + + for i := t.currentTableIndex; i < len(allSheets); i++ { + sheet := allSheets[i] + + if currentRowIndexInTable+1 < sheet.GetNumberRows() { + t.currentRowIndexInTable++ + currentRowIndexInTable = t.currentRowIndexInTable + break + } + + t.currentTableIndex++ + t.currentRowIndexInTable = 0 + currentRowIndexInTable = 0 + } + + if t.currentTableIndex >= len(allSheets) { + return nil + } + + currentSheet := allSheets[t.currentTableIndex] + + if t.currentRowIndexInTable >= currentSheet.GetNumberRows() { + return nil + } + + return &feideeMymoneyTransactionExcelFileDataRow{ + sheet: ¤tSheet, + rowIndex: t.currentRowIndexInTable, + } +} + +func createNewFeideeMymoneyTransactionExcelFileDataTable(data []byte) (*feideeMymoneyTransactionExcelFileDataTable, error) { + reader := bytes.NewReader(data) + workbook, err := xls.OpenReader(reader) + + if err != nil { + return nil, err + } + + allSheets := workbook.GetSheets() + var headerRowItems []string + + for i := 0; i < len(allSheets); i++ { + sheet := allSheets[i] + + if sheet.GetNumberRows() < 1 { + continue + } + + row, err := sheet.GetRow(0) + + if err != nil { + return nil, err + } + + cells := row.GetCols() + + if i == 0 { + for j := 0; j < len(cells); j++ { + headerItem := cells[j].GetString() + + if headerItem == "" { + break + } + + headerRowItems = append(headerRowItems, headerItem) + } + } else { + for j := 0; j < min(len(cells), len(headerRowItems)); j++ { + headerItem := cells[j].GetString() + + if headerItem != headerRowItems[j] { + return nil, errs.ErrFieldsInMultiTableAreDifferent + } + } + } + } + + return &feideeMymoneyTransactionExcelFileDataTable{ + workbook: &workbook, + headerLineColumnNames: headerRowItems, + }, nil +} diff --git a/pkg/converters/feidee_mymoney_transaction_data_xls_file_importer.go b/pkg/converters/feidee_mymoney_transaction_data_xls_file_importer.go new file mode 100644 index 00000000..455be6f2 --- /dev/null +++ b/pkg/converters/feidee_mymoney_transaction_data_xls_file_importer.go @@ -0,0 +1,50 @@ +package converters + +import ( + "github.com/mayswind/ezbookkeeping/pkg/core" + "github.com/mayswind/ezbookkeeping/pkg/models" +) + +// feideeMymoneyTransactionDataXlsImporter defines the structure of feidee mymoney xls importer for transaction data +type feideeMymoneyTransactionDataXlsImporter struct { + DataTableTransactionDataImporter +} + +var feideeMymoneyDataColumnNameMapping = map[DataTableColumn]string{ + DATA_TABLE_TRANSACTION_TIME: "日期", + DATA_TABLE_TRANSACTION_TYPE: "交易类型", + DATA_TABLE_CATEGORY: "分类", + DATA_TABLE_SUB_CATEGORY: "子分类", + DATA_TABLE_ACCOUNT_NAME: "账户1", + DATA_TABLE_AMOUNT: "金额", + DATA_TABLE_RELATED_ACCOUNT_NAME: "账户2", + DATA_TABLE_DESCRIPTION: "备注", +} + +var feideeMymoneyTransactionTypeNameMapping = map[models.TransactionType]string{ + models.TRANSACTION_TYPE_MODIFY_BALANCE: "余额变更", + models.TRANSACTION_TYPE_INCOME: "收入", + models.TRANSACTION_TYPE_EXPENSE: "支出", + models.TRANSACTION_TYPE_TRANSFER: "转账", +} + +// Initialize an feidee mymoney transaction data xls file importer singleton instance +var ( + FeideeMymoneyTransactionDataXlsImporter = &feideeMymoneyTransactionDataXlsImporter{ + DataTableTransactionDataImporter{ + dataColumnMapping: feideeMymoneyDataColumnNameMapping, + transactionTypeMapping: feideeMymoneyTransactionTypeNameMapping, + }, + } +) + +// ParseImportedData returns the imported data by parsing the feidee mymoney transaction xls data +func (c *feideeMymoneyTransactionDataXlsImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezoneOffset int16, accountMap map[string]*models.Account, categoryMap map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionTag, error) { + dataTable, err := createNewFeideeMymoneyTransactionExcelFileDataTable(data) + + if err != nil { + return nil, nil, nil, nil, err + } + + return c.parseImportedData(ctx, user, dataTable, defaultTimezoneOffset, accountMap, categoryMap, tagMap) +} diff --git a/pkg/converters/feidee_mymoney_transaction_data_xls_file_importer_test.go b/pkg/converters/feidee_mymoney_transaction_data_xls_file_importer_test.go new file mode 100644 index 00000000..ffb1f3f8 --- /dev/null +++ b/pkg/converters/feidee_mymoney_transaction_data_xls_file_importer_test.go @@ -0,0 +1,102 @@ +package converters + +import ( + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/mayswind/ezbookkeeping/pkg/core" + "github.com/mayswind/ezbookkeeping/pkg/models" + "github.com/mayswind/ezbookkeeping/pkg/utils" +) + +func TestFeideeMymoneyTransactionDataXlsImporterParseImportedData_MinimumValidData(t *testing.T) { + converter := FeideeMymoneyTransactionDataXlsImporter + context := core.NewNullContext() + + user := &models.User{ + Uid: 1234567890, + DefaultCurrency: "CNY", + } + + testdata, err := os.ReadFile("../../testdata/feidee_mymoney_test_file.xls") + assert.Nil(t, err) + + allNewTransactions, allNewAccounts, allNewSubCategories, allNewTags, err := converter.ParseImportedData(context, user, testdata, 0, nil, nil, nil) + assert.Nil(t, err) + + assert.Equal(t, 6, len(allNewTransactions)) + assert.Equal(t, 2, len(allNewAccounts)) + assert.Equal(t, 5, len(allNewSubCategories)) + assert.Equal(t, 0, len(allNewTags)) + + assert.Equal(t, int64(1234567890), allNewTransactions[0].Uid) + assert.Equal(t, models.TRANSACTION_DB_TYPE_MODIFY_BALANCE, allNewTransactions[0].Type) + assert.Equal(t, "2024-09-01 00:00:00", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime), time.UTC)) + assert.Equal(t, int64(12345), allNewTransactions[0].Amount) + assert.Equal(t, "Test Account", allNewTransactions[0].OriginalSourceAccountName) + assert.Equal(t, "", allNewTransactions[0].OriginalCategoryName) + + assert.Equal(t, int64(1234567890), allNewTransactions[1].Uid) + assert.Equal(t, models.TRANSACTION_DB_TYPE_INCOME, allNewTransactions[1].Type) + assert.Equal(t, "2024-09-01 01:23:45", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[1].TransactionTime), time.UTC)) + assert.Equal(t, int64(12), allNewTransactions[1].Amount) + assert.Equal(t, "Test Account", allNewTransactions[1].OriginalSourceAccountName) + assert.Equal(t, "Test Category", allNewTransactions[1].OriginalCategoryName) + + assert.Equal(t, int64(1234567890), allNewTransactions[2].Uid) + assert.Equal(t, models.TRANSACTION_DB_TYPE_EXPENSE, allNewTransactions[2].Type) + assert.Equal(t, "2024-09-01 12:34:56", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[2].TransactionTime), time.UTC)) + assert.Equal(t, int64(100), allNewTransactions[2].Amount) + assert.Equal(t, "Test Account2", allNewTransactions[2].OriginalSourceAccountName) + assert.Equal(t, "Test Category2", allNewTransactions[2].OriginalCategoryName) + + assert.Equal(t, int64(1234567890), allNewTransactions[3].Uid) + assert.Equal(t, models.TRANSACTION_DB_TYPE_TRANSFER_OUT, allNewTransactions[3].Type) + assert.Equal(t, "2024-09-01 23:59:59", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[3].TransactionTime), time.UTC)) + assert.Equal(t, int64(5), allNewTransactions[3].Amount) + assert.Equal(t, "Test Comment5", allNewTransactions[3].Comment) + assert.Equal(t, "Test Account", allNewTransactions[3].OriginalSourceAccountName) + assert.Equal(t, "Test Account2", allNewTransactions[3].OriginalDestinationAccountName) + assert.Equal(t, "Test Category3", allNewTransactions[3].OriginalCategoryName) + + assert.Equal(t, int64(1234567890), allNewTransactions[4].Uid) + assert.Equal(t, models.TRANSACTION_DB_TYPE_INCOME, allNewTransactions[4].Type) + assert.Equal(t, "2024-09-10 00:00:00", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[4].TransactionTime), time.UTC)) + assert.Equal(t, int64(-54300), allNewTransactions[4].Amount) + assert.Equal(t, "Test Account2", allNewTransactions[4].OriginalSourceAccountName) + assert.Equal(t, "Test Category5", allNewTransactions[4].OriginalCategoryName) + + assert.Equal(t, int64(1234567890), allNewTransactions[5].Uid) + assert.Equal(t, models.TRANSACTION_DB_TYPE_EXPENSE, allNewTransactions[5].Type) + assert.Equal(t, "2024-09-11 05:06:00", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[5].TransactionTime), time.UTC)) + assert.Equal(t, int64(-12340), allNewTransactions[5].Amount) + assert.Equal(t, "Line1\nLine2", allNewTransactions[5].Comment) + assert.Equal(t, "Test Account", allNewTransactions[5].OriginalSourceAccountName) + assert.Equal(t, "Test Category4", allNewTransactions[5].OriginalCategoryName) + + assert.Equal(t, int64(1234567890), allNewAccounts[0].Uid) + assert.Equal(t, "Test Account", allNewAccounts[0].Name) + assert.Equal(t, "CNY", allNewAccounts[0].Currency) + + assert.Equal(t, int64(1234567890), allNewAccounts[1].Uid) + assert.Equal(t, "Test Account2", allNewAccounts[1].Name) + assert.Equal(t, "CNY", allNewAccounts[1].Currency) + + assert.Equal(t, int64(1234567890), allNewSubCategories[0].Uid) + assert.Equal(t, "Test Category", allNewSubCategories[0].Name) + + assert.Equal(t, int64(1234567890), allNewSubCategories[1].Uid) + assert.Equal(t, "Test Category5", allNewSubCategories[1].Name) + + assert.Equal(t, int64(1234567890), allNewSubCategories[2].Uid) + assert.Equal(t, "Test Category2", allNewSubCategories[2].Name) + + assert.Equal(t, int64(1234567890), allNewSubCategories[3].Uid) + assert.Equal(t, "Test Category4", allNewSubCategories[3].Name) + + assert.Equal(t, int64(1234567890), allNewSubCategories[4].Uid) + assert.Equal(t, "Test Category3", allNewSubCategories[4].Name) +} diff --git a/pkg/converters/transaction_data_converters.go b/pkg/converters/transaction_data_converters.go index ca2918e6..6d2bd962 100644 --- a/pkg/converters/transaction_data_converters.go +++ b/pkg/converters/transaction_data_converters.go @@ -19,6 +19,8 @@ func GetTransactionDataImporter(fileType string) (TransactionDataImporter, error return EzBookKeepingTransactionDataCSVFileConverter, nil } else if fileType == "ezbookkeeping_tsv" { return EzBookKeepingTransactionDataTSVFileConverter, nil + } else if fileType == "feidee_mymoney_xls" { + return FeideeMymoneyTransactionDataXlsImporter, nil } else { return nil, errs.ErrImportFileTypeNotSupported } diff --git a/pkg/errs/converter.go b/pkg/errs/converter.go index 2ee94748..72518301 100644 --- a/pkg/errs/converter.go +++ b/pkg/errs/converter.go @@ -15,4 +15,5 @@ var ( ErrDestinationAccountNameCannotBeBlank = NewNormalError(NormalSubcategoryConverter, 8, http.StatusBadRequest, "destination account name cannot be blank") ErrAmountInvalid = NewNormalError(NormalSubcategoryConverter, 9, http.StatusBadRequest, "transaction amount is invalid") ErrGeographicLocationInvalid = NewNormalError(NormalSubcategoryConverter, 10, http.StatusBadRequest, "geographic location is invalid") + ErrFieldsInMultiTableAreDifferent = NewNormalError(NormalSubcategoryConverter, 11, http.StatusBadRequest, "fields in multiple table headers are different") ) diff --git a/pkg/utils/datetimes.go b/pkg/utils/datetimes.go index 9ea5897e..1ec7bf60 100644 --- a/pkg/utils/datetimes.go +++ b/pkg/utils/datetimes.go @@ -136,7 +136,8 @@ func ParseFromLongDateTime(t string, utcOffset int16) (time.Time, error) { } // ParseFromLongDateTimeWithoutSecond parses a formatted string in long date time format (no second) -func ParseFromLongDateTimeWithoutSecond(t string, timezone *time.Location) (time.Time, error) { +func ParseFromLongDateTimeWithoutSecond(t string, utcOffset int16) (time.Time, error) { + timezone := time.FixedZone("Timezone", int(utcOffset)*60) return time.ParseInLocation(longDateTimeWithoutSecondFormat, t, timezone) } diff --git a/pkg/utils/datetimes_test.go b/pkg/utils/datetimes_test.go index 21882fe1..b503d133 100644 --- a/pkg/utils/datetimes_test.go +++ b/pkg/utils/datetimes_test.go @@ -133,7 +133,7 @@ func TestParseFromLongDateTime(t *testing.T) { func TestParseFromLongDateTimeWithoutSecond(t *testing.T) { expectedValue := int64(1691947440) - actualTime, err := ParseFromLongDateTimeWithoutSecond("2023-08-13 17:24", time.UTC) + actualTime, err := ParseFromLongDateTimeWithoutSecond("2023-08-13 17:24", 0) assert.Equal(t, nil, err) actualValue := actualTime.Unix() diff --git a/pkg/utils/regexps.go b/pkg/utils/regexps.go index 19a54f19..6f2ad057 100644 --- a/pkg/utils/regexps.go +++ b/pkg/utils/regexps.go @@ -3,9 +3,12 @@ package utils import "regexp" var ( - usernamePattern = regexp.MustCompile("^(?i)[a-z0-9_-]+$") - emailPattern = regexp.MustCompile("^(?i)(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21-\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)\\])$") - hexRGBColorPattern = regexp.MustCompile("^(?i)([0-9a-f]{6}|[0-9a-f]{3})$") + usernamePattern = regexp.MustCompile("^(?i)[a-z0-9_-]+$") + emailPattern = regexp.MustCompile("^(?i)(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21-\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)\\])$") + hexRGBColorPattern = regexp.MustCompile("^(?i)([0-9a-f]{6}|[0-9a-f]{3})$") + longDateTimePattern = regexp.MustCompile("^([1-9][0-9]{3})-(0[1-9]|1[0-2])-(0[1-9]|1[0-9]|2[0-9]|3[01]) ([0-1][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])$") + longDateTimeWithoutSecondPattern = regexp.MustCompile("^([1-9][0-9]{3})-(0[1-9]|1[0-2])-(0[1-9]|1[0-9]|2[0-9]|3[01]) ([0-1][0-9]|2[0-3]):([0-5][0-9])$") + longDatePattern = regexp.MustCompile("^([1-9][0-9]{3})-(0[1-9]|1[0-2])-(0[1-9]|1[0-9]|2[0-9]|3[01])$") ) // IsValidUsername reports whether username is valid @@ -22,3 +25,18 @@ func IsValidEmail(email string) bool { func IsValidHexRGBColor(color string) bool { return hexRGBColorPattern.MatchString(color) } + +// IsValidLongDateTimeFormat reports whether long date time is valid format +func IsValidLongDateTimeFormat(datetime string) bool { + return longDateTimePattern.MatchString(datetime) +} + +// IsValidLongDateTimeWithoutSecondFormat reports long date time without seconds is valid format +func IsValidLongDateTimeWithoutSecondFormat(datetime string) bool { + return longDateTimeWithoutSecondPattern.MatchString(datetime) +} + +// IsValidLongDateFormat reports long date is valid format +func IsValidLongDateFormat(date string) bool { + return longDatePattern.MatchString(date) +} diff --git a/pkg/utils/regexps_test.go b/pkg/utils/regexps_test.go index b7b3f03c..3da8d5af 100644 --- a/pkg/utils/regexps_test.go +++ b/pkg/utils/regexps_test.go @@ -112,3 +112,120 @@ func TestIsValidHexRGBColor_InvalidHexRGBColor(t *testing.T) { actualValue = IsValidHexRGBColor(color) assert.Equal(t, expectedValue, actualValue) } + +func TestIsValidLongDateTimeFormat_ValidLongDateTimeFormat(t *testing.T) { + datetime := "2024-09-01 12:34:56" + expectedValue := true + actualValue := IsValidLongDateTimeFormat(datetime) + assert.Equal(t, expectedValue, actualValue) + + datetime = "2024-10-01 00:00:00" + expectedValue = true + actualValue = IsValidLongDateTimeFormat(datetime) + assert.Equal(t, expectedValue, actualValue) + + datetime = "9999-12-31 23:59:59" + expectedValue = true + actualValue = IsValidLongDateTimeFormat(datetime) + assert.Equal(t, expectedValue, actualValue) +} + +func TestIsValidLongDateTimeFormat_InvalidLongDateTimeFormat(t *testing.T) { + datetime := "2024-09-01" + expectedValue := false + actualValue := IsValidLongDateTimeFormat(datetime) + assert.Equal(t, expectedValue, actualValue) + + datetime = "2024-09-01 12" + expectedValue = false + actualValue = IsValidLongDateTimeFormat(datetime) + assert.Equal(t, expectedValue, actualValue) + + datetime = "2024-09-01 12:34" + expectedValue = false + actualValue = IsValidLongDateTimeFormat(datetime) + assert.Equal(t, expectedValue, actualValue) +} + +func TestIsValidLongDateTimeWithoutSecondFormat_ValidLongDateTimeWithoutSecondFormat(t *testing.T) { + datetime := "2024-09-01 12:34" + expectedValue := true + actualValue := IsValidLongDateTimeWithoutSecondFormat(datetime) + assert.Equal(t, expectedValue, actualValue) + + datetime = "2024-10-01 00:00" + expectedValue = true + actualValue = IsValidLongDateTimeWithoutSecondFormat(datetime) + assert.Equal(t, expectedValue, actualValue) + + datetime = "9999-12-31 23:59" + expectedValue = true + actualValue = IsValidLongDateTimeWithoutSecondFormat(datetime) + assert.Equal(t, expectedValue, actualValue) +} + +func TestIsValidLongDateTimeWithoutSecondFormat_InvalidLongDateTimeWithoutSecondFormat(t *testing.T) { + datetime := "2024-09-01" + expectedValue := false + actualValue := IsValidLongDateTimeWithoutSecondFormat(datetime) + assert.Equal(t, expectedValue, actualValue) + + datetime = "2024-09-01 12" + expectedValue = false + actualValue = IsValidLongDateTimeWithoutSecondFormat(datetime) + assert.Equal(t, expectedValue, actualValue) + + datetime = "2024-09-01 12:34:56" + expectedValue = false + actualValue = IsValidLongDateTimeWithoutSecondFormat(datetime) + assert.Equal(t, expectedValue, actualValue) +} + +func TestIsValidLongDateFormat_ValidLongDateFormat(t *testing.T) { + datetime := "2024-09-01" + expectedValue := true + actualValue := IsValidLongDateFormat(datetime) + assert.Equal(t, expectedValue, actualValue) + + datetime = "9999-12-31" + expectedValue = true + actualValue = IsValidLongDateFormat(datetime) + assert.Equal(t, expectedValue, actualValue) +} + +func TestIsValidLongDateFormat_InvalidLongDateFormat(t *testing.T) { + datetime := "24-09-01" + expectedValue := false + actualValue := IsValidLongDateFormat(datetime) + assert.Equal(t, expectedValue, actualValue) + + datetime = "2024-9-1" + expectedValue = false + actualValue = IsValidLongDateFormat(datetime) + assert.Equal(t, expectedValue, actualValue) + + datetime = "2024-09-1" + expectedValue = false + actualValue = IsValidLongDateFormat(datetime) + assert.Equal(t, expectedValue, actualValue) + + datetime = "2024-9-01" + expectedValue = false + actualValue = IsValidLongDateFormat(datetime) + assert.Equal(t, expectedValue, actualValue) + + datetime = "2024-09-01 12" + expectedValue = false + actualValue = IsValidLongDateFormat(datetime) + assert.Equal(t, expectedValue, actualValue) + + datetime = "2024-09-01 12:34" + expectedValue = false + actualValue = IsValidLongDateFormat(datetime) + assert.Equal(t, expectedValue, actualValue) + + datetime = "2024-09-01 12:34:56" + expectedValue = false + actualValue = IsValidLongDateFormat(datetime) + assert.Equal(t, expectedValue, actualValue) +} diff --git a/src/consts/file.js b/src/consts/file.js index 254d0565..ef1511d3 100644 --- a/src/consts/file.js +++ b/src/consts/file.js @@ -10,6 +10,11 @@ const supportedImportFileTypes = [ type: 'ezbookkeeping_tsv', name: 'ezbookkeeping Data Export File (TSV)', extensions: '.tsv' + }, + { + type: 'feidee_mymoney_xls', + name: 'Feidee MyMoney (Web) Data Export File', + extensions: '.xls' } ]; diff --git a/src/locales/en.json b/src/locales/en.json index fa305840..65fce611 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1117,6 +1117,7 @@ "destination account name cannot be blank": "Destination account name cannot be blank", "transaction amount is invalid": "Transaction amount is invalid", "geographic location is invalid": "Geographic location is invalid", + "fields in multiple table headers are different": "Fields in multiple table headers are different", "query items cannot be blank": "There are no query items", "query items too much": "There are too many query items", "query items have invalid item": "There is invalid item in query items", @@ -1495,6 +1496,7 @@ "File Type": "File Type", "ezbookkeeping Data Export File (CSV)": "ezbookkeeping Data Export File (CSV)", "ezbookkeeping Data Export File (TSV)": "ezbookkeeping Data Export File (TSV)", + "Feidee MyMoney (Web) Data Export File": "Feidee MyMoney (Web) Data Export File", "Data File": "Data File", "Click to select import file": "Click to select import file", "No data to import": "No data to import", diff --git a/src/locales/zh_Hans.json b/src/locales/zh_Hans.json index e76669cb..983adb66 100644 --- a/src/locales/zh_Hans.json +++ b/src/locales/zh_Hans.json @@ -1117,6 +1117,7 @@ "destination account name cannot be blank": "目标账户名不能为空", "transaction amount is invalid": "交易金额无效", "geographic location is invalid": "地理位置无效", + "fields in multiple table headers are different": "多个表头中的字段不同", "query items cannot be blank": "请求项目不能为空", "query items too much": "请求项目过多", "query items have invalid item": "请求项目中有非法项目", @@ -1495,6 +1496,7 @@ "File Type": "文件类型", "ezbookkeeping Data Export File (CSV)": "ezbookkeeping 数据导出文件 (CSV)", "ezbookkeeping Data Export File (TSV)": "ezbookkeeping 数据导出文件 (TSV)", + "Feidee MyMoney (Web) Data Export File": "金蝶随手记 (Web版) 数据导出文件", "Data File": "数据文件", "Click to select import file": "点击选择导入文件", "No data to import": "没有可以导入的数据", diff --git a/testdata/feidee_mymoney_test_file.xls b/testdata/feidee_mymoney_test_file.xls new file mode 100644 index 0000000000000000000000000000000000000000..b9b847564b4b5a94edaf6847d3c7c24c9e48403c GIT binary patch literal 33280 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 literal 0 HcmV?d00001 diff --git a/third-party-dependencies.json b/third-party-dependencies.json index c54a3c3b..f4d05616 100644 --- a/third-party-dependencies.json +++ b/third-party-dependencies.json @@ -111,6 +111,11 @@ "url": "https://github.com/boombuler/barcode", "licenseUrl": "https://github.com/boombuler/barcode/blob/v1.0.2/LICENSE" }, + { + "name": "xlsReader", + "url": "https://github.com/shakinm/xlsReader", + "licenseUrl": "https://github.com/shakinm/xlsReader/blob/v0.9.12/LICENSE" + }, { "name": "go-ordered-map", "url": "https://github.com/wk8/go-ordered-map",