support importing feidee mymoney web export data
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
+21
-3
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "没有可以导入的数据",
|
||||
|
||||
BIN
Binary file not shown.
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user