code refactor

This commit is contained in:
MaysWind
2024-10-13 19:44:29 +08:00
parent 5ac9eb5d5c
commit d9b819d1a1
26 changed files with 1331 additions and 1226 deletions
-56
View File
@@ -1,56 +0,0 @@
package datatable
import (
"time"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/models"
)
// ImportedDataTable defines the structure of imported data table
type ImportedDataTable interface {
// DataRowCount returns the total count of data row
DataRowCount() int
// HeaderLineColumnNames returns the header column name list
HeaderLineColumnNames() []string
// DataRowIterator returns the iterator of data row
DataRowIterator() ImportedDataRowIterator
}
// ImportedDataRow defines the structure of imported data row
type ImportedDataRow interface {
// IsValid returns whether this row contains valid data for importing
IsValid() bool
// ColumnCount returns the total count of column in this data row
ColumnCount() int
// GetData returns the data in the specified column index
GetData(columnIndex int) string
// GetTime returns the time in the specified column index
GetTime(columnIndex int, timezoneOffset int16) (time.Time, error)
// GetTimezoneOffset returns the time zone offset in the specified column index
GetTimezoneOffset(columnIndex int) (*time.Location, error)
}
// ImportedDataRowIterator defines the structure of imported data row iterator
type ImportedDataRowIterator interface {
// HasNext returns whether the iterator does not reach the end
HasNext() bool
// Next returns the next imported data row
Next(ctx core.Context, user *models.User) ImportedDataRow
}
// DataTableBuilder defines the structure of data table builder
type DataTableBuilder interface {
// AppendTransaction appends the specified transaction to data builder
AppendTransaction(data map[DataTableColumn]string)
// ReplaceDelimiters returns the text after removing the delimiters
ReplaceDelimiters(text string) string
}
@@ -14,30 +14,8 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/validators"
)
// DataTableColumn represents the data column type of data table
type DataTableColumn byte
// Data table columns
const (
DATA_TABLE_TRANSACTION_TIME DataTableColumn = 1
DATA_TABLE_TRANSACTION_TIMEZONE DataTableColumn = 2
DATA_TABLE_TRANSACTION_TYPE DataTableColumn = 3
DATA_TABLE_CATEGORY DataTableColumn = 4
DATA_TABLE_SUB_CATEGORY DataTableColumn = 5
DATA_TABLE_ACCOUNT_NAME DataTableColumn = 6
DATA_TABLE_ACCOUNT_CURRENCY DataTableColumn = 7
DATA_TABLE_AMOUNT DataTableColumn = 8
DATA_TABLE_RELATED_ACCOUNT_NAME DataTableColumn = 9
DATA_TABLE_RELATED_ACCOUNT_CURRENCY DataTableColumn = 10
DATA_TABLE_RELATED_AMOUNT DataTableColumn = 11
DATA_TABLE_GEOGRAPHIC_LOCATION DataTableColumn = 12
DATA_TABLE_TAGS DataTableColumn = 13
DATA_TABLE_DESCRIPTION DataTableColumn = 14
)
// DataTableTransactionDataExporter defines the structure of plain text data table exporter for transaction data
type DataTableTransactionDataExporter struct {
dataColumnMapping map[DataTableColumn]string
transactionTypeMapping map[models.TransactionType]string
geoLocationSeparator string
transactionTagSeparator string
@@ -45,20 +23,14 @@ type DataTableTransactionDataExporter struct {
// DataTableTransactionDataImporter defines the structure of plain text data table importer for transaction data
type DataTableTransactionDataImporter struct {
dataColumnMapping map[DataTableColumn]string
transactionTypeMapping map[models.TransactionType]string
geoLocationSeparator string
transactionTagSeparator string
postProcessFunc DataTableTransactionDataImporterPostProcessFunc
}
// DataTableTransactionDataImporterPostProcessFunc represents item post process function of DataTableTransactionDataImporter
type DataTableTransactionDataImporterPostProcessFunc func(core.Context, *models.ImportTransaction) error
// CreateNewExporter returns a new data table transaction data exporter according to the specified arguments
func CreateNewExporter(dataColumnMapping map[DataTableColumn]string, transactionTypeMapping map[models.TransactionType]string, geoLocationSeparator string, transactionTagSeparator string) *DataTableTransactionDataExporter {
func CreateNewExporter(transactionTypeMapping map[models.TransactionType]string, geoLocationSeparator string, transactionTagSeparator string) *DataTableTransactionDataExporter {
return &DataTableTransactionDataExporter{
dataColumnMapping: dataColumnMapping,
transactionTypeMapping: transactionTypeMapping,
geoLocationSeparator: geoLocationSeparator,
transactionTagSeparator: transactionTagSeparator,
@@ -66,9 +38,8 @@ func CreateNewExporter(dataColumnMapping map[DataTableColumn]string, transaction
}
// CreateNewImporter returns a new data table transaction data importer according to the specified arguments
func CreateNewImporter(dataColumnMapping map[DataTableColumn]string, transactionTypeMapping map[models.TransactionType]string, geoLocationSeparator string, transactionTagSeparator string) *DataTableTransactionDataImporter {
func CreateNewImporter(transactionTypeMapping map[models.TransactionType]string, geoLocationSeparator string, transactionTagSeparator string) *DataTableTransactionDataImporter {
return &DataTableTransactionDataImporter{
dataColumnMapping: dataColumnMapping,
transactionTypeMapping: transactionTypeMapping,
geoLocationSeparator: geoLocationSeparator,
transactionTagSeparator: transactionTagSeparator,
@@ -76,41 +47,14 @@ func CreateNewImporter(dataColumnMapping map[DataTableColumn]string, transaction
}
// CreateNewSimpleImporter returns a new data table transaction data importer according to the specified arguments
func CreateNewSimpleImporter(dataColumnMapping map[DataTableColumn]string, transactionTypeMapping map[models.TransactionType]string) *DataTableTransactionDataImporter {
func CreateNewSimpleImporter(transactionTypeMapping map[models.TransactionType]string) *DataTableTransactionDataImporter {
return &DataTableTransactionDataImporter{
dataColumnMapping: dataColumnMapping,
transactionTypeMapping: transactionTypeMapping,
}
}
// CreateNewSimpleImporterWithPostProcessFunc returns a new data table transaction data importer according to the specified arguments
func CreateNewSimpleImporterWithPostProcessFunc(dataColumnMapping map[DataTableColumn]string, transactionTypeMapping map[models.TransactionType]string, postProcessFunc DataTableTransactionDataImporterPostProcessFunc) *DataTableTransactionDataImporter {
return &DataTableTransactionDataImporter{
dataColumnMapping: dataColumnMapping,
transactionTypeMapping: transactionTypeMapping,
postProcessFunc: postProcessFunc,
}
}
// CreateNewSimpleImporterFromWritableDataTable returns a new data table transaction data importer according to the specified arguments
func CreateNewSimpleImporterFromWritableDataTable(writableDataTable *WritableDataTable, transactionTypeMapping map[models.TransactionType]string) *DataTableTransactionDataImporter {
return &DataTableTransactionDataImporter{
dataColumnMapping: writableDataTable.GetDataColumnMapping(),
transactionTypeMapping: transactionTypeMapping,
}
}
// CreateNewSimpleImporterFromWritableDataTableWithPostProcessFunc returns a new data table transaction data importer according to the specified arguments
func CreateNewSimpleImporterFromWritableDataTableWithPostProcessFunc(writableDataTable *WritableDataTable, transactionTypeMapping map[models.TransactionType]string, postProcessFunc DataTableTransactionDataImporterPostProcessFunc) *DataTableTransactionDataImporter {
return &DataTableTransactionDataImporter{
dataColumnMapping: writableDataTable.GetDataColumnMapping(),
transactionTypeMapping: transactionTypeMapping,
postProcessFunc: postProcessFunc,
}
}
// BuildExportedContent writes the exported transaction data to the data table builder
func (c *DataTableTransactionDataExporter) BuildExportedContent(ctx core.Context, dataTableBuilder DataTableBuilder, uid int64, transactions []*models.Transaction, accountMap map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, allTagIndexes map[int64][]int64) error {
func (c *DataTableTransactionDataExporter) BuildExportedContent(ctx core.Context, dataTableBuilder TransactionDataTableBuilder, uid int64, transactions []*models.Transaction, accountMap map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, allTagIndexes map[int64][]int64) error {
for i := 0; i < len(transactions); i++ {
transaction := transactions[i]
@@ -118,27 +62,27 @@ func (c *DataTableTransactionDataExporter) BuildExportedContent(ctx core.Context
continue
}
dataRowMap := make(map[DataTableColumn]string, 15)
dataRowMap := make(map[TransactionDataTableColumn]string, 15)
transactionTimeZone := time.FixedZone("Transaction Timezone", int(transaction.TimezoneUtcOffset)*60)
dataRowMap[DATA_TABLE_TRANSACTION_TIME] = utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime), transactionTimeZone)
dataRowMap[DATA_TABLE_TRANSACTION_TIMEZONE] = utils.FormatTimezoneOffset(transactionTimeZone)
dataRowMap[DATA_TABLE_TRANSACTION_TYPE] = dataTableBuilder.ReplaceDelimiters(c.getDisplayTransactionTypeName(transaction.Type))
dataRowMap[DATA_TABLE_CATEGORY] = c.getExportedTransactionCategoryName(dataTableBuilder, transaction.CategoryId, categoryMap)
dataRowMap[DATA_TABLE_SUB_CATEGORY] = c.getExportedTransactionSubCategoryName(dataTableBuilder, transaction.CategoryId, categoryMap)
dataRowMap[DATA_TABLE_ACCOUNT_NAME] = c.getExportedAccountName(dataTableBuilder, transaction.AccountId, accountMap)
dataRowMap[DATA_TABLE_ACCOUNT_CURRENCY] = c.getAccountCurrency(dataTableBuilder, transaction.AccountId, accountMap)
dataRowMap[DATA_TABLE_AMOUNT] = utils.FormatAmount(transaction.Amount)
dataRowMap[TRANSACTION_DATA_TABLE_TRANSACTION_TIME] = utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime), transactionTimeZone)
dataRowMap[TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE] = utils.FormatTimezoneOffset(transactionTimeZone)
dataRowMap[TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = dataTableBuilder.ReplaceDelimiters(c.getDisplayTransactionTypeName(transaction.Type))
dataRowMap[TRANSACTION_DATA_TABLE_CATEGORY] = c.getExportedTransactionCategoryName(dataTableBuilder, transaction.CategoryId, categoryMap)
dataRowMap[TRANSACTION_DATA_TABLE_SUB_CATEGORY] = c.getExportedTransactionSubCategoryName(dataTableBuilder, transaction.CategoryId, categoryMap)
dataRowMap[TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = c.getExportedAccountName(dataTableBuilder, transaction.AccountId, accountMap)
dataRowMap[TRANSACTION_DATA_TABLE_ACCOUNT_CURRENCY] = c.getAccountCurrency(dataTableBuilder, transaction.AccountId, accountMap)
dataRowMap[TRANSACTION_DATA_TABLE_AMOUNT] = utils.FormatAmount(transaction.Amount)
if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT {
dataRowMap[DATA_TABLE_RELATED_ACCOUNT_NAME] = c.getExportedAccountName(dataTableBuilder, transaction.RelatedAccountId, accountMap)
dataRowMap[DATA_TABLE_RELATED_ACCOUNT_CURRENCY] = c.getAccountCurrency(dataTableBuilder, transaction.RelatedAccountId, accountMap)
dataRowMap[DATA_TABLE_RELATED_AMOUNT] = utils.FormatAmount(transaction.RelatedAccountAmount)
dataRowMap[TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = c.getExportedAccountName(dataTableBuilder, transaction.RelatedAccountId, accountMap)
dataRowMap[TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_CURRENCY] = c.getAccountCurrency(dataTableBuilder, transaction.RelatedAccountId, accountMap)
dataRowMap[TRANSACTION_DATA_TABLE_RELATED_AMOUNT] = utils.FormatAmount(transaction.RelatedAccountAmount)
}
dataRowMap[DATA_TABLE_GEOGRAPHIC_LOCATION] = c.getExportedGeographicLocation(transaction)
dataRowMap[DATA_TABLE_TAGS] = c.getExportedTags(dataTableBuilder, transaction.TransactionId, allTagIndexes, tagMap)
dataRowMap[DATA_TABLE_DESCRIPTION] = dataTableBuilder.ReplaceDelimiters(transaction.Comment)
dataRowMap[TRANSACTION_DATA_TABLE_GEOGRAPHIC_LOCATION] = c.getExportedGeographicLocation(transaction)
dataRowMap[TRANSACTION_DATA_TABLE_TAGS] = c.getExportedTags(dataTableBuilder, transaction.TransactionId, allTagIndexes, tagMap)
dataRowMap[TRANSACTION_DATA_TABLE_DESCRIPTION] = dataTableBuilder.ReplaceDelimiters(transaction.Comment)
dataTableBuilder.AppendTransaction(dataRowMap)
}
@@ -162,7 +106,7 @@ func (c *DataTableTransactionDataExporter) getDisplayTransactionTypeName(transac
return transactionTypeName
}
func (c *DataTableTransactionDataExporter) getExportedTransactionCategoryName(dataTableBuilder DataTableBuilder, categoryId int64, categoryMap map[int64]*models.TransactionCategory) string {
func (c *DataTableTransactionDataExporter) getExportedTransactionCategoryName(dataTableBuilder TransactionDataTableBuilder, categoryId int64, categoryMap map[int64]*models.TransactionCategory) string {
category, exists := categoryMap[categoryId]
if !exists {
@@ -182,7 +126,7 @@ func (c *DataTableTransactionDataExporter) getExportedTransactionCategoryName(da
return dataTableBuilder.ReplaceDelimiters(parentCategory.Name)
}
func (c *DataTableTransactionDataExporter) getExportedTransactionSubCategoryName(dataTableBuilder DataTableBuilder, categoryId int64, categoryMap map[int64]*models.TransactionCategory) string {
func (c *DataTableTransactionDataExporter) getExportedTransactionSubCategoryName(dataTableBuilder TransactionDataTableBuilder, categoryId int64, categoryMap map[int64]*models.TransactionCategory) string {
category, exists := categoryMap[categoryId]
if exists {
@@ -192,7 +136,7 @@ func (c *DataTableTransactionDataExporter) getExportedTransactionSubCategoryName
}
}
func (c *DataTableTransactionDataExporter) getExportedAccountName(dataTableBuilder DataTableBuilder, accountId int64, accountMap map[int64]*models.Account) string {
func (c *DataTableTransactionDataExporter) getExportedAccountName(dataTableBuilder TransactionDataTableBuilder, accountId int64, accountMap map[int64]*models.Account) string {
account, exists := accountMap[accountId]
if exists {
@@ -202,7 +146,7 @@ func (c *DataTableTransactionDataExporter) getExportedAccountName(dataTableBuild
}
}
func (c *DataTableTransactionDataExporter) getAccountCurrency(dataTableBuilder DataTableBuilder, accountId int64, accountMap map[int64]*models.Account) string {
func (c *DataTableTransactionDataExporter) getAccountCurrency(dataTableBuilder TransactionDataTableBuilder, accountId int64, accountMap map[int64]*models.Account) string {
account, exists := accountMap[accountId]
if exists {
@@ -220,7 +164,7 @@ func (c *DataTableTransactionDataExporter) getExportedGeographicLocation(transac
return ""
}
func (c *DataTableTransactionDataExporter) getExportedTags(dataTableBuilder DataTableBuilder, transactionId int64, allTagIndexes map[int64][]int64, tagMap map[int64]*models.TransactionTag) string {
func (c *DataTableTransactionDataExporter) getExportedTags(dataTableBuilder TransactionDataTableBuilder, transactionId int64, allTagIndexes map[int64][]int64, tagMap map[int64]*models.TransactionTag) string {
tagIndexes, exists := allTagIndexes[transactionId]
if !exists {
@@ -248,8 +192,8 @@ func (c *DataTableTransactionDataExporter) getExportedTags(dataTableBuilder Data
}
// ParseImportedData returns the imported transaction data
func (c *DataTableTransactionDataImporter) ParseImportedData(ctx core.Context, user *models.User, dataTable ImportedDataTable, defaultTimezoneOffset int16, accountMap map[string]*models.Account, expenseCategoryMap map[string]*models.TransactionCategory, incomeCategoryMap map[string]*models.TransactionCategory, transferCategoryMap map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) {
if dataTable.DataRowCount() < 1 {
func (c *DataTableTransactionDataImporter) ParseImportedData(ctx core.Context, user *models.User, dataTable TransactionDataTable, defaultTimezoneOffset int16, accountMap map[string]*models.Account, expenseCategoryMap map[string]*models.TransactionCategory, incomeCategoryMap map[string]*models.TransactionCategory, transferCategoryMap map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) {
if dataTable.TransactionRowCount() < 1 {
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] cannot parse import data for user \"uid:%d\", because data table row count is less 1", user.Uid)
return nil, nil, nil, nil, nil, nil, errs.ErrNotFoundTransactionDataInFile
}
@@ -260,29 +204,12 @@ func (c *DataTableTransactionDataImporter) ParseImportedData(ctx core.Context, u
return nil, nil, nil, nil, nil, nil, err
}
headerLineItems := dataTable.HeaderLineColumnNames()
headerItemMap := make(map[string]int)
for i := 0; i < len(headerLineItems); i++ {
headerItemMap[headerLineItems[i]] = i
}
timeColumnIdx, timeColumnExists := headerItemMap[c.dataColumnMapping[DATA_TABLE_TRANSACTION_TIME]]
timezoneColumnIdx, timezoneColumnExists := headerItemMap[c.dataColumnMapping[DATA_TABLE_TRANSACTION_TIMEZONE]]
typeColumnIdx, typeColumnExists := headerItemMap[c.dataColumnMapping[DATA_TABLE_TRANSACTION_TYPE]]
subCategoryColumnIdx, subCategoryColumnExists := headerItemMap[c.dataColumnMapping[DATA_TABLE_SUB_CATEGORY]]
accountColumnIdx, accountColumnExists := headerItemMap[c.dataColumnMapping[DATA_TABLE_ACCOUNT_NAME]]
accountCurrencyColumnIdx, accountCurrencyColumnExists := headerItemMap[c.dataColumnMapping[DATA_TABLE_ACCOUNT_CURRENCY]]
amountColumnIdx, amountColumnExists := headerItemMap[c.dataColumnMapping[DATA_TABLE_AMOUNT]]
account2ColumnIdx, account2ColumnExists := headerItemMap[c.dataColumnMapping[DATA_TABLE_RELATED_ACCOUNT_NAME]]
account2CurrencyColumnIdx, account2CurrencyColumnExists := headerItemMap[c.dataColumnMapping[DATA_TABLE_RELATED_ACCOUNT_CURRENCY]]
amount2ColumnIdx, amount2ColumnExists := headerItemMap[c.dataColumnMapping[DATA_TABLE_RELATED_AMOUNT]]
geoLocationIdx, geoLocationExists := headerItemMap[c.dataColumnMapping[DATA_TABLE_GEOGRAPHIC_LOCATION]]
tagsColumnIdx, tagsColumnExists := headerItemMap[c.dataColumnMapping[DATA_TABLE_TAGS]]
descriptionColumnIdx, descriptionColumnExists := headerItemMap[c.dataColumnMapping[DATA_TABLE_DESCRIPTION]]
if !timeColumnExists || !typeColumnExists || !subCategoryColumnExists ||
!accountColumnExists || !amountColumnExists || !account2ColumnExists {
if !dataTable.HasColumn(TRANSACTION_DATA_TABLE_TRANSACTION_TIME) ||
!dataTable.HasColumn(TRANSACTION_DATA_TABLE_TRANSACTION_TYPE) ||
!dataTable.HasColumn(TRANSACTION_DATA_TABLE_SUB_CATEGORY) ||
!dataTable.HasColumn(TRANSACTION_DATA_TABLE_ACCOUNT_NAME) ||
!dataTable.HasColumn(TRANSACTION_DATA_TABLE_AMOUNT) ||
!dataTable.HasColumn(TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME) {
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] cannot parse import data for user \"uid:%d\", because missing essential columns in header row", user.Uid)
return nil, nil, nil, nil, nil, nil, errs.ErrMissingRequiredFieldInHeaderRow
}
@@ -307,59 +234,53 @@ func (c *DataTableTransactionDataImporter) ParseImportedData(ctx core.Context, u
tagMap = make(map[string]*models.TransactionTag)
}
allNewTransactions := make(models.ImportedTransactionSlice, 0, dataTable.DataRowCount())
allNewTransactions := make(models.ImportedTransactionSlice, 0, dataTable.TransactionRowCount())
allNewAccounts := make([]*models.Account, 0)
allNewSubExpenseCategories := make([]*models.TransactionCategory, 0)
allNewSubIncomeCategories := make([]*models.TransactionCategory, 0)
allNewSubTransferCategories := make([]*models.TransactionCategory, 0)
allNewTags := make([]*models.TransactionTag, 0)
dataRowIterator := dataTable.DataRowIterator()
dataRowIterator := dataTable.TransactionRowIterator()
dataRowIndex := 0
for dataRowIterator.HasNext() {
dataRowIndex++
dataRow := dataRowIterator.Next(ctx, user)
dataRow, err := dataRowIterator.Next(ctx, user)
if err != nil {
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] cannot parse data row \"index:%d\" for user \"uid:%d\", because %s", dataRowIndex, user.Uid, err.Error())
return nil, nil, nil, nil, nil, nil, err
}
if !dataRow.IsValid() {
continue
}
columnCount := dataRow.ColumnCount()
if columnCount < 1 || (columnCount == 1 && dataRow.GetData(0) == "") {
continue
}
if columnCount < len(headerLineItems) {
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] cannot parse data row \"index:%d\" for user \"uid:%d\", because may missing some columns (column count %d in data row is less than header column count %d)", dataRowIndex, user.Uid, columnCount, len(headerLineItems))
return nil, nil, nil, nil, nil, nil, errs.ErrFewerFieldsInDataRowThanInHeaderRow
}
timezoneOffset := defaultTimezoneOffset
if timezoneColumnExists {
transactionTimezone, err := dataRow.GetTimezoneOffset(timezoneColumnIdx)
if dataTable.HasColumn(TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE) {
transactionTimezone, err := utils.ParseFromTimezoneOffset(dataRow.GetData(TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE))
if err != nil {
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] cannot parse time zone \"%s\" in data row \"index:%d\" for user \"uid:%d\", because %s", dataRow.GetData(timezoneColumnIdx), dataRowIndex, user.Uid, err.Error())
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] cannot parse time zone \"%s\" in data row \"index:%d\" for user \"uid:%d\", because %s", dataRow.GetData(TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE), dataRowIndex, user.Uid, err.Error())
return nil, nil, nil, nil, nil, nil, errs.ErrTransactionTimeZoneInvalid
}
timezoneOffset = utils.GetTimezoneOffsetMinutes(transactionTimezone)
}
transactionTime, err := dataRow.GetTime(timeColumnIdx, timezoneOffset)
transactionTime, err := utils.ParseFromLongDateTime(dataRow.GetData(TRANSACTION_DATA_TABLE_TRANSACTION_TIME), timezoneOffset)
if err != nil {
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] cannot parse time \"%s\" in data row \"index:%d\" for user \"uid:%d\", because %s", dataRow.GetData(timeColumnIdx), dataRowIndex, user.Uid, err.Error())
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] cannot parse time \"%s\" in data row \"index:%d\" for user \"uid:%d\", because %s", dataRow.GetData(TRANSACTION_DATA_TABLE_TRANSACTION_TIME), dataRowIndex, user.Uid, err.Error())
return nil, nil, nil, nil, nil, nil, errs.ErrTransactionTimeInvalid
}
transactionDbType, err := c.getTransactionDbType(nameDbTypeMap, dataRow.GetData(typeColumnIdx))
transactionDbType, err := c.getTransactionDbType(nameDbTypeMap, dataRow.GetData(TRANSACTION_DATA_TABLE_TRANSACTION_TYPE))
if err != nil {
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] cannot parse transaction type \"%s\" in data row \"index:%d\" for user \"uid:%d\", because %s", dataRow.GetData(typeColumnIdx), dataRowIndex, user.Uid, err.Error())
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] cannot parse transaction type \"%s\" in data row \"index:%d\" for user \"uid:%d\", because %s", dataRow.GetData(TRANSACTION_DATA_TABLE_TRANSACTION_TYPE), dataRowIndex, user.Uid, err.Error())
return nil, nil, nil, nil, nil, nil, errs.Or(err, errs.ErrTransactionTypeInvalid)
}
@@ -374,7 +295,7 @@ func (c *DataTableTransactionDataImporter) ParseImportedData(ctx core.Context, u
return nil, nil, nil, nil, nil, nil, errs.Or(err, errs.ErrTransactionTypeInvalid)
}
subCategoryName = dataRow.GetData(subCategoryColumnIdx)
subCategoryName = dataRow.GetData(TRANSACTION_DATA_TABLE_SUB_CATEGORY)
if transactionDbType == models.TRANSACTION_DB_TYPE_EXPENSE {
subCategory, exists := expenseCategoryMap[subCategoryName]
@@ -409,11 +330,11 @@ func (c *DataTableTransactionDataImporter) ParseImportedData(ctx core.Context, u
}
}
accountName := dataRow.GetData(accountColumnIdx)
accountName := dataRow.GetData(TRANSACTION_DATA_TABLE_ACCOUNT_NAME)
accountCurrency := user.DefaultCurrency
if accountCurrencyColumnExists {
accountCurrency = dataRow.GetData(accountCurrencyColumnIdx)
if dataTable.HasColumn(TRANSACTION_DATA_TABLE_ACCOUNT_CURRENCY) {
accountCurrency = dataRow.GetData(TRANSACTION_DATA_TABLE_ACCOUNT_CURRENCY)
if _, ok := validators.AllCurrencyNames[accountCurrency]; !ok {
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] account currency \"%s\" is not supported in data row \"index:%d\" for user \"uid:%d\"", accountCurrency, dataRowIndex, user.Uid)
@@ -429,7 +350,7 @@ func (c *DataTableTransactionDataImporter) ParseImportedData(ctx core.Context, u
accountMap[accountName] = account
}
if accountCurrencyColumnExists {
if dataTable.HasColumn(TRANSACTION_DATA_TABLE_ACCOUNT_CURRENCY) {
if account.Name != "" && account.Currency != accountCurrency {
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] currency \"%s\" in data row \"index:%d\" not equals currency \"%s\" of the account for user \"uid:%d\"", accountCurrency, dataRowIndex, account.Currency, user.Uid)
return nil, nil, nil, nil, nil, nil, errs.ErrAccountCurrencyInvalid
@@ -438,10 +359,10 @@ func (c *DataTableTransactionDataImporter) ParseImportedData(ctx core.Context, u
accountCurrency = account.Currency
}
amount, err := utils.ParseAmount(dataRow.GetData(amountColumnIdx))
amount, err := utils.ParseAmount(dataRow.GetData(TRANSACTION_DATA_TABLE_AMOUNT))
if err != nil {
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] cannot parse acmount \"%s\" in data row \"index:%d\" for user \"uid:%d\", because %s", dataRow.GetData(amountColumnIdx), dataRowIndex, user.Uid, err.Error())
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] cannot parse acmount \"%s\" in data row \"index:%d\" for user \"uid:%d\", because %s", dataRow.GetData(TRANSACTION_DATA_TABLE_AMOUNT), dataRowIndex, user.Uid, err.Error())
return nil, nil, nil, nil, nil, nil, errs.ErrAmountInvalid
}
@@ -451,11 +372,11 @@ func (c *DataTableTransactionDataImporter) ParseImportedData(ctx core.Context, u
account2Currency := ""
if transactionDbType == models.TRANSACTION_DB_TYPE_TRANSFER_OUT {
account2Name = dataRow.GetData(account2ColumnIdx)
account2Name = dataRow.GetData(TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME)
account2Currency = user.DefaultCurrency
if account2CurrencyColumnExists {
account2Currency = dataRow.GetData(account2CurrencyColumnIdx)
if dataTable.HasColumn(TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_CURRENCY) {
account2Currency = dataRow.GetData(TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_CURRENCY)
if _, ok := validators.AllCurrencyNames[account2Currency]; !ok {
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] account2 currency \"%s\" is not supported in data row \"index:%d\" for user \"uid:%d\"", account2Currency, dataRowIndex, user.Uid)
@@ -471,7 +392,7 @@ func (c *DataTableTransactionDataImporter) ParseImportedData(ctx core.Context, u
accountMap[account2Name] = account2
}
if account2CurrencyColumnExists {
if dataTable.HasColumn(TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_CURRENCY) {
if account2.Name != "" && account2.Currency != account2Currency {
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] currency \"%s\" in data row \"index:%d\" not equals currency \"%s\" of the account2 for user \"uid:%d\"", account2Currency, dataRowIndex, account2.Currency, user.Uid)
return nil, nil, nil, nil, nil, nil, errs.ErrAccountCurrencyInvalid
@@ -482,11 +403,11 @@ func (c *DataTableTransactionDataImporter) ParseImportedData(ctx core.Context, u
relatedAccountId = account2.AccountId
if amount2ColumnExists {
relatedAccountAmount, err = utils.ParseAmount(dataRow.GetData(amount2ColumnIdx))
if dataTable.HasColumn(TRANSACTION_DATA_TABLE_RELATED_AMOUNT) {
relatedAccountAmount, err = utils.ParseAmount(dataRow.GetData(TRANSACTION_DATA_TABLE_RELATED_AMOUNT))
if err != nil {
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] cannot parse acmount2 \"%s\" in data row \"index:%d\" for user \"uid:%d\", because %s", dataRow.GetData(amount2ColumnIdx), dataRowIndex, user.Uid, err.Error())
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] cannot parse acmount2 \"%s\" in data row \"index:%d\" for user \"uid:%d\", because %s", dataRow.GetData(TRANSACTION_DATA_TABLE_RELATED_AMOUNT), dataRowIndex, user.Uid, err.Error())
return nil, nil, nil, nil, nil, nil, errs.ErrAmountInvalid
}
} else if transactionDbType == models.TRANSACTION_DB_TYPE_TRANSFER_OUT {
@@ -497,21 +418,21 @@ func (c *DataTableTransactionDataImporter) ParseImportedData(ctx core.Context, u
geoLongitude := float64(0)
geoLatitude := float64(0)
if geoLocationExists {
geoLocationItems := strings.Split(dataRow.GetData(geoLocationIdx), c.geoLocationSeparator)
if dataTable.HasColumn(TRANSACTION_DATA_TABLE_GEOGRAPHIC_LOCATION) {
geoLocationItems := strings.Split(dataRow.GetData(TRANSACTION_DATA_TABLE_GEOGRAPHIC_LOCATION), c.geoLocationSeparator)
if len(geoLocationItems) == 2 {
geoLongitude, err = utils.StringToFloat64(geoLocationItems[0])
if err != nil {
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] cannot parse geographic location \"%s\" in data row \"index:%d\" for user \"uid:%d\", because %s", dataRow.GetData(geoLocationIdx), dataRowIndex, user.Uid, err.Error())
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] cannot parse geographic location \"%s\" in data row \"index:%d\" for user \"uid:%d\", because %s", dataRow.GetData(TRANSACTION_DATA_TABLE_GEOGRAPHIC_LOCATION), dataRowIndex, user.Uid, err.Error())
return nil, nil, nil, nil, nil, nil, errs.ErrGeographicLocationInvalid
}
geoLatitude, err = utils.StringToFloat64(geoLocationItems[1])
if err != nil {
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] cannot parse geographic location \"%s\" in data row \"index:%d\" for user \"uid:%d\", because %s", dataRow.GetData(geoLocationIdx), dataRowIndex, user.Uid, err.Error())
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] cannot parse geographic location \"%s\" in data row \"index:%d\" for user \"uid:%d\", because %s", dataRow.GetData(TRANSACTION_DATA_TABLE_GEOGRAPHIC_LOCATION), dataRowIndex, user.Uid, err.Error())
return nil, nil, nil, nil, nil, nil, errs.ErrGeographicLocationInvalid
}
}
@@ -520,8 +441,8 @@ func (c *DataTableTransactionDataImporter) ParseImportedData(ctx core.Context, u
var tagIds []string
var tagNames []string
if tagsColumnExists {
tagNameItems := strings.Split(dataRow.GetData(tagsColumnIdx), c.transactionTagSeparator)
if dataTable.HasColumn(TRANSACTION_DATA_TABLE_TAGS) {
tagNameItems := strings.Split(dataRow.GetData(TRANSACTION_DATA_TABLE_TAGS), c.transactionTagSeparator)
for i := 0; i < len(tagNameItems); i++ {
tagName := tagNameItems[i]
@@ -548,8 +469,8 @@ func (c *DataTableTransactionDataImporter) ParseImportedData(ctx core.Context, u
description := ""
if descriptionColumnExists {
description = dataRow.GetData(descriptionColumnIdx)
if dataTable.HasColumn(TRANSACTION_DATA_TABLE_DESCRIPTION) {
description = dataRow.GetData(TRANSACTION_DATA_TABLE_DESCRIPTION)
}
transaction := &models.ImportTransaction{
@@ -578,15 +499,6 @@ func (c *DataTableTransactionDataImporter) ParseImportedData(ctx core.Context, u
OriginalTagNames: tagNames,
}
if c.postProcessFunc != nil {
err = c.postProcessFunc(ctx, transaction)
if err != nil {
log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] cannot post process data row \"index:%d\" for user \"uid:%d\", because %s", dataRowIndex, user.Uid, err.Error())
return nil, nil, nil, nil, nil, nil, err
}
}
allNewTransactions = append(allNewTransactions, transaction)
}
@@ -0,0 +1,204 @@
package datatable
import (
"bytes"
"github.com/shakinm/xlsReader/xls"
"github.com/mayswind/ezbookkeeping/pkg/errs"
)
// DefaultExcelFileImportedDataTable defines the structure of default excel file data table
type DefaultExcelFileImportedDataTable struct {
workbook *xls.Workbook
headerLineColumnNames []string
}
// DefaultExcelFileDataRow defines the structure of default excel file data table row
type DefaultExcelFileDataRow struct {
sheet *xls.Sheet
rowIndex int
}
// DefaultExcelFileDataRowIterator defines the structure of default excel file data table row iterator
type DefaultExcelFileDataRowIterator struct {
dataTable *DefaultExcelFileImportedDataTable
currentTableIndex int
currentRowIndexInTable int
}
// DataRowCount returns the total count of data row
func (t *DefaultExcelFileImportedDataTable) 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
}
// HeaderColumnNames returns the header column name list
func (t *DefaultExcelFileImportedDataTable) HeaderColumnNames() []string {
return t.headerLineColumnNames
}
// DataRowIterator returns the iterator of data row
func (t *DefaultExcelFileImportedDataTable) DataRowIterator() ImportedDataRowIterator {
return &DefaultExcelFileDataRowIterator{
dataTable: t,
currentTableIndex: 0,
currentRowIndexInTable: 0,
}
}
// ColumnCount returns the total count of column in this data row
func (r *DefaultExcelFileDataRow) 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 *DefaultExcelFileDataRow) 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()
}
// HasNext returns whether the iterator does not reach the end
func (t *DefaultExcelFileDataRowIterator) 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 *DefaultExcelFileDataRowIterator) 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 &DefaultExcelFileDataRow{
sheet: &currentSheet,
rowIndex: t.currentRowIndexInTable,
}
}
// CreateNewDefaultExcelFileImportedDataTable returns default excel xls data table by file binary data
func CreateNewDefaultExcelFileImportedDataTable(data []byte) (*DefaultExcelFileImportedDataTable, 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 &DefaultExcelFileImportedDataTable{
workbook: &workbook,
headerLineColumnNames: headerRowItems,
}, nil
}
@@ -0,0 +1,124 @@
package datatable
import (
"encoding/csv"
"io"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/log"
)
// DefaultPlainTextImportedDataTable defines the structure of default plain text data table
type DefaultPlainTextImportedDataTable struct {
allLines [][]string
}
// DefaultPlainTextImportedDataRow defines the structure of default plain text data table row
type DefaultPlainTextImportedDataRow struct {
dataTable *DefaultPlainTextImportedDataTable
allItems []string
}
// DefaultPlainTextImportedDataRowIterator defines the structure of default plain text data table row iterator
type DefaultPlainTextImportedDataRowIterator struct {
dataTable *DefaultPlainTextImportedDataTable
currentIndex int
}
// DataRowCount returns the total count of data row
func (t *DefaultPlainTextImportedDataTable) DataRowCount() int {
if len(t.allLines) < 1 {
return 0
}
return len(t.allLines) - 1
}
// HeaderColumnNames returns the header column name list
func (t *DefaultPlainTextImportedDataTable) HeaderColumnNames() []string {
if len(t.allLines) < 1 {
return nil
}
return t.allLines[0]
}
// DataRowIterator returns the iterator of data row
func (t *DefaultPlainTextImportedDataTable) DataRowIterator() ImportedDataRowIterator {
return &DefaultPlainTextImportedDataRowIterator{
dataTable: t,
currentIndex: 0,
}
}
// ColumnCount returns the total count of column in this data row
func (r *DefaultPlainTextImportedDataRow) ColumnCount() int {
return len(r.allItems)
}
// GetData returns the data in the specified column index
func (r *DefaultPlainTextImportedDataRow) GetData(columnIndex int) string {
if columnIndex >= len(r.allItems) {
return ""
}
return r.allItems[columnIndex]
}
// HasNext returns whether the iterator does not reach the end
func (t *DefaultPlainTextImportedDataRowIterator) HasNext() bool {
return t.currentIndex+1 < len(t.dataTable.allLines)
}
// Next returns the next imported data row
func (t *DefaultPlainTextImportedDataRowIterator) Next() ImportedDataRow {
if t.currentIndex+1 >= len(t.dataTable.allLines) {
return nil
}
t.currentIndex++
rowItems := t.dataTable.allLines[t.currentIndex]
return &DefaultPlainTextImportedDataRow{
dataTable: t.dataTable,
allItems: rowItems,
}
}
// CreateNewDefaultCsvDataTable returns default csv data table by io readers
func CreateNewDefaultCsvDataTable(ctx core.Context, reader io.Reader) (*DefaultPlainTextImportedDataTable, error) {
return createNewDefaultPlainTextDataTable(ctx, reader, ',')
}
func createNewDefaultPlainTextDataTable(ctx core.Context, reader io.Reader, comma rune) (*DefaultPlainTextImportedDataTable, error) {
csvReader := csv.NewReader(reader)
csvReader.Comma = comma
csvReader.FieldsPerRecord = -1
allLines := make([][]string, 0)
for {
items, err := csvReader.Read()
if err == io.EOF {
break
}
if err != nil {
log.Errorf(ctx, "[Default_plain_text_imported_data_table.createNewDefaultPlainTextDataTable] cannot parse plain text data, because %s", err.Error())
return nil, errs.ErrInvalidCSVFile
}
if len(items) == 0 && items[0] == "" {
continue
}
allLines = append(allLines, items)
}
return &DefaultPlainTextImportedDataTable{
allLines: allLines,
}, nil
}
@@ -0,0 +1,31 @@
package datatable
// ImportedDataTable defines the structure of imported data table
type ImportedDataTable interface {
// DataRowCount returns the total count of data row
DataRowCount() int
// HeaderColumnNames returns the header column name list
HeaderColumnNames() []string
// DataRowIterator returns the iterator of data row
DataRowIterator() ImportedDataRowIterator
}
// ImportedDataRow defines the structure of imported data row
type ImportedDataRow interface {
// ColumnCount returns the total count of column in this data row
ColumnCount() int
// GetData returns the data in the specified column index
GetData(columnIndex int) string
}
// ImportedDataRowIterator defines the structure of imported data row iterator
type ImportedDataRowIterator interface {
// HasNext returns whether the iterator does not reach the end
HasNext() bool
// Next returns the next imported data row
Next() ImportedDataRow
}
@@ -0,0 +1,188 @@
package datatable
import (
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/models"
)
// ImportedTransactionDataTable defines the structure of imported transaction data table
type ImportedTransactionDataTable struct {
innerDataTable ImportedDataTable
dataColumnMapping map[TransactionDataTableColumn]string
dataColumnIndexes map[TransactionDataTableColumn]int
rowParser TransactionDataRowParser
addedColumns map[TransactionDataTableColumn]any
}
// ImportedTransactionDataRow defines the structure of imported transaction data row
type ImportedTransactionDataRow struct {
transactionDataTable *ImportedTransactionDataTable
rowData map[TransactionDataTableColumn]string
rowDataValid bool
}
// ImportedTransactionDataRowIterator defines the structure of imported transaction data row iterator
type ImportedTransactionDataRowIterator struct {
transactionDataTable *ImportedTransactionDataTable
innerIterator ImportedDataRowIterator
}
// HasColumn returns whether the data table has specified column
func (t *ImportedTransactionDataTable) HasColumn(column TransactionDataTableColumn) bool {
index, exists := t.dataColumnIndexes[column]
if exists && index >= 0 {
return exists
}
if t.addedColumns != nil {
_, exists = t.addedColumns[column]
if exists {
return exists
}
}
return false
}
// TransactionRowCount returns the total count of transaction data row
func (t *ImportedTransactionDataTable) TransactionRowCount() int {
return t.innerDataTable.DataRowCount()
}
// TransactionRowIterator returns the iterator of transaction data row
func (t *ImportedTransactionDataTable) TransactionRowIterator() TransactionDataRowIterator {
return &ImportedTransactionDataRowIterator{
transactionDataTable: t,
innerIterator: t.innerDataTable.DataRowIterator(),
}
}
// IsValid returns whether this row is valid data for importing
func (r *ImportedTransactionDataRow) IsValid() bool {
return r.rowDataValid
}
// GetData returns the data in the specified column type
func (r *ImportedTransactionDataRow) GetData(column TransactionDataTableColumn) string {
if !r.rowDataValid {
return ""
}
_, exists := r.transactionDataTable.dataColumnIndexes[column]
if exists {
return r.rowData[column]
}
if r.transactionDataTable.addedColumns != nil {
_, exists = r.transactionDataTable.addedColumns[column]
if exists {
return r.rowData[column]
}
}
return ""
}
// HasNext returns whether the iterator does not reach the end
func (t *ImportedTransactionDataRowIterator) HasNext() bool {
return t.innerIterator.HasNext()
}
// Next returns the next transaction data row
func (t *ImportedTransactionDataRowIterator) Next(ctx core.Context, user *models.User) (daraRow TransactionDataRow, err error) {
importedRow := t.innerIterator.Next()
if importedRow == nil {
return nil, nil
}
if importedRow.ColumnCount() == 1 && importedRow.GetData(0) == "" {
return &ImportedTransactionDataRow{
transactionDataTable: t.transactionDataTable,
rowData: nil,
rowDataValid: false,
}, nil
}
if importedRow.ColumnCount() < len(t.transactionDataTable.dataColumnIndexes) {
log.Errorf(ctx, "[imported_transaction_data_table.Next] cannot parse data row, because may missing some columns (column count %d in data row is less than header column count %d)", importedRow.ColumnCount(), len(t.transactionDataTable.dataColumnIndexes))
return nil, errs.ErrFewerFieldsInDataRowThanInHeaderRow
}
rowData := make(map[TransactionDataTableColumn]string, len(t.transactionDataTable.dataColumnIndexes))
rowDataValid := true
for column, columnIndex := range t.transactionDataTable.dataColumnIndexes {
if columnIndex < 0 || columnIndex >= importedRow.ColumnCount() {
continue
}
value := importedRow.GetData(columnIndex)
rowData[column] = value
}
if t.transactionDataTable.rowParser != nil {
rowData, rowDataValid, err = t.transactionDataTable.rowParser.Parse(rowData)
if err != nil {
log.Errorf(ctx, "[imported_transaction_data_table.Next] cannot parse data row, because %s", err.Error())
return nil, err
}
}
return &ImportedTransactionDataRow{
transactionDataTable: t.transactionDataTable,
rowData: rowData,
rowDataValid: rowDataValid,
}, nil
}
// CreateImportedTransactionDataTable returns transaction data table from imported data table
func CreateImportedTransactionDataTable(dataTable ImportedDataTable, dataColumnMapping map[TransactionDataTableColumn]string) *ImportedTransactionDataTable {
return CreateImportedTransactionDataTableWithRowParser(dataTable, dataColumnMapping, nil)
}
// CreateImportedTransactionDataTableWithRowParser returns transaction data table from imported data table
func CreateImportedTransactionDataTableWithRowParser(dataTable ImportedDataTable, dataColumnMapping map[TransactionDataTableColumn]string, rowParser TransactionDataRowParser) *ImportedTransactionDataTable {
headerLineItems := dataTable.HeaderColumnNames()
headerItemMap := make(map[string]int, len(headerLineItems))
for i := 0; i < len(headerLineItems); i++ {
headerItemMap[headerLineItems[i]] = i
}
dataColumnIndexes := make(map[TransactionDataTableColumn]int, len(headerLineItems))
for column, columnName := range dataColumnMapping {
columnIndex, exists := headerItemMap[columnName]
if exists {
dataColumnIndexes[column] = columnIndex
}
}
var addedColumns map[TransactionDataTableColumn]any
if rowParser != nil {
addedColumnsByParser := rowParser.GetAddedColumns()
addedColumns = make(map[TransactionDataTableColumn]any, len(addedColumnsByParser))
for i := 0; i < len(addedColumnsByParser); i++ {
addedColumns[addedColumnsByParser[i]] = true
}
}
return &ImportedTransactionDataTable{
innerDataTable: dataTable,
dataColumnMapping: dataColumnMapping,
dataColumnIndexes: dataColumnIndexes,
rowParser: rowParser,
addedColumns: addedColumns,
}
}
@@ -0,0 +1,75 @@
package datatable
import (
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/models"
)
// TransactionDataTable defines the structure of transaction data table
type TransactionDataTable interface {
// HasColumn returns whether the transaction data table has specified column
HasColumn(column TransactionDataTableColumn) bool
// TransactionRowCount returns the total count of transaction data row
TransactionRowCount() int
// TransactionRowIterator returns the iterator of transaction data row
TransactionRowIterator() TransactionDataRowIterator
}
// TransactionDataRow defines the structure of transaction data row
type TransactionDataRow interface {
// IsValid returns whether this row is valid data for importing
IsValid() bool
// GetData returns the data in the specified column type
GetData(column TransactionDataTableColumn) string
}
// TransactionDataRowIterator defines the structure of transaction data row iterator
type TransactionDataRowIterator interface {
// HasNext returns whether the iterator does not reach the end
HasNext() bool
// Next returns the next transaction data row
Next(ctx core.Context, user *models.User) (daraRow TransactionDataRow, err error)
}
// TransactionDataRowParser defines the structure of transaction data row parser
type TransactionDataRowParser interface {
// GetAddedColumns returns the added columns after converting the data row
GetAddedColumns() []TransactionDataTableColumn
// Parse returns the converted transaction data row
Parse(data map[TransactionDataTableColumn]string) (rowData map[TransactionDataTableColumn]string, rowDataValid bool, err error)
}
// TransactionDataTableBuilder defines the structure of data table builder
type TransactionDataTableBuilder interface {
// AppendTransaction appends the specified transaction to data builder
AppendTransaction(data map[TransactionDataTableColumn]string)
// ReplaceDelimiters returns the text after removing the delimiters
ReplaceDelimiters(text string) string
}
// TransactionDataTableColumn represents the data column type of data table
type TransactionDataTableColumn byte
// Transaction data table columns
const (
TRANSACTION_DATA_TABLE_TRANSACTION_TIME TransactionDataTableColumn = 1
TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE TransactionDataTableColumn = 2
TRANSACTION_DATA_TABLE_TRANSACTION_TYPE TransactionDataTableColumn = 3
TRANSACTION_DATA_TABLE_CATEGORY TransactionDataTableColumn = 4
TRANSACTION_DATA_TABLE_SUB_CATEGORY TransactionDataTableColumn = 5
TRANSACTION_DATA_TABLE_ACCOUNT_NAME TransactionDataTableColumn = 6
TRANSACTION_DATA_TABLE_ACCOUNT_CURRENCY TransactionDataTableColumn = 7
TRANSACTION_DATA_TABLE_AMOUNT TransactionDataTableColumn = 8
TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME TransactionDataTableColumn = 9
TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_CURRENCY TransactionDataTableColumn = 10
TRANSACTION_DATA_TABLE_RELATED_AMOUNT TransactionDataTableColumn = 11
TRANSACTION_DATA_TABLE_GEOGRAPHIC_LOCATION TransactionDataTableColumn = 12
TRANSACTION_DATA_TABLE_TAGS TransactionDataTableColumn = 13
TRANSACTION_DATA_TABLE_DESCRIPTION TransactionDataTableColumn = 14
)
@@ -1,152 +0,0 @@
package datatable
import (
"time"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
// WritableDataTable defines the structure of writable data table
type WritableDataTable struct {
allData []map[DataTableColumn]string
columns []DataTableColumn
}
// WritableDataRow defines the structure of data row of writable data table
type WritableDataRow struct {
dataTable *WritableDataTable
rowData map[DataTableColumn]string
}
// WritableDataRowIterator defines the structure of data row iterator of writable data table
type WritableDataRowIterator struct {
dataTable *WritableDataTable
nextIndex int
}
// Add appends a new record to data table
func (t *WritableDataTable) Add(data map[DataTableColumn]string) {
finalData := make(map[DataTableColumn]string, len(data))
for i := 0; i < len(t.columns); i++ {
column := t.columns[i]
if value, exists := data[column]; exists {
finalData[column] = value
}
}
t.allData = append(t.allData, finalData)
}
// Get returns the record in the specified index
func (t *WritableDataTable) Get(index int) ImportedDataRow {
if index >= len(t.allData) {
return nil
}
rowData := t.allData[index]
return &WritableDataRow{
dataTable: t,
rowData: rowData,
}
}
// DataRowCount returns the total count of data row
func (t *WritableDataTable) DataRowCount() int {
return len(t.allData)
}
// GetDataColumnMapping returns data column map for data importer
func (t *WritableDataTable) GetDataColumnMapping() map[DataTableColumn]string {
dataColumnMapping := make(map[DataTableColumn]string, len(t.columns))
for i := 0; i < len(t.columns); i++ {
column := t.columns[i]
dataColumnMapping[column] = utils.IntToString(int(column))
}
return dataColumnMapping
}
// HeaderLineColumnNames returns the header column name list
func (t *WritableDataTable) HeaderLineColumnNames() []string {
columnIndexes := make([]string, len(t.columns))
for i := 0; i < len(t.columns); i++ {
columnIndexes[i] = utils.IntToString(int(t.columns[i]))
}
return columnIndexes
}
// DataRowIterator returns the iterator of data row
func (t *WritableDataTable) DataRowIterator() ImportedDataRowIterator {
return &WritableDataRowIterator{
dataTable: t,
nextIndex: 0,
}
}
// IsValid returns whether this row contains valid data for importing
func (r *WritableDataRow) IsValid() bool {
return true
}
// ColumnCount returns the total count of column in this data row
func (r *WritableDataRow) ColumnCount() int {
return len(r.rowData)
}
// GetData returns the data in the specified column index
func (r *WritableDataRow) GetData(columnIndex int) string {
if columnIndex >= len(r.dataTable.columns) {
return ""
}
dataColumn := r.dataTable.columns[columnIndex]
return r.rowData[dataColumn]
}
// GetTime returns the time in the specified column index
func (r *WritableDataRow) GetTime(columnIndex int, timezoneOffset int16) (time.Time, error) {
return utils.ParseFromLongDateTime(r.GetData(columnIndex), timezoneOffset)
}
// GetTimezoneOffset returns the time zone offset in the specified column index
func (r *WritableDataRow) GetTimezoneOffset(columnIndex int) (*time.Location, error) {
return utils.ParseFromTimezoneOffset(r.GetData(columnIndex))
}
// HasNext returns whether the iterator does not reach the end
func (t *WritableDataRowIterator) HasNext() bool {
return t.nextIndex < len(t.dataTable.allData)
}
// Next returns the next imported data row
func (t *WritableDataRowIterator) Next(ctx core.Context, user *models.User) ImportedDataRow {
if t.nextIndex >= len(t.dataTable.allData) {
return nil
}
rowData := t.dataTable.allData[t.nextIndex]
t.nextIndex++
return &WritableDataRow{
dataTable: t.dataTable,
rowData: rowData,
}
}
// CreateNewWritableDataTable returns a new writable data table according to the specified columns
func CreateNewWritableDataTable(columns []DataTableColumn) *WritableDataTable {
return &WritableDataTable{
allData: make([]map[DataTableColumn]string, 0),
columns: columns,
}
}
@@ -1,208 +0,0 @@
package datatable
import (
"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 TestWritableDataTableAdd(t *testing.T) {
columns := make([]DataTableColumn, 5)
columns[0] = DATA_TABLE_TRANSACTION_TIME
columns[1] = DATA_TABLE_TRANSACTION_TYPE
columns[2] = DATA_TABLE_SUB_CATEGORY
columns[3] = DATA_TABLE_ACCOUNT_NAME
columns[4] = DATA_TABLE_AMOUNT
writableDataTable := CreateNewWritableDataTable(columns)
assert.Equal(t, 0, writableDataTable.DataRowCount())
expectedTransactionUnixTime := time.Now().Unix()
expectedTextualTransactionTime := utils.FormatUnixTimeToLongDateTime(expectedTransactionUnixTime, time.Local)
expectedTransactionType := "Expense"
expectedSubCategory := "Test Category"
expectedAccountName := "Test Account"
expectedAmount := "123.45"
writableDataTable.Add(map[DataTableColumn]string{
DATA_TABLE_TRANSACTION_TIME: expectedTextualTransactionTime,
DATA_TABLE_TRANSACTION_TYPE: expectedTransactionType,
DATA_TABLE_SUB_CATEGORY: expectedSubCategory,
DATA_TABLE_ACCOUNT_NAME: expectedAccountName,
DATA_TABLE_AMOUNT: expectedAmount,
})
assert.Equal(t, 1, writableDataTable.DataRowCount())
dataRow := writableDataTable.Get(0)
actualTransactionTime, err := dataRow.GetTime(0, utils.GetTimezoneOffsetMinutes(time.Local))
assert.Nil(t, err)
actualTransactionUnixTime := actualTransactionTime.Unix()
assert.Equal(t, expectedTransactionUnixTime, actualTransactionUnixTime)
actualTextualTransactionTime := dataRow.GetData(0)
assert.Equal(t, expectedTextualTransactionTime, actualTextualTransactionTime)
actualTransactionType := dataRow.GetData(1)
assert.Equal(t, expectedTransactionType, actualTransactionType)
actualSubCategory := dataRow.GetData(2)
assert.Equal(t, expectedSubCategory, actualSubCategory)
actualAccountName := dataRow.GetData(3)
assert.Equal(t, expectedAccountName, actualAccountName)
actualAmount := dataRow.GetData(4)
assert.Equal(t, expectedAmount, actualAmount)
}
func TestWritableDataTableAdd_NotExistsColumn(t *testing.T) {
columns := make([]DataTableColumn, 1)
columns[0] = DATA_TABLE_TRANSACTION_TIME
writableDataTable := CreateNewWritableDataTable(columns)
expectedTransactionUnixTime := time.Now().Unix()
expectedTextualTransactionTime := utils.FormatUnixTimeToLongDateTime(expectedTransactionUnixTime, time.Local)
expectedTransactionType := "Expense"
writableDataTable.Add(map[DataTableColumn]string{
DATA_TABLE_TRANSACTION_TIME: expectedTextualTransactionTime,
DATA_TABLE_TRANSACTION_TYPE: expectedTransactionType,
})
assert.Equal(t, 1, writableDataTable.DataRowCount())
dataRow := writableDataTable.Get(0)
assert.Equal(t, 1, dataRow.ColumnCount())
}
func TestWritableDataTableGet_NotExistsRow(t *testing.T) {
columns := make([]DataTableColumn, 1)
columns[0] = DATA_TABLE_TRANSACTION_TIME
writableDataTable := CreateNewWritableDataTable(columns)
assert.Equal(t, 0, writableDataTable.DataRowCount())
dataRow := writableDataTable.Get(0)
assert.Nil(t, dataRow)
}
func TestWritableDataRowGetData_NotExistsColumn(t *testing.T) {
columns := make([]DataTableColumn, 1)
columns[0] = DATA_TABLE_TRANSACTION_TIME
writableDataTable := CreateNewWritableDataTable(columns)
expectedTransactionUnixTime := time.Now().Unix()
expectedTextualTransactionTime := utils.FormatUnixTimeToLongDateTime(expectedTransactionUnixTime, time.Local)
writableDataTable.Add(map[DataTableColumn]string{
DATA_TABLE_TRANSACTION_TIME: expectedTextualTransactionTime,
})
assert.Equal(t, 1, writableDataTable.DataRowCount())
dataRow := writableDataTable.Get(0)
assert.Equal(t, 1, dataRow.ColumnCount())
assert.Equal(t, "", dataRow.GetData(1))
}
func TestWritableDataTableDataRowIterator(t *testing.T) {
columns := make([]DataTableColumn, 5)
columns[0] = DATA_TABLE_TRANSACTION_TIME
columns[1] = DATA_TABLE_TRANSACTION_TYPE
columns[2] = DATA_TABLE_SUB_CATEGORY
columns[3] = DATA_TABLE_ACCOUNT_NAME
columns[4] = DATA_TABLE_AMOUNT
writableDataTable := CreateNewWritableDataTable(columns)
assert.Equal(t, 0, writableDataTable.DataRowCount())
expectedTransactionUnixTimes := make([]int64, 3)
expectedTextualTransactionTimes := make([]string, 3)
expectedTransactionTypes := make([]string, 3)
expectedSubCategories := make([]string, 3)
expectedAccountNames := make([]string, 3)
expectedAmounts := make([]string, 3)
expectedTransactionUnixTimes[0] = time.Now().Add(-5 * time.Hour).Unix()
expectedTextualTransactionTimes[0] = utils.FormatUnixTimeToLongDateTime(expectedTransactionUnixTimes[0], time.Local)
expectedTransactionTypes[0] = "Balance Modification"
expectedSubCategories[0] = ""
expectedAccountNames[0] = "Test Account"
expectedAmounts[0] = "123.45"
writableDataTable.Add(map[DataTableColumn]string{
DATA_TABLE_TRANSACTION_TIME: expectedTextualTransactionTimes[0],
DATA_TABLE_TRANSACTION_TYPE: expectedTransactionTypes[0],
DATA_TABLE_SUB_CATEGORY: expectedSubCategories[0],
DATA_TABLE_ACCOUNT_NAME: expectedAccountNames[0],
DATA_TABLE_AMOUNT: expectedAmounts[0],
})
expectedTransactionUnixTimes[1] = time.Now().Add(-45 * time.Minute).Unix()
expectedTextualTransactionTimes[1] = utils.FormatUnixTimeToLongDateTime(expectedTransactionUnixTimes[1], time.Local)
expectedTransactionTypes[1] = "Expense"
expectedSubCategories[1] = "Test Category2"
expectedAccountNames[1] = "Test Account"
expectedAmounts[1] = "-23.4"
writableDataTable.Add(map[DataTableColumn]string{
DATA_TABLE_TRANSACTION_TIME: expectedTextualTransactionTimes[1],
DATA_TABLE_TRANSACTION_TYPE: expectedTransactionTypes[1],
DATA_TABLE_SUB_CATEGORY: expectedSubCategories[1],
DATA_TABLE_ACCOUNT_NAME: expectedAccountNames[1],
DATA_TABLE_AMOUNT: expectedAmounts[1],
})
expectedTransactionUnixTimes[2] = time.Now().Unix()
expectedTextualTransactionTimes[2] = utils.FormatUnixTimeToLongDateTime(expectedTransactionUnixTimes[2], time.Local)
expectedTransactionTypes[2] = "Income"
expectedSubCategories[2] = "Test Category3"
expectedAccountNames[2] = "Test Account2"
expectedAmounts[2] = "123"
writableDataTable.Add(map[DataTableColumn]string{
DATA_TABLE_TRANSACTION_TIME: expectedTextualTransactionTimes[2],
DATA_TABLE_TRANSACTION_TYPE: expectedTransactionTypes[2],
DATA_TABLE_SUB_CATEGORY: expectedSubCategories[2],
DATA_TABLE_ACCOUNT_NAME: expectedAccountNames[2],
DATA_TABLE_AMOUNT: expectedAmounts[2],
})
assert.Equal(t, 3, writableDataTable.DataRowCount())
index := 0
iterator := writableDataTable.DataRowIterator()
for iterator.HasNext() {
dataRow := iterator.Next(core.NewNullContext(), &models.User{})
actualTransactionTime, err := dataRow.GetTime(0, utils.GetTimezoneOffsetMinutes(time.Local))
assert.Nil(t, err)
actualTransactionUnixTime := actualTransactionTime.Unix()
assert.Equal(t, expectedTransactionUnixTimes[index], actualTransactionUnixTime)
actualTextualTransactionTime := dataRow.GetData(0)
assert.Equal(t, expectedTextualTransactionTimes[index], actualTextualTransactionTime)
actualTransactionType := dataRow.GetData(1)
assert.Equal(t, expectedTransactionTypes[index], actualTransactionType)
actualSubCategory := dataRow.GetData(2)
assert.Equal(t, expectedSubCategories[index], actualSubCategory)
actualAccountName := dataRow.GetData(3)
assert.Equal(t, expectedAccountNames[index], actualAccountName)
actualAmount := dataRow.GetData(4)
assert.Equal(t, expectedAmounts[index], actualAmount)
index++
}
assert.Equal(t, 3, index)
}
@@ -0,0 +1,169 @@
package datatable
import (
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/models"
)
// WritableTransactionDataTable defines the structure of writable transaction data table
type WritableTransactionDataTable struct {
allData []map[TransactionDataTableColumn]string
supportedColumns map[TransactionDataTableColumn]any
rowParser TransactionDataRowParser
addedColumns map[TransactionDataTableColumn]any
}
// WritableTransactionDataRow defines the structure of transaction data row of writable data table
type WritableTransactionDataRow struct {
dataTable *WritableTransactionDataTable
rowData map[TransactionDataTableColumn]string
rowDataValid bool
}
// WritableTransactionDataRowIterator defines the structure of transaction data row iterator of writable data table
type WritableTransactionDataRowIterator struct {
dataTable *WritableTransactionDataTable
nextIndex int
}
// Add appends a new record to data table
func (t *WritableTransactionDataTable) Add(data map[TransactionDataTableColumn]string) {
finalData := make(map[TransactionDataTableColumn]string, len(data))
for column, value := range data {
_, exists := t.supportedColumns[column]
if exists {
finalData[column] = value
}
}
t.allData = append(t.allData, finalData)
}
// Get returns the record in the specified index
func (t *WritableTransactionDataTable) Get(index int) *WritableTransactionDataRow {
if index >= len(t.allData) {
return nil
}
rowData := t.allData[index]
return &WritableTransactionDataRow{
dataTable: t,
rowData: rowData,
}
}
// HasColumn returns whether the data table has specified column
func (t *WritableTransactionDataTable) HasColumn(column TransactionDataTableColumn) bool {
_, exists := t.supportedColumns[column]
if exists {
return exists
}
if t.addedColumns != nil {
_, exists = t.addedColumns[column]
if exists {
return exists
}
}
return false
}
// TransactionRowCount returns the total count of transaction data row
func (t *WritableTransactionDataTable) TransactionRowCount() int {
return len(t.allData)
}
// TransactionRowIterator returns the iterator of transaction data row
func (t *WritableTransactionDataTable) TransactionRowIterator() TransactionDataRowIterator {
return &WritableTransactionDataRowIterator{
dataTable: t,
nextIndex: 0,
}
}
// ColumnCount returns the total count of column in this data row
func (r *WritableTransactionDataRow) ColumnCount() int {
return len(r.rowData)
}
// IsValid returns whether this row is valid data for importing
func (r *WritableTransactionDataRow) IsValid() bool {
return r.rowDataValid
}
// GetData returns the data in the specified column type
func (r *WritableTransactionDataRow) GetData(column TransactionDataTableColumn) string {
return r.rowData[column]
}
// HasNext returns whether the iterator does not reach the end
func (t *WritableTransactionDataRowIterator) HasNext() bool {
return t.nextIndex < len(t.dataTable.allData)
}
// Next returns the next transaction data row
func (t *WritableTransactionDataRowIterator) Next(ctx core.Context, user *models.User) (daraRow TransactionDataRow, err error) {
if t.nextIndex >= len(t.dataTable.allData) {
return nil, nil
}
rowData := t.dataTable.allData[t.nextIndex]
rowDataValid := true
if t.dataTable.rowParser != nil {
rowData, rowDataValid, err = t.dataTable.rowParser.Parse(rowData)
if err != nil {
log.Errorf(ctx, "[writable_transaction_data_table.Next] cannot parse data row, because %s", err.Error())
return nil, err
}
}
t.nextIndex++
return &WritableTransactionDataRow{
dataTable: t.dataTable,
rowData: rowData,
rowDataValid: rowDataValid,
}, nil
}
// CreateNewWritableTransactionDataTable returns a new writable transaction data table according to the specified columns
func CreateNewWritableTransactionDataTable(columns []TransactionDataTableColumn) *WritableTransactionDataTable {
return CreateNewWritableTransactionDataTableWithRowParser(columns, nil)
}
// CreateNewWritableTransactionDataTableWithRowParser returns a new writable transaction data table according to the specified columns
func CreateNewWritableTransactionDataTableWithRowParser(columns []TransactionDataTableColumn, rowParser TransactionDataRowParser) *WritableTransactionDataTable {
supportedColumns := make(map[TransactionDataTableColumn]any, len(columns))
for i := 0; i < len(columns); i++ {
column := columns[i]
supportedColumns[column] = true
}
var addedColumns map[TransactionDataTableColumn]any
if rowParser != nil {
addedColumnsByParser := rowParser.GetAddedColumns()
addedColumns = make(map[TransactionDataTableColumn]any, len(addedColumnsByParser))
for i := 0; i < len(addedColumnsByParser); i++ {
addedColumns[addedColumnsByParser[i]] = true
}
}
return &WritableTransactionDataTable{
allData: make([]map[TransactionDataTableColumn]string, 0),
supportedColumns: supportedColumns,
rowParser: rowParser,
addedColumns: addedColumns,
}
}
@@ -0,0 +1,196 @@
package datatable
import (
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/models"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
func TestWritableDataTableAdd(t *testing.T) {
columns := make([]TransactionDataTableColumn, 5)
columns[0] = TRANSACTION_DATA_TABLE_TRANSACTION_TIME
columns[1] = TRANSACTION_DATA_TABLE_TRANSACTION_TYPE
columns[2] = TRANSACTION_DATA_TABLE_SUB_CATEGORY
columns[3] = TRANSACTION_DATA_TABLE_ACCOUNT_NAME
columns[4] = TRANSACTION_DATA_TABLE_AMOUNT
writableDataTable := CreateNewWritableTransactionDataTable(columns)
assert.Equal(t, 0, writableDataTable.TransactionRowCount())
expectedTransactionTime := "2024-09-01 01:23:45"
expectedTransactionType := "Expense"
expectedSubCategory := "Test Category"
expectedAccountName := "Test Account"
expectedAmount := "123.45"
writableDataTable.Add(map[TransactionDataTableColumn]string{
TRANSACTION_DATA_TABLE_TRANSACTION_TIME: expectedTransactionTime,
TRANSACTION_DATA_TABLE_TRANSACTION_TYPE: expectedTransactionType,
TRANSACTION_DATA_TABLE_SUB_CATEGORY: expectedSubCategory,
TRANSACTION_DATA_TABLE_ACCOUNT_NAME: expectedAccountName,
TRANSACTION_DATA_TABLE_AMOUNT: expectedAmount,
})
assert.Equal(t, 1, writableDataTable.TransactionRowCount())
dataRow := writableDataTable.Get(0)
actualTransactionTime := dataRow.GetData(TRANSACTION_DATA_TABLE_TRANSACTION_TIME)
assert.Equal(t, expectedTransactionTime, actualTransactionTime)
actualTransactionType := dataRow.GetData(TRANSACTION_DATA_TABLE_TRANSACTION_TYPE)
assert.Equal(t, expectedTransactionType, actualTransactionType)
actualSubCategory := dataRow.GetData(TRANSACTION_DATA_TABLE_SUB_CATEGORY)
assert.Equal(t, expectedSubCategory, actualSubCategory)
actualAccountName := dataRow.GetData(TRANSACTION_DATA_TABLE_ACCOUNT_NAME)
assert.Equal(t, expectedAccountName, actualAccountName)
actualAmount := dataRow.GetData(TRANSACTION_DATA_TABLE_AMOUNT)
assert.Equal(t, expectedAmount, actualAmount)
}
func TestWritableDataTableAdd_NotExistsColumn(t *testing.T) {
columns := make([]TransactionDataTableColumn, 1)
columns[0] = TRANSACTION_DATA_TABLE_TRANSACTION_TIME
writableDataTable := CreateNewWritableTransactionDataTable(columns)
expectedTransactionUnixTime := time.Now().Unix()
expectedTextualTransactionTime := utils.FormatUnixTimeToLongDateTime(expectedTransactionUnixTime, time.Local)
expectedTransactionType := "Expense"
writableDataTable.Add(map[TransactionDataTableColumn]string{
TRANSACTION_DATA_TABLE_TRANSACTION_TIME: expectedTextualTransactionTime,
TRANSACTION_DATA_TABLE_TRANSACTION_TYPE: expectedTransactionType,
})
assert.Equal(t, 1, writableDataTable.TransactionRowCount())
dataRow := writableDataTable.Get(0)
assert.Equal(t, 1, dataRow.ColumnCount())
}
func TestWritableDataTableGet_NotExistsRow(t *testing.T) {
columns := make([]TransactionDataTableColumn, 1)
columns[0] = TRANSACTION_DATA_TABLE_TRANSACTION_TIME
writableDataTable := CreateNewWritableTransactionDataTable(columns)
assert.Equal(t, 0, writableDataTable.TransactionRowCount())
dataRow := writableDataTable.Get(0)
assert.Nil(t, dataRow)
}
func TestWritableDataRowGetData_NotExistsColumn(t *testing.T) {
columns := make([]TransactionDataTableColumn, 1)
columns[0] = TRANSACTION_DATA_TABLE_TRANSACTION_TIME
writableDataTable := CreateNewWritableTransactionDataTable(columns)
expectedTransactionUnixTime := time.Now().Unix()
expectedTextualTransactionTime := utils.FormatUnixTimeToLongDateTime(expectedTransactionUnixTime, time.Local)
writableDataTable.Add(map[TransactionDataTableColumn]string{
TRANSACTION_DATA_TABLE_TRANSACTION_TIME: expectedTextualTransactionTime,
})
assert.Equal(t, 1, writableDataTable.TransactionRowCount())
dataRow := writableDataTable.Get(0)
assert.Equal(t, 1, dataRow.ColumnCount())
assert.Equal(t, "", dataRow.GetData(TRANSACTION_DATA_TABLE_TRANSACTION_TYPE))
}
func TestWritableDataTableDataRowIterator(t *testing.T) {
columns := make([]TransactionDataTableColumn, 5)
columns[0] = TRANSACTION_DATA_TABLE_TRANSACTION_TIME
columns[1] = TRANSACTION_DATA_TABLE_TRANSACTION_TYPE
columns[2] = TRANSACTION_DATA_TABLE_SUB_CATEGORY
columns[3] = TRANSACTION_DATA_TABLE_ACCOUNT_NAME
columns[4] = TRANSACTION_DATA_TABLE_AMOUNT
writableDataTable := CreateNewWritableTransactionDataTable(columns)
assert.Equal(t, 0, writableDataTable.TransactionRowCount())
expectedTransactionUnixTimes := make([]int64, 3)
expectedTransactionTimes := make([]string, 3)
expectedTransactionTypes := make([]string, 3)
expectedSubCategories := make([]string, 3)
expectedAccountNames := make([]string, 3)
expectedAmounts := make([]string, 3)
expectedTransactionUnixTimes[0] = time.Now().Add(-5 * time.Hour).Unix()
expectedTransactionTimes[0] = utils.FormatUnixTimeToLongDateTime(expectedTransactionUnixTimes[0], time.Local)
expectedTransactionTypes[0] = "Balance Modification"
expectedSubCategories[0] = ""
expectedAccountNames[0] = "Test Account"
expectedAmounts[0] = "123.45"
writableDataTable.Add(map[TransactionDataTableColumn]string{
TRANSACTION_DATA_TABLE_TRANSACTION_TIME: expectedTransactionTimes[0],
TRANSACTION_DATA_TABLE_TRANSACTION_TYPE: expectedTransactionTypes[0],
TRANSACTION_DATA_TABLE_SUB_CATEGORY: expectedSubCategories[0],
TRANSACTION_DATA_TABLE_ACCOUNT_NAME: expectedAccountNames[0],
TRANSACTION_DATA_TABLE_AMOUNT: expectedAmounts[0],
})
expectedTransactionUnixTimes[1] = time.Now().Add(-45 * time.Minute).Unix()
expectedTransactionTimes[1] = utils.FormatUnixTimeToLongDateTime(expectedTransactionUnixTimes[1], time.Local)
expectedTransactionTypes[1] = "Expense"
expectedSubCategories[1] = "Test Category2"
expectedAccountNames[1] = "Test Account"
expectedAmounts[1] = "-23.4"
writableDataTable.Add(map[TransactionDataTableColumn]string{
TRANSACTION_DATA_TABLE_TRANSACTION_TIME: expectedTransactionTimes[1],
TRANSACTION_DATA_TABLE_TRANSACTION_TYPE: expectedTransactionTypes[1],
TRANSACTION_DATA_TABLE_SUB_CATEGORY: expectedSubCategories[1],
TRANSACTION_DATA_TABLE_ACCOUNT_NAME: expectedAccountNames[1],
TRANSACTION_DATA_TABLE_AMOUNT: expectedAmounts[1],
})
expectedTransactionUnixTimes[2] = time.Now().Unix()
expectedTransactionTimes[2] = utils.FormatUnixTimeToLongDateTime(expectedTransactionUnixTimes[2], time.Local)
expectedTransactionTypes[2] = "Income"
expectedSubCategories[2] = "Test Category3"
expectedAccountNames[2] = "Test Account2"
expectedAmounts[2] = "123"
writableDataTable.Add(map[TransactionDataTableColumn]string{
TRANSACTION_DATA_TABLE_TRANSACTION_TIME: expectedTransactionTimes[2],
TRANSACTION_DATA_TABLE_TRANSACTION_TYPE: expectedTransactionTypes[2],
TRANSACTION_DATA_TABLE_SUB_CATEGORY: expectedSubCategories[2],
TRANSACTION_DATA_TABLE_ACCOUNT_NAME: expectedAccountNames[2],
TRANSACTION_DATA_TABLE_AMOUNT: expectedAmounts[2],
})
assert.Equal(t, 3, writableDataTable.TransactionRowCount())
index := 0
iterator := writableDataTable.TransactionRowIterator()
for iterator.HasNext() {
dataRow, err := iterator.Next(core.NewNullContext(), &models.User{})
assert.Nil(t, err)
actualTransactionTime := dataRow.GetData(TRANSACTION_DATA_TABLE_TRANSACTION_TIME)
assert.Equal(t, expectedTransactionTimes[index], actualTransactionTime)
actualTransactionType := dataRow.GetData(TRANSACTION_DATA_TABLE_TRANSACTION_TYPE)
assert.Equal(t, expectedTransactionTypes[index], actualTransactionType)
actualSubCategory := dataRow.GetData(TRANSACTION_DATA_TABLE_SUB_CATEGORY)
assert.Equal(t, expectedSubCategories[index], actualSubCategory)
actualAccountName := dataRow.GetData(TRANSACTION_DATA_TABLE_ACCOUNT_NAME)
assert.Equal(t, expectedAccountNames[index], actualAccountName)
actualAmount := dataRow.GetData(TRANSACTION_DATA_TABLE_AMOUNT)
assert.Equal(t, expectedAmounts[index], actualAmount)
index++
}
assert.Equal(t, 3, index)
}