code refactor

This commit is contained in:
MaysWind
2024-09-07 03:01:57 +08:00
parent 9622d5de06
commit bc3cb79f91
15 changed files with 954 additions and 641 deletions
+23 -23
View File
@@ -20,16 +20,16 @@ const pageCountForDataExport = 1000
// DataManagementsApi represents data management api
type DataManagementsApi struct {
ApiUsingConfig
ezBookKeepingCsvExporter *converters.EzBookKeepingCSVFileConverter
ezBookKeepingTsvExporter *converters.EzBookKeepingTSVFileConverter
tokens *services.TokenService
users *services.UserService
accounts *services.AccountService
transactions *services.TransactionService
categories *services.TransactionCategoryService
tags *services.TransactionTagService
pictures *services.TransactionPictureService
templates *services.TransactionTemplateService
ezBookKeepingCsvConverter converters.TransactionDataConverter
ezBookKeepingTsvConverter converters.TransactionDataConverter
tokens *services.TokenService
users *services.UserService
accounts *services.AccountService
transactions *services.TransactionService
categories *services.TransactionCategoryService
tags *services.TransactionTagService
pictures *services.TransactionPictureService
templates *services.TransactionTemplateService
}
// Initialize a data management api singleton instance
@@ -38,16 +38,16 @@ var (
ApiUsingConfig: ApiUsingConfig{
container: settings.Container,
},
ezBookKeepingCsvExporter: &converters.EzBookKeepingCSVFileConverter{},
ezBookKeepingTsvExporter: &converters.EzBookKeepingTSVFileConverter{},
tokens: services.Tokens,
users: services.Users,
accounts: services.Accounts,
transactions: services.Transactions,
categories: services.TransactionCategories,
tags: services.TransactionTags,
pictures: services.TransactionPictures,
templates: services.TransactionTemplates,
ezBookKeepingCsvConverter: converters.EzBookKeepingTransactionDataCSVFileConverter,
ezBookKeepingTsvConverter: converters.EzBookKeepingTransactionDataTSVFileConverter,
tokens: services.Tokens,
users: services.Users,
accounts: services.Accounts,
transactions: services.Transactions,
categories: services.TransactionCategories,
tags: services.TransactionTags,
pictures: services.TransactionPictures,
templates: services.TransactionTemplates,
}
)
@@ -247,12 +247,12 @@ func (a *DataManagementsApi) getExportedFileContent(c *core.WebContext, fileType
return nil, "", errs.ErrOperationFailed
}
var dataExporter converters.DataConverter
var dataExporter converters.TransactionDataExporter
if fileType == "tsv" {
dataExporter = a.ezBookKeepingTsvExporter
dataExporter = a.ezBookKeepingTsvConverter
} else {
dataExporter = a.ezBookKeepingCsvExporter
dataExporter = a.ezBookKeepingCsvConverter
}
result, err := dataExporter.ToExportedContent(uid, allTransactions, accountMap, categoryMap, tagMap, tagIndexes)
+26 -26
View File
@@ -20,16 +20,16 @@ const pageCountForDataExport = 1000
// UserDataCli represents user data cli
type UserDataCli struct {
CliUsingConfig
ezBookKeepingCsvExporter *converters.EzBookKeepingCSVFileConverter
ezBookKeepingTsvExporter *converters.EzBookKeepingTSVFileConverter
accounts *services.AccountService
transactions *services.TransactionService
categories *services.TransactionCategoryService
tags *services.TransactionTagService
users *services.UserService
twoFactorAuthorizations *services.TwoFactorAuthorizationService
tokens *services.TokenService
forgetPasswords *services.ForgetPasswordService
ezBookKeepingCsvConverter converters.TransactionDataConverter
ezBookKeepingTsvConverter converters.TransactionDataConverter
accounts *services.AccountService
transactions *services.TransactionService
categories *services.TransactionCategoryService
tags *services.TransactionTagService
users *services.UserService
twoFactorAuthorizations *services.TwoFactorAuthorizationService
tokens *services.TokenService
forgetPasswords *services.ForgetPasswordService
}
// Initialize an user data cli singleton instance
@@ -38,16 +38,16 @@ var (
CliUsingConfig: CliUsingConfig{
container: settings.Container,
},
ezBookKeepingCsvExporter: &converters.EzBookKeepingCSVFileConverter{},
ezBookKeepingTsvExporter: &converters.EzBookKeepingTSVFileConverter{},
accounts: services.Accounts,
transactions: services.Transactions,
categories: services.TransactionCategories,
tags: services.TransactionTags,
users: services.Users,
twoFactorAuthorizations: services.TwoFactorAuthorizations,
tokens: services.Tokens,
forgetPasswords: services.ForgetPasswords,
ezBookKeepingCsvConverter: converters.EzBookKeepingTransactionDataCSVFileConverter,
ezBookKeepingTsvConverter: converters.EzBookKeepingTransactionDataTSVFileConverter,
accounts: services.Accounts,
transactions: services.Transactions,
categories: services.TransactionCategories,
tags: services.TransactionTags,
users: services.Users,
twoFactorAuthorizations: services.TwoFactorAuthorizations,
tokens: services.Tokens,
forgetPasswords: services.ForgetPasswords,
}
)
@@ -645,12 +645,12 @@ func (l *UserDataCli) ExportTransaction(c *core.CliContext, username string, fil
return nil, err
}
var dataExporter converters.DataConverter
var dataExporter converters.TransactionDataExporter
if fileType == "tsv" {
dataExporter = l.ezBookKeepingTsvExporter
dataExporter = l.ezBookKeepingTsvConverter
} else {
dataExporter = l.ezBookKeepingCsvExporter
dataExporter = l.ezBookKeepingCsvConverter
}
result, err := dataExporter.ToExportedContent(uid, allTransactions, accountMap, categoryMap, tagMap, tagIndexesMap)
@@ -669,12 +669,12 @@ func (l *UserDataCli) ImportTransaction(c *core.CliContext, username string, fil
return errs.ErrUsernameIsEmpty
}
var dataImporter converters.DataConverter
var dataImporter converters.TransactionDataImporter
if fileType == "ezbookkeeping_csv" {
dataImporter = l.ezBookKeepingCsvExporter
dataImporter = l.ezBookKeepingCsvConverter
} else if fileType == "ezbookkeeping_tsv" {
dataImporter = l.ezBookKeepingTsvExporter
dataImporter = l.ezBookKeepingTsvConverter
} else {
return errs.ErrImportFileTypeNotSupported
}
+37
View File
@@ -0,0 +1,37 @@
package converters
// 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 {
// 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
}
// DataTableBuilder defines the structure of data table builder
type DataTableBuilder interface {
// AppendTransaction appends the specified transaction to data builder
AppendTransaction(data map[DataTableColumn]string)
}
@@ -0,0 +1,493 @@
package converters
import (
"fmt"
"sort"
"strings"
"time"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/utils"
"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
)
// DataTableTransactionDataConverter defines the structure of data table importer for transaction data
type DataTableTransactionDataConverter struct {
dataColumnMapping map[DataTableColumn]string
transactionTypeMapping map[models.TransactionDbType]string
transactionTypeNameMapping map[string]models.TransactionDbType
columnSeparator string
lineSeparator string
geoLocationSeparator string
transactionTagSeparator string
}
func (c *DataTableTransactionDataConverter) buildExportedContent(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 {
for i := 0; i < len(transactions); i++ {
transaction := transactions[i]
if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN {
continue
}
dataRowMap := make(map[DataTableColumn]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] = c.replaceDelimiters(c.getDisplayTransactionTypeName(transaction.Type))
dataRowMap[DATA_TABLE_CATEGORY] = c.getExportedTransactionCategoryName(transaction.CategoryId, categoryMap)
dataRowMap[DATA_TABLE_SUB_CATEGORY] = c.getExportedTransactionSubCategoryName(transaction.CategoryId, categoryMap)
dataRowMap[DATA_TABLE_ACCOUNT_NAME] = c.getExportedAccountName(transaction.AccountId, accountMap)
dataRowMap[DATA_TABLE_ACCOUNT_CURRENCY] = c.getAccountCurrency(transaction.AccountId, accountMap)
dataRowMap[DATA_TABLE_AMOUNT] = utils.FormatAmount(transaction.Amount)
if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT {
dataRowMap[DATA_TABLE_RELATED_ACCOUNT_NAME] = c.getExportedAccountName(transaction.RelatedAccountId, accountMap)
dataRowMap[DATA_TABLE_RELATED_ACCOUNT_CURRENCY] = c.getAccountCurrency(transaction.RelatedAccountId, accountMap)
dataRowMap[DATA_TABLE_RELATED_AMOUNT] = utils.FormatAmount(transaction.RelatedAccountAmount)
}
dataRowMap[DATA_TABLE_GEOGRAPHIC_LOCATION] = c.getExportedGeographicLocation(transaction)
dataRowMap[DATA_TABLE_TAGS] = c.getExportedTags(transaction.TransactionId, allTagIndexes, tagMap)
dataRowMap[DATA_TABLE_DESCRIPTION] = c.replaceDelimiters(transaction.Comment)
dataTableBuilder.AppendTransaction(dataRowMap)
}
return nil
}
func (c *DataTableTransactionDataConverter) parseImportedData(user *models.User, dataTable ImportedDataTable, defaultTimezoneOffset int16, accountMap map[string]*models.Account, categoryMap map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) ([]*models.Transaction, []*models.Account, []*models.TransactionCategory, []*models.TransactionTag, error) {
if dataTable.DataRowCount() < 1 {
return nil, nil, nil, nil, errs.ErrOperationFailed
}
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 || !amount2ColumnExists {
return nil, nil, nil, nil, errs.ErrFormatInvalid
}
if accountMap == nil {
accountMap = make(map[string]*models.Account)
}
if categoryMap == nil {
categoryMap = make(map[string]*models.TransactionCategory)
}
if tagMap == nil {
tagMap = make(map[string]*models.TransactionTag)
}
allNewTransactions := make(ImportedTransactionSlice, 0, dataTable.DataRowCount())
allNewAccounts := make([]*models.Account, 0)
allNewSubCategories := make([]*models.TransactionCategory, 0)
allNewTags := make([]*models.TransactionTag, 0)
dataRowIterator := dataTable.DataRowIterator()
for dataRowIterator.HasNext() {
dataRow := dataRowIterator.Next()
columnCount := dataRow.ColumnCount()
if columnCount < 1 {
continue
}
if columnCount < len(headerLineItems) {
return nil, nil, nil, nil, errs.ErrFormatInvalid
}
timezoneOffset := defaultTimezoneOffset
if timezoneColumnExists {
transactionTimezone, err := utils.ParseFromTimezoneOffset(dataRow.GetData(timezoneColumnIdx))
if err != nil {
return nil, nil, nil, nil, err
}
timezoneOffset = utils.GetTimezoneOffsetMinutes(transactionTimezone)
}
transactionTime, err := utils.ParseFromLongDateTime(dataRow.GetData(timeColumnIdx), timezoneOffset)
if err != nil {
return nil, nil, nil, nil, err
}
transactionDbType, err := c.getTransactionDbType(dataRow.GetData(typeColumnIdx))
if err != nil {
return nil, nil, nil, nil, err
}
categoryId := int64(0)
if transactionDbType != models.TRANSACTION_DB_TYPE_MODIFY_BALANCE {
transactionCategoryType, err := c.getTransactionCategoryType(transactionDbType)
if err != nil {
return nil, nil, nil, nil, err
}
subCategoryName := dataRow.GetData(subCategoryColumnIdx)
if subCategoryName == "" {
return nil, nil, nil, nil, errs.ErrFormatInvalid
}
subCategory, exists := categoryMap[subCategoryName]
if !exists {
subCategory = c.createNewTransactionCategoryModel(user.Uid, subCategoryName, transactionCategoryType)
allNewSubCategories = append(allNewSubCategories, subCategory)
categoryMap[subCategoryName] = subCategory
}
categoryId = subCategory.CategoryId
}
accountName := dataRow.GetData(accountColumnIdx)
if accountName == "" {
return nil, nil, nil, nil, errs.ErrFormatInvalid
}
account, exists := accountMap[accountName]
if !exists {
currency := user.DefaultCurrency
if accountCurrencyColumnExists {
currency = dataRow.GetData(accountCurrencyColumnIdx)
if _, ok := validators.AllCurrencyNames[currency]; !ok {
return nil, nil, nil, nil, errs.ErrAccountCurrencyInvalid
}
}
account = c.createNewAccountModel(user.Uid, accountName, currency)
allNewAccounts = append(allNewAccounts, account)
accountMap[accountName] = account
}
if accountCurrencyColumnExists {
if account.Currency != dataRow.GetData(accountCurrencyColumnIdx) {
return nil, nil, nil, nil, errs.ErrAccountCurrencyInvalid
}
}
amount, err := utils.ParseAmount(dataRow.GetData(amountColumnIdx))
if err != nil {
return nil, nil, nil, nil, err
}
relatedAccountId := int64(0)
relatedAccountAmount := int64(0)
if transactionDbType == models.TRANSACTION_DB_TYPE_TRANSFER_OUT {
account2Name := dataRow.GetData(account2ColumnIdx)
if account2Name == "" {
return nil, nil, nil, nil, errs.ErrFormatInvalid
}
account2, exists := accountMap[account2Name]
if !exists {
currency := user.DefaultCurrency
if accountCurrencyColumnExists {
currency = dataRow.GetData(account2CurrencyColumnIdx)
if _, ok := validators.AllCurrencyNames[currency]; !ok {
return nil, nil, nil, nil, errs.ErrAccountCurrencyInvalid
}
}
account2 = c.createNewAccountModel(user.Uid, account2Name, currency)
allNewAccounts = append(allNewAccounts, account2)
accountMap[account2Name] = account2
}
if account2CurrencyColumnExists {
if account2.Currency != dataRow.GetData(account2CurrencyColumnIdx) {
return nil, nil, nil, nil, errs.ErrAccountCurrencyInvalid
}
}
relatedAccountId = account2.AccountId
relatedAccountAmount, err = utils.ParseAmount(dataRow.GetData(amount2ColumnIdx))
if err != nil {
return nil, nil, nil, nil, err
}
}
geoLongitude := float64(0)
geoLatitude := float64(0)
if geoLocationExists {
geoLocationItems := strings.Split(dataRow.GetData(geoLocationIdx), c.geoLocationSeparator)
if len(geoLocationItems) == 2 {
geoLongitude, err = utils.StringToFloat64(geoLocationItems[0])
if err != nil {
return nil, nil, nil, nil, err
}
geoLatitude, err = utils.StringToFloat64(geoLocationItems[1])
if err != nil {
return nil, nil, nil, nil, err
}
}
}
if tagsColumnExists {
tagNames := strings.Split(dataRow.GetData(tagsColumnIdx), c.transactionTagSeparator)
for i := 0; i < len(tagNames); i++ {
tagName := tagNames[i]
if tagName == "" {
continue
}
tag, exists := tagMap[tagName]
if !exists {
tag = c.createNewTransactionTagModel(user.Uid, tagName)
allNewTags = append(allNewTags, tag)
tagMap[tagName] = tag
}
}
}
description := ""
if descriptionColumnExists {
description = dataRow.GetData(descriptionColumnIdx)
}
transaction := &models.Transaction{
Uid: user.Uid,
Type: transactionDbType,
CategoryId: categoryId,
TransactionTime: utils.GetMinTransactionTimeFromUnixTime(transactionTime.Unix()),
TimezoneUtcOffset: timezoneOffset,
AccountId: account.AccountId,
Amount: amount,
HideAmount: false,
RelatedAccountId: relatedAccountId,
RelatedAccountAmount: relatedAccountAmount,
Comment: description,
GeoLongitude: geoLongitude,
GeoLatitude: geoLatitude,
CreatedIp: "127.0.0.1",
}
allNewTransactions = append(allNewTransactions, transaction)
}
sort.Sort(allNewTransactions)
return allNewTransactions, allNewAccounts, allNewSubCategories, allNewTags, nil
}
func (c *DataTableTransactionDataConverter) getTransactionDbType(transactionTypeName string) (models.TransactionDbType, error) {
transactionType, exists := c.transactionTypeNameMapping[transactionTypeName]
if !exists {
return 0, errs.ErrTransactionTypeInvalid
}
return transactionType, nil
}
func (c *DataTableTransactionDataConverter) getTransactionCategoryType(transactionType models.TransactionDbType) (models.TransactionCategoryType, error) {
if transactionType == models.TRANSACTION_DB_TYPE_INCOME {
return models.CATEGORY_TYPE_INCOME, nil
} else if transactionType == models.TRANSACTION_DB_TYPE_EXPENSE {
return models.CATEGORY_TYPE_EXPENSE, nil
} else if transactionType == models.TRANSACTION_DB_TYPE_TRANSFER_OUT {
return models.CATEGORY_TYPE_TRANSFER, nil
} else {
return 0, errs.ErrTransactionTypeInvalid
}
}
func (c *DataTableTransactionDataConverter) getDisplayTransactionTypeName(transactionDbType models.TransactionDbType) string {
transactionTypeName, exists := c.transactionTypeMapping[transactionDbType]
if !exists {
return ""
}
return transactionTypeName
}
func (c *DataTableTransactionDataConverter) getExportedTransactionCategoryName(categoryId int64, categoryMap map[int64]*models.TransactionCategory) string {
category, exists := categoryMap[categoryId]
if !exists {
return ""
}
if category.ParentCategoryId == 0 {
return c.replaceDelimiters(category.Name)
}
parentCategory, exists := categoryMap[category.ParentCategoryId]
if !exists {
return ""
}
return c.replaceDelimiters(parentCategory.Name)
}
func (c *DataTableTransactionDataConverter) getExportedTransactionSubCategoryName(categoryId int64, categoryMap map[int64]*models.TransactionCategory) string {
category, exists := categoryMap[categoryId]
if exists {
return c.replaceDelimiters(category.Name)
} else {
return ""
}
}
func (c *DataTableTransactionDataConverter) getExportedAccountName(accountId int64, accountMap map[int64]*models.Account) string {
account, exists := accountMap[accountId]
if exists {
return c.replaceDelimiters(account.Name)
} else {
return ""
}
}
func (c *DataTableTransactionDataConverter) getAccountCurrency(accountId int64, accountMap map[int64]*models.Account) string {
account, exists := accountMap[accountId]
if exists {
return c.replaceDelimiters(account.Currency)
} else {
return ""
}
}
func (c *DataTableTransactionDataConverter) getExportedGeographicLocation(transaction *models.Transaction) string {
if transaction.GeoLongitude != 0 || transaction.GeoLatitude != 0 {
return fmt.Sprintf("%f%s%f", transaction.GeoLongitude, c.geoLocationSeparator, transaction.GeoLatitude)
}
return ""
}
func (c *DataTableTransactionDataConverter) getExportedTags(transactionId int64, allTagIndexes map[int64][]int64, tagMap map[int64]*models.TransactionTag) string {
tagIndexes, exists := allTagIndexes[transactionId]
if !exists {
return ""
}
var ret strings.Builder
for i := 0; i < len(tagIndexes); i++ {
tagIndex := tagIndexes[i]
tag, exists := tagMap[tagIndex]
if !exists {
continue
}
if ret.Len() > 0 {
ret.WriteString(c.transactionTagSeparator)
}
ret.WriteString(strings.Replace(tag.Name, c.transactionTagSeparator, " ", -1))
}
return c.replaceDelimiters(ret.String())
}
func (c *DataTableTransactionDataConverter) replaceDelimiters(text string) string {
text = strings.Replace(text, "\r\n", " ", -1)
text = strings.Replace(text, "\r", " ", -1)
text = strings.Replace(text, "\n", " ", -1)
text = strings.Replace(text, c.columnSeparator, " ", -1)
text = strings.Replace(text, c.lineSeparator, " ", -1)
return text
}
func (c *DataTableTransactionDataConverter) createNewAccountModel(uid int64, accountName string, currency string) *models.Account {
return &models.Account{
Uid: uid,
Name: accountName,
Currency: currency,
}
}
func (c *DataTableTransactionDataConverter) createNewTransactionCategoryModel(uid int64, categoryName string, transactionCategoryType models.TransactionCategoryType) *models.TransactionCategory {
return &models.TransactionCategory{
Uid: uid,
Name: categoryName,
Type: transactionCategoryType,
}
}
func (c *DataTableTransactionDataConverter) createNewTransactionTagModel(uid int64, tagName string) *models.TransactionTag {
return &models.TransactionTag{
Uid: uid,
Name: tagName,
}
}
-22
View File
@@ -1,22 +0,0 @@
package converters
import (
"github.com/mayswind/ezbookkeeping/pkg/models"
)
// EzBookKeepingCSVFileConverter defines the structure of CSV file converter
type EzBookKeepingCSVFileConverter struct {
EzBookKeepingPlainFileConverter
}
const csvSeparator = ","
// ToExportedContent returns the exported CSV data
func (e *EzBookKeepingCSVFileConverter) ToExportedContent(uid int64, transactions []*models.Transaction, accountMap map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, allTagIndexes map[int64][]int64) ([]byte, error) {
return e.toExportedContent(uid, csvSeparator, transactions, accountMap, categoryMap, tagMap, allTagIndexes)
}
// ParseImportedData parses transactions of ezbookkeeping CSV data
func (e *EzBookKeepingCSVFileConverter) ParseImportedData(user *models.User, data []byte, defaultTimezoneOffset int16, accountMap map[string]*models.Account, categoryMap map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) ([]*models.Transaction, []*models.Account, []*models.TransactionCategory, []*models.TransactionTag, error) {
return e.parseImportedData(user, csvSeparator, data, defaultTimezoneOffset, accountMap, categoryMap, tagMap)
}
-491
View File
@@ -1,491 +0,0 @@
package converters
import (
"fmt"
"sort"
"strings"
"time"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/utils"
"github.com/mayswind/ezbookkeeping/pkg/validators"
)
// EzBookKeepingPlainFileConverter defines the structure of plain file converter
type EzBookKeepingPlainFileConverter struct {
}
const lineSeparator = "\n"
const geoLocationSeparator = " "
const transactionTagSeparator = ";"
const headerLine = "Time,Timezone,Type,Category,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description" + lineSeparator
const dataLineFormat = "%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s" + lineSeparator
// toExportedContent returns the exported plain data
func (e *EzBookKeepingPlainFileConverter) toExportedContent(uid int64, separator string, transactions []*models.Transaction, accountMap map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, allTagIndexes map[int64][]int64) ([]byte, error) {
var ret strings.Builder
ret.Grow(len(transactions) * 100)
actualHeaderLine := headerLine
actualDataLineFormat := dataLineFormat
if separator != "," {
actualHeaderLine = strings.Replace(headerLine, ",", separator, -1)
actualDataLineFormat = strings.Replace(dataLineFormat, ",", separator, -1)
}
ret.WriteString(actualHeaderLine)
for i := 0; i < len(transactions); i++ {
transaction := transactions[i]
if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN {
continue
}
transactionTimeZone := time.FixedZone("Transaction Timezone", int(transaction.TimezoneUtcOffset)*60)
transactionTime := utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime), transactionTimeZone)
transactionTimezone := utils.FormatTimezoneOffset(transactionTimeZone)
transactionType := e.getTransactionTypeName(transaction.Type)
category := e.replaceDelimiters(e.getTransactionCategoryName(transaction.CategoryId, categoryMap), separator)
subCategory := e.replaceDelimiters(e.getTransactionSubCategoryName(transaction.CategoryId, categoryMap), separator)
account := e.replaceDelimiters(e.getAccountName(transaction.AccountId, accountMap), separator)
accountCurrency := e.getAccountCurrency(transaction.AccountId, accountMap)
amount := utils.FormatAmount(transaction.Amount)
account2 := ""
account2Currency := ""
account2Amount := ""
geoLocation := ""
if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT {
account2 = e.replaceDelimiters(e.getAccountName(transaction.RelatedAccountId, accountMap), separator)
account2Currency = e.getAccountCurrency(transaction.RelatedAccountId, accountMap)
account2Amount = utils.FormatAmount(transaction.RelatedAccountAmount)
}
if transaction.GeoLongitude != 0 || transaction.GeoLatitude != 0 {
geoLocation = fmt.Sprintf("%f%s%f", transaction.GeoLongitude, geoLocationSeparator, transaction.GeoLatitude)
}
tags := e.replaceDelimiters(e.getTags(transaction.TransactionId, allTagIndexes, tagMap), separator)
comment := e.replaceDelimiters(transaction.Comment, separator)
ret.WriteString(fmt.Sprintf(actualDataLineFormat, transactionTime, transactionTimezone, transactionType, category, subCategory, account, accountCurrency, amount, account2, account2Currency, account2Amount, geoLocation, tags, comment))
}
return []byte(ret.String()), nil
}
func (e *EzBookKeepingPlainFileConverter) parseImportedData(user *models.User, separator string, data []byte, defaultTimezoneOffset int16, accountMap map[string]*models.Account, categoryMap map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) ([]*models.Transaction, []*models.Account, []*models.TransactionCategory, []*models.TransactionTag, error) {
lines := strings.Split(string(data), lineSeparator)
if len(lines) < 2 {
return nil, nil, nil, nil, errs.ErrOperationFailed
}
headerLineItems := strings.Split(lines[0], separator)
headerItemMap := make(map[string]int)
for i := 0; i < len(headerLineItems); i++ {
headerItemMap[headerLineItems[i]] = i
}
timeColumnIdx, timeColumnExists := headerItemMap["Time"]
timezoneColumnIdx, timezoneColumnExists := headerItemMap["Timezone"]
typeColumnIdx, typeColumnExists := headerItemMap["Type"]
subCategoryColumnIdx, subCategoryColumnExists := headerItemMap["Sub Category"]
accountColumnIdx, accountColumnExists := headerItemMap["Account"]
accountCurrencyColumnIdx, accountCurrencyColumnExists := headerItemMap["Account Currency"]
amountColumnIdx, amountColumnExists := headerItemMap["Amount"]
account2ColumnIdx, account2ColumnExists := headerItemMap["Account2"]
account2CurrencyColumnIdx, account2CurrencyColumnExists := headerItemMap["Account2 Currency"]
amount2ColumnIdx, amount2ColumnExists := headerItemMap["Account2 Amount"]
geoLocationIdx, geoLocationExists := headerItemMap["Geographic Location"]
tagsColumnIdx, tagsColumnExists := headerItemMap["Tags"]
descriptionColumnIdx, descriptionColumnExists := headerItemMap["Description"]
if !timeColumnExists || !typeColumnExists || !subCategoryColumnExists ||
!accountColumnExists || !amountColumnExists || !account2ColumnExists || !amount2ColumnExists {
return nil, nil, nil, nil, errs.ErrFormatInvalid
}
if accountMap == nil {
accountMap = make(map[string]*models.Account)
}
if categoryMap == nil {
categoryMap = make(map[string]*models.TransactionCategory)
}
if tagMap == nil {
tagMap = make(map[string]*models.TransactionTag)
}
allNewTransactions := make(ImportTransactionSlice, 0, len(lines))
allNewAccounts := make([]*models.Account, 0)
allNewSubCategories := make([]*models.TransactionCategory, 0)
allNewTags := make([]*models.TransactionTag, 0)
for i := 1; i < len(lines); i++ {
line := lines[i]
if len(line) < 1 {
continue
}
lineItems := strings.Split(line, separator)
if len(lineItems) < len(headerLineItems) {
return nil, nil, nil, nil, errs.ErrFormatInvalid
}
timezoneOffset := defaultTimezoneOffset
if timezoneColumnExists {
transactionTimezone, err := utils.ParseFromTimezoneOffset(lineItems[timezoneColumnIdx])
if err != nil {
return nil, nil, nil, nil, err
}
timezoneOffset = utils.GetTimezoneOffsetMinutes(transactionTimezone)
}
transactionTime, err := utils.ParseFromLongDateTime(lineItems[timeColumnIdx], timezoneOffset)
if err != nil {
return nil, nil, nil, nil, err
}
transactionDbType, err := e.getTransactionDbType(lineItems[typeColumnIdx])
if err != nil {
return nil, nil, nil, nil, err
}
categoryId := int64(0)
if transactionDbType != models.TRANSACTION_DB_TYPE_MODIFY_BALANCE {
transactionCategoryType, err := e.getTransactionCategoryType(transactionDbType)
if err != nil {
return nil, nil, nil, nil, err
}
subCategoryName := lineItems[subCategoryColumnIdx]
if subCategoryName == "" {
return nil, nil, nil, nil, errs.ErrFormatInvalid
}
subCategory, exists := categoryMap[subCategoryName]
if !exists {
subCategory = e.createNewTransactionCategoryModel(user.Uid, subCategoryName, transactionCategoryType)
allNewSubCategories = append(allNewSubCategories, subCategory)
categoryMap[subCategoryName] = subCategory
}
categoryId = subCategory.CategoryId
}
accountName := lineItems[accountColumnIdx]
if accountName == "" {
return nil, nil, nil, nil, errs.ErrFormatInvalid
}
account, exists := accountMap[accountName]
if !exists {
currency := user.DefaultCurrency
if accountCurrencyColumnExists {
currency = lineItems[accountCurrencyColumnIdx]
if _, ok := validators.AllCurrencyNames[currency]; !ok {
return nil, nil, nil, nil, errs.ErrAccountCurrencyInvalid
}
}
account = e.createNewAccountModel(user.Uid, accountName, currency)
allNewAccounts = append(allNewAccounts, account)
accountMap[accountName] = account
}
if accountCurrencyColumnExists {
if account.Currency != lineItems[accountCurrencyColumnIdx] {
return nil, nil, nil, nil, errs.ErrAccountCurrencyInvalid
}
}
amount, err := utils.ParseAmount(lineItems[amountColumnIdx])
if err != nil {
return nil, nil, nil, nil, err
}
relatedAccountId := int64(0)
relatedAccountAmount := int64(0)
if transactionDbType == models.TRANSACTION_DB_TYPE_TRANSFER_OUT {
account2Name := lineItems[account2ColumnIdx]
if account2Name == "" {
return nil, nil, nil, nil, errs.ErrFormatInvalid
}
account2, exists := accountMap[account2Name]
if !exists {
currency := user.DefaultCurrency
if accountCurrencyColumnExists {
currency = lineItems[account2CurrencyColumnIdx]
if _, ok := validators.AllCurrencyNames[currency]; !ok {
return nil, nil, nil, nil, errs.ErrAccountCurrencyInvalid
}
}
account2 = e.createNewAccountModel(user.Uid, account2Name, currency)
allNewAccounts = append(allNewAccounts, account2)
accountMap[account2Name] = account2
}
if account2CurrencyColumnExists {
if account2.Currency != lineItems[account2CurrencyColumnIdx] {
return nil, nil, nil, nil, errs.ErrAccountCurrencyInvalid
}
}
relatedAccountId = account2.AccountId
relatedAccountAmount, err = utils.ParseAmount(lineItems[amount2ColumnIdx])
if err != nil {
return nil, nil, nil, nil, err
}
}
geoLongitude := float64(0)
geoLatitude := float64(0)
if geoLocationExists {
geoLocationItems := strings.Split(lineItems[geoLocationIdx], geoLocationSeparator)
if len(geoLocationItems) == 2 {
geoLongitude, err = utils.StringToFloat64(geoLocationItems[0])
if err != nil {
return nil, nil, nil, nil, err
}
geoLatitude, err = utils.StringToFloat64(geoLocationItems[1])
if err != nil {
return nil, nil, nil, nil, err
}
}
}
if tagsColumnExists {
tagNames := strings.Split(lineItems[tagsColumnIdx], transactionTagSeparator)
for i := 0; i < len(tagNames); i++ {
tagName := tagNames[i]
if tagName == "" {
continue
}
tag, exists := tagMap[tagName]
if !exists {
tag = e.createNewTransactionTagModel(user.Uid, tagName)
allNewTags = append(allNewTags, tag)
tagMap[tagName] = tag
}
}
}
description := ""
if descriptionColumnExists {
description = lineItems[descriptionColumnIdx]
}
transaction := &models.Transaction{
Uid: user.Uid,
Type: transactionDbType,
CategoryId: categoryId,
TransactionTime: utils.GetMinTransactionTimeFromUnixTime(transactionTime.Unix()),
TimezoneUtcOffset: timezoneOffset,
AccountId: account.AccountId,
Amount: amount,
HideAmount: false,
RelatedAccountId: relatedAccountId,
RelatedAccountAmount: relatedAccountAmount,
Comment: description,
GeoLongitude: geoLongitude,
GeoLatitude: geoLatitude,
CreatedIp: "127.0.0.1",
}
allNewTransactions = append(allNewTransactions, transaction)
}
sort.Sort(allNewTransactions)
return allNewTransactions, allNewAccounts, allNewSubCategories, allNewTags, nil
}
func (e *EzBookKeepingPlainFileConverter) getTransactionTypeName(transactionDbType models.TransactionDbType) string {
if transactionDbType == models.TRANSACTION_DB_TYPE_MODIFY_BALANCE {
return "Balance Modification"
} else if transactionDbType == models.TRANSACTION_DB_TYPE_INCOME {
return "Income"
} else if transactionDbType == models.TRANSACTION_DB_TYPE_EXPENSE {
return "Expense"
} else if transactionDbType == models.TRANSACTION_DB_TYPE_TRANSFER_OUT || transactionDbType == models.TRANSACTION_DB_TYPE_TRANSFER_IN {
return "Transfer"
} else {
return ""
}
}
func (e *EzBookKeepingPlainFileConverter) getTransactionDbType(transactionTypeName string) (models.TransactionDbType, error) {
if transactionTypeName == "Balance Modification" {
return models.TRANSACTION_DB_TYPE_MODIFY_BALANCE, nil
} else if transactionTypeName == "Income" {
return models.TRANSACTION_DB_TYPE_INCOME, nil
} else if transactionTypeName == "Expense" {
return models.TRANSACTION_DB_TYPE_EXPENSE, nil
} else if transactionTypeName == "Transfer" {
return models.TRANSACTION_DB_TYPE_TRANSFER_OUT, nil
} else {
return 0, errs.ErrTransactionTypeInvalid
}
}
func (e *EzBookKeepingPlainFileConverter) getTransactionCategoryType(transactionType models.TransactionDbType) (models.TransactionCategoryType, error) {
if transactionType == models.TRANSACTION_DB_TYPE_INCOME {
return models.CATEGORY_TYPE_INCOME, nil
} else if transactionType == models.TRANSACTION_DB_TYPE_EXPENSE {
return models.CATEGORY_TYPE_EXPENSE, nil
} else if transactionType == models.TRANSACTION_DB_TYPE_TRANSFER_OUT {
return models.CATEGORY_TYPE_TRANSFER, nil
} else {
return 0, errs.ErrTransactionTypeInvalid
}
}
func (e *EzBookKeepingPlainFileConverter) getTransactionCategoryName(categoryId int64, categoryMap map[int64]*models.TransactionCategory) string {
category, exists := categoryMap[categoryId]
if !exists {
return ""
}
if category.ParentCategoryId == 0 {
return category.Name
}
parentCategory, exists := categoryMap[category.ParentCategoryId]
if !exists {
return ""
}
return parentCategory.Name
}
func (e *EzBookKeepingPlainFileConverter) getTransactionSubCategoryName(categoryId int64, categoryMap map[int64]*models.TransactionCategory) string {
category, exists := categoryMap[categoryId]
if exists {
return category.Name
} else {
return ""
}
}
func (e *EzBookKeepingPlainFileConverter) getAccountName(accountId int64, accountMap map[int64]*models.Account) string {
account, exists := accountMap[accountId]
if exists {
return account.Name
} else {
return ""
}
}
func (e *EzBookKeepingPlainFileConverter) getAccountCurrency(accountId int64, accountMap map[int64]*models.Account) string {
account, exists := accountMap[accountId]
if exists {
return account.Currency
} else {
return ""
}
}
func (e *EzBookKeepingPlainFileConverter) getTags(transactionId int64, allTagIndexes map[int64][]int64, tagMap map[int64]*models.TransactionTag) string {
tagIndexes, exists := allTagIndexes[transactionId]
if !exists {
return ""
}
var ret strings.Builder
for i := 0; i < len(tagIndexes); i++ {
tagIndex := tagIndexes[i]
tag, exists := tagMap[tagIndex]
if !exists {
continue
}
if ret.Len() > 0 {
ret.WriteString(transactionTagSeparator)
}
ret.WriteString(strings.Replace(tag.Name, transactionTagSeparator, " ", -1))
}
return ret.String()
}
func (e *EzBookKeepingPlainFileConverter) replaceDelimiters(text string, separator string) string {
text = strings.Replace(text, separator, " ", -1)
text = strings.Replace(text, "\r\n", " ", -1)
text = strings.Replace(text, "\r", " ", -1)
text = strings.Replace(text, "\n", " ", -1)
return text
}
func (e *EzBookKeepingPlainFileConverter) createNewAccountModel(uid int64, accountName string, currency string) *models.Account {
return &models.Account{
Uid: uid,
Name: accountName,
Currency: currency,
}
}
func (e *EzBookKeepingPlainFileConverter) createNewTransactionCategoryModel(uid int64, categoryName string, transactionCategoryType models.TransactionCategoryType) *models.TransactionCategory {
return &models.TransactionCategory{
Uid: uid,
Name: categoryName,
Type: transactionCategoryType,
}
}
func (e *EzBookKeepingPlainFileConverter) createNewTransactionTagModel(uid int64, tagName string) *models.TransactionTag {
return &models.TransactionTag{
Uid: uid,
Name: tagName,
}
}
@@ -0,0 +1,24 @@
package converters
// ezBookKeepingTransactionDataCSVFileConverter defines the structure of CSV file converter
type ezBookKeepingTransactionDataCSVFileConverter struct {
ezBookKeepingTransactionDataPlainTextConverter
}
// Initialize an ezbookkeeping transaction data csv file converter singleton instance
var (
EzBookKeepingTransactionDataCSVFileConverter = &ezBookKeepingTransactionDataCSVFileConverter{
ezBookKeepingTransactionDataPlainTextConverter{
DataTableTransactionDataConverter: DataTableTransactionDataConverter{
dataColumnMapping: ezbookkeepingDataColumnNameMapping,
transactionTypeMapping: ezbookkeepingTransactionTypeNameMapping,
transactionTypeNameMapping: ezbookkeepingNameTransactionTypeMapping,
columnSeparator: ",",
lineSeparator: "\n",
geoLocationSeparator: " ",
transactionTagSeparator: ";",
},
columns: ezbookkeepingDataColumns,
},
}
)
@@ -0,0 +1,82 @@
package converters
import (
"github.com/mayswind/ezbookkeeping/pkg/models"
)
// ezBookKeepingTransactionDataPlainTextConverter defines the structure of plain file converter for transaction data
type ezBookKeepingTransactionDataPlainTextConverter struct {
DataTableTransactionDataConverter
columns []DataTableColumn
}
var ezbookkeepingDataColumnNameMapping = map[DataTableColumn]string{
DATA_TABLE_TRANSACTION_TIME: "Time",
DATA_TABLE_TRANSACTION_TIMEZONE: "Timezone",
DATA_TABLE_TRANSACTION_TYPE: "Type",
DATA_TABLE_CATEGORY: "Category",
DATA_TABLE_SUB_CATEGORY: "Sub Category",
DATA_TABLE_ACCOUNT_NAME: "Account",
DATA_TABLE_ACCOUNT_CURRENCY: "Account Currency",
DATA_TABLE_AMOUNT: "Amount",
DATA_TABLE_RELATED_ACCOUNT_NAME: "Account2",
DATA_TABLE_RELATED_ACCOUNT_CURRENCY: "Account2 Currency",
DATA_TABLE_RELATED_AMOUNT: "Account2 Amount",
DATA_TABLE_GEOGRAPHIC_LOCATION: "Geographic Location",
DATA_TABLE_TAGS: "Tags",
DATA_TABLE_DESCRIPTION: "Description",
}
var ezbookkeepingTransactionTypeNameMapping = map[models.TransactionDbType]string{
models.TRANSACTION_DB_TYPE_MODIFY_BALANCE: "Balance Modification",
models.TRANSACTION_DB_TYPE_INCOME: "Income",
models.TRANSACTION_DB_TYPE_EXPENSE: "Expense",
models.TRANSACTION_DB_TYPE_TRANSFER_OUT: "Transfer",
models.TRANSACTION_DB_TYPE_TRANSFER_IN: "Transfer",
}
var ezbookkeepingNameTransactionTypeMapping = map[string]models.TransactionDbType{
"Balance Modification": models.TRANSACTION_DB_TYPE_MODIFY_BALANCE,
"Income": models.TRANSACTION_DB_TYPE_INCOME,
"Expense": models.TRANSACTION_DB_TYPE_EXPENSE,
"Transfer": models.TRANSACTION_DB_TYPE_TRANSFER_OUT,
}
var ezbookkeepingDataColumns = []DataTableColumn{
DATA_TABLE_TRANSACTION_TIME,
DATA_TABLE_TRANSACTION_TIMEZONE,
DATA_TABLE_TRANSACTION_TYPE,
DATA_TABLE_CATEGORY,
DATA_TABLE_SUB_CATEGORY,
DATA_TABLE_ACCOUNT_NAME,
DATA_TABLE_ACCOUNT_CURRENCY,
DATA_TABLE_AMOUNT,
DATA_TABLE_RELATED_ACCOUNT_NAME,
DATA_TABLE_RELATED_ACCOUNT_CURRENCY,
DATA_TABLE_RELATED_AMOUNT,
DATA_TABLE_GEOGRAPHIC_LOCATION,
DATA_TABLE_TAGS,
DATA_TABLE_DESCRIPTION,
}
// ToExportedContent returns the exported plain text transaction data
func (c *ezBookKeepingTransactionDataPlainTextConverter) ToExportedContent(uid int64, transactions []*models.Transaction, accountMap map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, allTagIndexes map[int64][]int64) ([]byte, error) {
dataTableBuilder := createNewezbookkeepingTransactionPlainTextDataTableBuilder(len(transactions), c.columns, c.dataColumnMapping, c.columnSeparator, c.lineSeparator)
err := c.buildExportedContent(dataTableBuilder, uid, transactions, accountMap, categoryMap, tagMap, allTagIndexes)
if err != nil {
return nil, err
}
return []byte(dataTableBuilder.String()), nil
}
func (c *ezBookKeepingTransactionDataPlainTextConverter) ParseImportedData(user *models.User, data []byte, defaultTimezoneOffset int16, accountMap map[string]*models.Account, categoryMap map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) ([]*models.Transaction, []*models.Account, []*models.TransactionCategory, []*models.TransactionTag, error) {
dataTable, err := createNewezbookkeepingTransactionPlainTextDataTable(string(data), c.columnSeparator, c.lineSeparator)
if err != nil {
return nil, nil, nil, nil, err
}
return c.parseImportedData(user, dataTable, defaultTimezoneOffset, accountMap, categoryMap, tagMap)
}
@@ -10,7 +10,7 @@ import (
)
func TestEzBookKeepingPlainFileConverterToExportedContent(t *testing.T) {
converter := &EzBookKeepingPlainFileConverter{}
converter := EzBookKeepingTransactionDataCSVFileConverter
transactions := make([]*models.Transaction, 3)
transactions[0] = &models.Transaction{
@@ -116,21 +116,21 @@ func TestEzBookKeepingPlainFileConverterToExportedContent(t *testing.T) {
"2024-09-01 12:34:56,+08:00,Income,Test Category,Test Sub Category,Test Account,CNY,123.45,,,,123.450000 45.670000,Test Tag;Test Tag2,Hello World\n" +
"2024-09-01 12:34:56,+00:00,Expense,Test Category2,Test Sub Category2,Test Account,CNY,-0.10,,,,,Test Tag,Foo#Bar\n" +
"2024-09-01 12:34:56,-05:00,Transfer,Test Category3,Test Sub Category3,Test Account,CNY,123.45,Test Account2,USD,17.35,,Test Tag2,T\te s t test\n"
actualContent, err := converter.toExportedContent(123, ",", transactions, accountMap, categoryMap, tagMap, allTagIndexes)
actualContent, err := converter.ToExportedContent(123, transactions, accountMap, categoryMap, tagMap, allTagIndexes)
assert.Nil(t, err)
assert.Equal(t, expectedContent, string(actualContent))
}
func TestEzBookKeepingPlainFileConverterParseImportedData_MinimumValidData(t *testing.T) {
converter := &EzBookKeepingPlainFileConverter{}
converter := EzBookKeepingTransactionDataCSVFileConverter
user := &models.User{
Uid: 1234567890,
DefaultCurrency: "CNY",
}
allNewTransactions, allNewAccounts, allNewSubCategories, allNewTags, err := converter.parseImportedData(user, ",", []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
allNewTransactions, allNewAccounts, allNewSubCategories, allNewTags, err := converter.ParseImportedData(user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
"2024-09-01 00:00:00,Balance Modification,,Test Account,123.45,,\n"+
"2024-09-01 01:23:45,Income,Test Category,Test Account,0.12,,\n"+
"2024-09-01 12:34:56,Expense,Test Category2,Test Account,1.00,,\n"+
@@ -182,44 +182,44 @@ func TestEzBookKeepingPlainFileConverterParseImportedData_MinimumValidData(t *te
}
func TestEzBookKeepingPlainFileConverterParseImportedData_ParseInvalidTime(t *testing.T) {
converter := &EzBookKeepingPlainFileConverter{}
converter := EzBookKeepingTransactionDataCSVFileConverter
user := &models.User{
Uid: 1234567890,
DefaultCurrency: "CNY",
}
_, _, _, _, err := converter.parseImportedData(user, ",", []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
_, _, _, _, err := converter.ParseImportedData(user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
"2024-09-01T12:34:56,Expense,Test Category,Test Account,123.45,,"), 0, nil, nil, nil)
assert.NotNil(t, err)
_, _, _, _, err = converter.parseImportedData(user, ",", []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
_, _, _, _, err = converter.ParseImportedData(user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
"09/01/2024 12:34:56,Expense,Test Category,Test Account,123.45,,"), 0, nil, nil, nil)
assert.NotNil(t, err)
}
func TestEzBookKeepingPlainFileConverterParseImportedData_ParseInvalidType(t *testing.T) {
converter := &EzBookKeepingPlainFileConverter{}
converter := EzBookKeepingTransactionDataCSVFileConverter
user := &models.User{
Uid: 1234567890,
DefaultCurrency: "CNY",
}
_, _, _, _, err := converter.parseImportedData(user, ",", []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
_, _, _, _, err := converter.ParseImportedData(user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
"2024-09-01 12:34:56,Type,Test Category,Test Account,123.45,,"), 0, nil, nil, nil)
assert.NotNil(t, err)
}
func TestEzBookKeepingPlainFileConverterParseImportedData_ParseValidTimezone(t *testing.T) {
converter := &EzBookKeepingPlainFileConverter{}
converter := EzBookKeepingTransactionDataCSVFileConverter
user := &models.User{
Uid: 1234567890,
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, err := converter.parseImportedData(user, ",", []byte("Time,Timezone,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
allNewTransactions, _, _, _, err := converter.ParseImportedData(user, []byte("Time,Timezone,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
"2024-09-01 12:34:56,+08:00,Expense,Test Category,Test Account,123.45,,"), 0, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
@@ -227,57 +227,57 @@ func TestEzBookKeepingPlainFileConverterParseImportedData_ParseValidTimezone(t *
}
func TestEzBookKeepingPlainFileConverterParseImportedData_ParseInvalidTimezone(t *testing.T) {
converter := &EzBookKeepingPlainFileConverter{}
converter := EzBookKeepingTransactionDataCSVFileConverter
user := &models.User{
Uid: 1234567890,
DefaultCurrency: "CNY",
}
_, _, _, _, err := converter.parseImportedData(user, ",", []byte("Time,Timezone,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
_, _, _, _, err := converter.ParseImportedData(user, []byte("Time,Timezone,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
"2024-09-01 12:34:56,Asia/Shanghai,Expense,Test Category,Test Account,123.45,,"), 0, nil, nil, nil)
assert.NotNil(t, err)
}
func TestEzBookKeepingPlainFileConverterParseImportedData_ParseInvalidSubCategoryName(t *testing.T) {
converter := &EzBookKeepingPlainFileConverter{}
converter := EzBookKeepingTransactionDataCSVFileConverter
user := &models.User{
Uid: 1234567890,
DefaultCurrency: "CNY",
}
_, _, _, _, err := converter.parseImportedData(user, ",", []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
_, _, _, _, err := converter.ParseImportedData(user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
"2024-09-01 12:34:56,Expense,,Test Account,123.45,,"), 0, nil, nil, nil)
assert.NotNil(t, err)
}
func TestEzBookKeepingPlainFileConverterParseImportedData_ParseInvalidAccountName(t *testing.T) {
converter := &EzBookKeepingPlainFileConverter{}
converter := EzBookKeepingTransactionDataCSVFileConverter
user := &models.User{
Uid: 1234567890,
DefaultCurrency: "CNY",
}
_, _, _, _, err := converter.parseImportedData(user, ",", []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
_, _, _, _, err := converter.ParseImportedData(user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
"2024-09-01 12:34:56,Expense,Test Category,,123.45,,"), 0, nil, nil, nil)
assert.NotNil(t, err)
_, _, _, _, err = converter.parseImportedData(user, ",", []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
_, _, _, _, err = converter.ParseImportedData(user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
"2024-09-01 12:34:56,Transfer,Test Category,Test Account,123.45,,123.45"), 0, nil, nil, nil)
assert.NotNil(t, err)
}
func TestEzBookKeepingPlainFileConverterParseImportedData_ParseValidAccountCurrency(t *testing.T) {
converter := &EzBookKeepingPlainFileConverter{}
converter := EzBookKeepingTransactionDataCSVFileConverter
user := &models.User{
Uid: 1234567890,
DefaultCurrency: "CNY",
}
allNewTransactions, allNewAccounts, _, _, err := converter.parseImportedData(user, ",", []byte("Time,Type,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount\n"+
allNewTransactions, allNewAccounts, _, _, err := converter.ParseImportedData(user, []byte("Time,Type,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount\n"+
"2024-09-01 01:23:45,Balance Modification,Test Category,Test Account,USD,123.45,,,\n"+
"2024-09-01 12:34:56,Transfer,Test Category2,Test Account,USD,1.23,Test Account2,EUR,1.10"), 0, nil, nil, nil)
@@ -296,67 +296,67 @@ func TestEzBookKeepingPlainFileConverterParseImportedData_ParseValidAccountCurre
}
func TestEzBookKeepingPlainFileConverterParseImportedData_ParseInvalidAccountCurrency(t *testing.T) {
converter := &EzBookKeepingPlainFileConverter{}
converter := EzBookKeepingTransactionDataCSVFileConverter
user := &models.User{
Uid: 1234567890,
DefaultCurrency: "CNY",
}
_, _, _, _, err := converter.parseImportedData(user, ",", []byte("Time,Type,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount\n"+
_, _, _, _, err := converter.ParseImportedData(user, []byte("Time,Type,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount\n"+
"2024-09-01 01:23:45,Balance Modification,,Test Account,USD,123.45,,,\n"+
"2024-09-01 12:34:56,Transfer,Test Category3,Test Account,CNY,1.23,Test Account2,EUR,1.10"), 0, nil, nil, nil)
assert.NotNil(t, err)
_, _, _, _, err = converter.parseImportedData(user, ",", []byte("Time,Type,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount\n"+
_, _, _, _, err = converter.ParseImportedData(user, []byte("Time,Type,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount\n"+
"2024-09-01 01:23:45,Balance Modification,,Test Account,USD,123.45,,,\n"+
"2024-09-01 12:34:56,Transfer,Test Category3,Test Account2,CNY,1.23,Test Account,EUR,1.10"), 0, nil, nil, nil)
assert.NotNil(t, err)
}
func TestEzBookKeepingPlainFileConverterParseImportedData_ParseNotSupportedCurrency(t *testing.T) {
converter := &EzBookKeepingPlainFileConverter{}
converter := EzBookKeepingTransactionDataCSVFileConverter
user := &models.User{
Uid: 1234567890,
DefaultCurrency: "CNY",
}
_, _, _, _, err := converter.parseImportedData(user, ",", []byte("Time,Type,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount\n"+
_, _, _, _, err := converter.ParseImportedData(user, []byte("Time,Type,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount\n"+
"2024-09-01 01:23:45,Balance Modification,Test Category,Test Account,XXX,123.45,,,"), 0, nil, nil, nil)
assert.NotNil(t, err)
_, _, _, _, err = converter.parseImportedData(user, ",", []byte("Time,Type,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount\n"+
_, _, _, _, err = converter.ParseImportedData(user, []byte("Time,Type,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount\n"+
"2024-09-01 01:23:45,Transfer,Test Category,Test Account,USD,123.45,Test Account2,XXX,123.45"), 0, nil, nil, nil)
assert.NotNil(t, err)
}
func TestEzBookKeepingPlainFileConverterParseImportedData_ParseInvalidAmount(t *testing.T) {
converter := &EzBookKeepingPlainFileConverter{}
converter := EzBookKeepingTransactionDataCSVFileConverter
user := &models.User{
Uid: 1234567890,
DefaultCurrency: "CNY",
}
_, _, _, _, err := converter.parseImportedData(user, ",", []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
_, _, _, _, err := converter.ParseImportedData(user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
"2024-09-01 12:34:56,Expense,Test Category,Test Account,123 45,,"), 0, nil, nil, nil)
assert.NotNil(t, err)
_, _, _, _, err = converter.parseImportedData(user, ",", []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
_, _, _, _, err = converter.ParseImportedData(user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount\n"+
"2024-09-01 12:34:56,Transfer,Test Category,Test Account,123.45,Test Account2,123 45"), 0, nil, nil, nil)
assert.NotNil(t, err)
}
func TestEzBookKeepingPlainFileConverterParseImportedData_ParseValidGeographicLocation(t *testing.T) {
converter := &EzBookKeepingPlainFileConverter{}
converter := EzBookKeepingTransactionDataCSVFileConverter
user := &models.User{
Uid: 1234567890,
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, err := converter.parseImportedData(user, ",", []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount,Geographic Location\n"+
allNewTransactions, _, _, _, err := converter.ParseImportedData(user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount,Geographic Location\n"+
"2024-09-01 12:34:56,Expense,Test Category,Test Account,123.45,,,123.45 45.56"), 0, nil, nil, nil)
assert.Nil(t, err)
@@ -366,38 +366,38 @@ func TestEzBookKeepingPlainFileConverterParseImportedData_ParseValidGeographicLo
}
func TestEzBookKeepingPlainFileConverterParseImportedData_ParseInvalidGeographicLocation(t *testing.T) {
converter := &EzBookKeepingPlainFileConverter{}
converter := EzBookKeepingTransactionDataCSVFileConverter
user := &models.User{
Uid: 1234567890,
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, err := converter.parseImportedData(user, ",", []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount,Geographic Location\n"+
allNewTransactions, _, _, _, err := converter.ParseImportedData(user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount,Geographic Location\n"+
"2024-09-01 12:34:56,Expense,Test Category,Test Account,123.45,,,1"), 0, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, float64(0), allNewTransactions[0].GeoLongitude)
assert.Equal(t, float64(0), allNewTransactions[0].GeoLatitude)
_, _, _, _, err = converter.parseImportedData(user, ",", []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount,Geographic Location\n"+
_, _, _, _, err = converter.ParseImportedData(user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount,Geographic Location\n"+
"2024-09-01 12:34:56,Expense,Test Category,Test Account,123.45,,,a b"), 0, nil, nil, nil)
assert.NotNil(t, err)
_, _, _, _, err = converter.parseImportedData(user, ",", []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount,Geographic Location\n"+
_, _, _, _, err = converter.ParseImportedData(user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount,Geographic Location\n"+
"2024-09-01 12:34:56,Expense,Test Category,Test Account,123.45,,,1 "), 0, nil, nil, nil)
assert.NotNil(t, err)
}
func TestEzBookKeepingPlainFileConverterParseImportedData_ParseTag(t *testing.T) {
converter := &EzBookKeepingPlainFileConverter{}
converter := EzBookKeepingTransactionDataCSVFileConverter
user := &models.User{
Uid: 1234567890,
DefaultCurrency: "CNY",
}
_, _, _, allNewTags, err := converter.parseImportedData(user, ",", []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount,Tags\n"+
_, _, _, allNewTags, err := converter.ParseImportedData(user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount,Tags\n"+
"2024-09-01 00:00:00,Balance Modification,,Test Account,123.45,,,foo;;bar.;#test;hello\tworld;;"), 0, nil, nil, nil)
assert.Nil(t, err)
@@ -418,14 +418,14 @@ func TestEzBookKeepingPlainFileConverterParseImportedData_ParseTag(t *testing.T)
}
func TestEzBookKeepingPlainFileConverterParseImportedData_ParseDescription(t *testing.T) {
converter := &EzBookKeepingPlainFileConverter{}
converter := EzBookKeepingTransactionDataCSVFileConverter
user := &models.User{
Uid: 1234567890,
DefaultCurrency: "CNY",
}
allNewTransactions, _, _, _, err := converter.parseImportedData(user, ",", []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount,Description\n"+
allNewTransactions, _, _, _, err := converter.ParseImportedData(user, []byte("Time,Type,Sub Category,Account,Amount,Account2,Account2 Amount,Description\n"+
"2024-09-01 12:34:56,Expense,Test Category,Test Account,123.45,,,foo bar\t#test"), 0, nil, nil, nil)
assert.Nil(t, err)
@@ -434,45 +434,45 @@ func TestEzBookKeepingPlainFileConverterParseImportedData_ParseDescription(t *te
}
func TestEzBookKeepingPlainFileConverterParseImportedData_MissingRequiredColumn(t *testing.T) {
converter := &EzBookKeepingPlainFileConverter{}
converter := EzBookKeepingTransactionDataCSVFileConverter
user := &models.User{
Uid: 1,
DefaultCurrency: "CNY",
}
_, _, _, _, err := converter.parseImportedData(user, ",", []byte(""), 0, nil, nil, nil)
_, _, _, _, err := converter.ParseImportedData(user, []byte(""), 0, nil, nil, nil)
assert.NotNil(t, err)
_, _, _, _, err = converter.parseImportedData(user, ",", []byte("Timezone,Type,Category,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n"+
_, _, _, _, err = converter.ParseImportedData(user, []byte("Timezone,Type,Category,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n"+
"+08:00,Balance Modification,Test Category,Test Sub Category,Test Account,CNY,123.45,,,,,,"), 0, nil, nil, nil)
assert.NotNil(t, err)
_, _, _, _, err = converter.parseImportedData(user, ",", []byte("Time,Category,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n"+
_, _, _, _, err = converter.ParseImportedData(user, []byte("Time,Category,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n"+
"2024-09-01 00:00:00,+08:00,Test Category,Test Sub Category,Test Account,CNY,123.45,,,,,,"), 0, nil, nil, nil)
assert.NotNil(t, err)
_, _, _, _, err = converter.parseImportedData(user, ",", []byte("Time,Type,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n"+
_, _, _, _, err = converter.ParseImportedData(user, []byte("Time,Type,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n"+
"2024-09-01 00:00:00,+08:00,Balance Modification,Test Account,CNY,123.45,,,,,,"), 0, nil, nil, nil)
assert.NotNil(t, err)
_, _, _, _, err = converter.parseImportedData(user, ",", []byte("Time,Timezone,Type,Category,Sub Category,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n"+
_, _, _, _, err = converter.ParseImportedData(user, []byte("Time,Timezone,Type,Category,Sub Category,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n"+
"2024-09-01 00:00:00,+08:00,Balance Modification,Test Category,Test Sub Category,CNY,123.45,,,,,,"), 0, nil, nil, nil)
assert.NotNil(t, err)
_, _, _, _, err = converter.parseImportedData(user, ",", []byte("Time,Timezone,Type,Category,Sub Category,Account,Account Currency,Account2,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n"+
_, _, _, _, err = converter.ParseImportedData(user, []byte("Time,Timezone,Type,Category,Sub Category,Account,Account Currency,Account2,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n"+
"2024-09-01 00:00:00,+08:00,Balance Modification,Test Category,Test Sub Category,Test Account,CNY,,,,,,"), 0, nil, nil, nil)
assert.NotNil(t, err)
_, _, _, _, err = converter.parseImportedData(user, ",", []byte("Time,Timezone,Type,Category,Sub Category,Account,Account Currency,Amount,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n"+
_, _, _, _, err = converter.ParseImportedData(user, []byte("Time,Timezone,Type,Category,Sub Category,Account,Account Currency,Amount,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n"+
"2024-09-01 00:00:00,+08:00,Balance Modification,Test Category,Test Sub Category,Test Account,CNY,123.45,,,,,"), 0, nil, nil, nil)
assert.NotNil(t, err)
_, _, _, _, err = converter.parseImportedData(user, ",", []byte("Time,Timezone,Type,Category,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Geographic Location,Tags,Description\n"+
_, _, _, _, err = converter.ParseImportedData(user, []byte("Time,Timezone,Type,Category,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Geographic Location,Tags,Description\n"+
"2024-09-01 00:00:00,+08:00,Balance Modification,Test Category,Test Sub Category,Test Account,CNY,123.45,,,,,"), 0, nil, nil, nil)
assert.NotNil(t, err)
_, _, _, _, err = converter.parseImportedData(user, ",", []byte("Time,Timezone,Type,Category,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n"+
_, _, _, _, err = converter.ParseImportedData(user, []byte("Time,Timezone,Type,Category,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount,Geographic Location,Tags,Description\n"+
"2024-09-01 00:00:00,+08:00,Balance Modification,Test Category,Test Sub Category,Test Account,CNY,123.45,,,,,"), 0, nil, nil, nil)
assert.NotNil(t, err)
}
@@ -0,0 +1,179 @@
package converters
import (
"fmt"
"strings"
"github.com/mayswind/ezbookkeeping/pkg/errs"
)
// ezBookKeepingTransactionPlainTextDataTable defines the structure of ezbookkeeping transaction plain text data table
type ezBookKeepingTransactionPlainTextDataTable struct {
columnSeparator string
lineSeparator string
allLines []string
headerLineColumnNames []string
}
// ezBookKeepingTransactionPlainTextDataRow defines the structure of ezbookkeeping transaction plain text data row
type ezBookKeepingTransactionPlainTextDataRow struct {
allItems []string
}
// ezBookKeepingTransactionPlainTextDataRowIterator defines the structure of ezbookkeeping transaction plain text data row iterator
type ezBookKeepingTransactionPlainTextDataRowIterator struct {
dataTable *ezBookKeepingTransactionPlainTextDataTable
currentIndex int
}
// ezBookKeepingTransactionPlainTextDataTableBuilder defines the structure of ezbookkeeping transaction plain text data table builder
type ezBookKeepingTransactionPlainTextDataTableBuilder struct {
columnSeparator string
lineSeparator string
columns []DataTableColumn
dataColumnNameMapping map[DataTableColumn]string
dataLineFormat string
builder *strings.Builder
}
// DataRowCount returns the total count of data row
func (t *ezBookKeepingTransactionPlainTextDataTable) DataRowCount() int {
if len(t.allLines) < 1 {
return 0
}
return len(t.allLines) - 1
}
// HeaderLineColumnNames returns the header column name list
func (t *ezBookKeepingTransactionPlainTextDataTable) HeaderLineColumnNames() []string {
return t.headerLineColumnNames
}
// DataRowIterator returns the iterator of data row
func (t *ezBookKeepingTransactionPlainTextDataTable) DataRowIterator() ImportedDataRowIterator {
return &ezBookKeepingTransactionPlainTextDataRowIterator{
dataTable: t,
currentIndex: 0,
}
}
// ColumnCount returns the total count of column in this data row
func (r *ezBookKeepingTransactionPlainTextDataRow) ColumnCount() int {
return len(r.allItems)
}
// GetData returns the data in the specified column index
func (r *ezBookKeepingTransactionPlainTextDataRow) GetData(columnIndex int) string {
return r.allItems[columnIndex]
}
// HasNext returns whether the iterator does not reach the end
func (t *ezBookKeepingTransactionPlainTextDataRowIterator) HasNext() bool {
return t.currentIndex+1 < len(t.dataTable.allLines)
}
// Next returns the next imported data row
func (t *ezBookKeepingTransactionPlainTextDataRowIterator) Next() ImportedDataRow {
if t.currentIndex+1 >= len(t.dataTable.allLines) {
return nil
}
t.currentIndex++
rowContent := t.dataTable.allLines[t.currentIndex]
rowItems := strings.Split(rowContent, t.dataTable.columnSeparator)
return &ezBookKeepingTransactionPlainTextDataRow{
allItems: rowItems,
}
}
// AppendTransaction appends the specified transaction to data builder
func (b *ezBookKeepingTransactionPlainTextDataTableBuilder) AppendTransaction(data map[DataTableColumn]string) {
dataRowParams := make([]any, len(b.columns))
for i := 0; i < len(b.columns); i++ {
dataRowParams[i] = data[b.columns[i]]
}
b.builder.WriteString(fmt.Sprintf(b.dataLineFormat, dataRowParams...))
}
// String returns the textual representation of this data
func (b *ezBookKeepingTransactionPlainTextDataTableBuilder) String() string {
return b.builder.String()
}
func (b *ezBookKeepingTransactionPlainTextDataTableBuilder) generateHeaderLine() string {
var ret strings.Builder
for i := 0; i < len(b.columns); i++ {
if ret.Len() > 0 {
ret.WriteString(b.columnSeparator)
}
dataColumn := b.columns[i]
columnName := b.dataColumnNameMapping[dataColumn]
ret.WriteString(columnName)
}
ret.WriteString(b.lineSeparator)
return ret.String()
}
func (b *ezBookKeepingTransactionPlainTextDataTableBuilder) generateDataLineFormat() string {
var ret strings.Builder
for i := 0; i < len(b.columns); i++ {
if ret.Len() > 0 {
ret.WriteString(b.columnSeparator)
}
ret.WriteString("%s")
}
ret.WriteString(b.lineSeparator)
return ret.String()
}
func createNewezbookkeepingTransactionPlainTextDataTable(content string, columnSeparator string, lineSeparator string) (*ezBookKeepingTransactionPlainTextDataTable, error) {
allLines := strings.Split(content, lineSeparator)
if len(allLines) < 2 {
return nil, errs.ErrOperationFailed
}
headerLineItems := strings.Split(allLines[0], columnSeparator)
return &ezBookKeepingTransactionPlainTextDataTable{
columnSeparator: columnSeparator,
lineSeparator: lineSeparator,
allLines: allLines,
headerLineColumnNames: headerLineItems,
}, nil
}
func createNewezbookkeepingTransactionPlainTextDataTableBuilder(transactionCount int, columns []DataTableColumn, dataColumnNameMapping map[DataTableColumn]string, columnSeparator string, lineSeparator string) *ezBookKeepingTransactionPlainTextDataTableBuilder {
var builder strings.Builder
builder.Grow(transactionCount * 100)
dataTableBuilder := &ezBookKeepingTransactionPlainTextDataTableBuilder{
columnSeparator: columnSeparator,
lineSeparator: lineSeparator,
columns: columns,
dataColumnNameMapping: dataColumnNameMapping,
builder: &builder,
}
headerLine := dataTableBuilder.generateHeaderLine()
dataLineFormat := dataTableBuilder.generateDataLineFormat()
dataTableBuilder.builder.WriteString(headerLine)
dataTableBuilder.dataLineFormat = dataLineFormat
return dataTableBuilder
}
@@ -0,0 +1,24 @@
package converters
// ezBookKeepingTransactionDataTSVFileConverter defines the structure of TSV file converter
type ezBookKeepingTransactionDataTSVFileConverter struct {
ezBookKeepingTransactionDataPlainTextConverter
}
// Initialize an ezbookkeeping transaction data tsv file converter singleton instance
var (
EzBookKeepingTransactionDataTSVFileConverter = &ezBookKeepingTransactionDataTSVFileConverter{
ezBookKeepingTransactionDataPlainTextConverter{
DataTableTransactionDataConverter: DataTableTransactionDataConverter{
dataColumnMapping: ezbookkeepingDataColumnNameMapping,
transactionTypeMapping: ezbookkeepingTransactionTypeNameMapping,
transactionTypeNameMapping: ezbookkeepingNameTransactionTypeMapping,
columnSeparator: "\t",
lineSeparator: "\n",
geoLocationSeparator: " ",
transactionTagSeparator: ";",
},
columns: ezbookkeepingDataColumns,
},
}
)
-22
View File
@@ -1,22 +0,0 @@
package converters
import (
"github.com/mayswind/ezbookkeeping/pkg/models"
)
// EzBookKeepingTSVFileConverter defines the structure of TSV file converter
type EzBookKeepingTSVFileConverter struct {
EzBookKeepingPlainFileConverter
}
const tsvSeparator = "\t"
// ToExportedContent returns the exported TSV data
func (e *EzBookKeepingTSVFileConverter) ToExportedContent(uid int64, transactions []*models.Transaction, accountMap map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, allTagIndexes map[int64][]int64) ([]byte, error) {
return e.toExportedContent(uid, tsvSeparator, transactions, accountMap, categoryMap, tagMap, allTagIndexes)
}
// ParseImportedData parses transactions of ezbookkeeping TSV data
func (e *EzBookKeepingTSVFileConverter) ParseImportedData(user *models.User, data []byte, defaultTimezoneOffset int16, accountMap map[string]*models.Account, categoryMap map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) ([]*models.Transaction, []*models.Account, []*models.TransactionCategory, []*models.TransactionTag, error) {
return e.parseImportedData(user, tsvSeparator, data, defaultTimezoneOffset, accountMap, categoryMap, tagMap)
}
@@ -2,21 +2,21 @@ package converters
import "github.com/mayswind/ezbookkeeping/pkg/models"
// ImportTransactionSlice represents the slice data structure of import transaction data
type ImportTransactionSlice []*models.Transaction
// ImportedTransactionSlice represents the slice data structure of import transaction data
type ImportedTransactionSlice []*models.Transaction
// Len returns the count of items
func (s ImportTransactionSlice) Len() int {
func (s ImportedTransactionSlice) Len() int {
return len(s)
}
// Swap swaps two items
func (s ImportTransactionSlice) Swap(i, j int) {
func (s ImportedTransactionSlice) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
// Less reports whether the first item is less than the second one
func (s ImportTransactionSlice) Less(i, j int) bool {
func (s ImportedTransactionSlice) Less(i, j int) bool {
if s[i].Type != s[j].Type && (s[i].Type == models.TRANSACTION_DB_TYPE_MODIFY_BALANCE || s[j].Type == models.TRANSACTION_DB_TYPE_MODIFY_BALANCE) {
if s[i].Type == models.TRANSACTION_DB_TYPE_MODIFY_BALANCE {
return true
@@ -10,7 +10,7 @@ import (
)
func TestImportTransactionSliceLess(t *testing.T) {
var transactionSlice ImportTransactionSlice
var transactionSlice ImportedTransactionSlice
transactionSlice = append(transactionSlice, &models.Transaction{
TransactionId: 1,
Type: models.TRANSACTION_DB_TYPE_EXPENSE,
@@ -4,11 +4,20 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/models"
)
// DataConverter defines the structure of data converter
type DataConverter interface {
// TransactionDataExporter defines the structure of transaction data exporter
type TransactionDataExporter interface {
// ToExportedContent returns the exported data
ToExportedContent(uid int64, transactions []*models.Transaction, accountMap map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, allTagIndexes map[int64][]int64) ([]byte, error)
}
// TransactionDataImporter defines the structure of transaction data importer
type TransactionDataImporter interface {
// ParseImportedData returns the imported data
ParseImportedData(user *models.User, data []byte, defaultTimezoneOffset int16, accountMap map[string]*models.Account, categoryMap map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) ([]*models.Transaction, []*models.Account, []*models.TransactionCategory, []*models.TransactionTag, error)
}
// TransactionDataConverter defines the structure of transaction data converter
type TransactionDataConverter interface {
TransactionDataExporter
TransactionDataImporter
}