diff --git a/go.mod b/go.mod index 97a2d2d1..d10595a6 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( github.com/stretchr/testify v1.10.0 github.com/urfave/cli/v2 v2.27.5 github.com/wk8/go-ordered-map/v2 v2.1.8 + github.com/xuri/excelize/v2 v2.9.0 golang.org/x/crypto v0.33.0 golang.org/x/net v0.34.0 golang.org/x/text v0.22.0 @@ -66,8 +67,11 @@ require ( github.com/minio/md5-simd v1.1.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/richardlehane/mscfb v1.0.4 // indirect + github.com/richardlehane/msoleps v1.0.4 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62 // indirect github.com/rs/xid v1.6.0 // indirect @@ -77,6 +81,8 @@ require ( github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect + github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d // indirect + github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 // indirect golang.org/x/arch v0.13.0 // indirect golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect golang.org/x/sys v0.30.0 // indirect diff --git a/go.sum b/go.sum index dd53a1b3..a8344e45 100644 --- a/go.sum +++ b/go.sum @@ -111,6 +111,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= @@ -122,6 +124,11 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg= github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= +github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM= +github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk= +github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= +github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00= +github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62 h1:pyecQtsPmlkCsMkYhT5iZ+sUXuwee+OvfuJjinEA3ko= @@ -158,6 +165,12 @@ github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/ github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d h1:llb0neMWDQe87IzJLS4Ci7psK/lVsjIS2otl+1WyRyY= +github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= +github.com/xuri/excelize/v2 v2.9.0 h1:1tgOaEq92IOEumR1/JfYS/eR0KHOCsRv/rYXXh6YJQE= +github.com/xuri/excelize/v2 v2.9.0/go.mod h1:uqey4QBZ9gdMeWApPLdhm9x+9o2lq4iVmjiLfBS5hdE= +github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A= +github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.13.0 h1:KCkqVVV1kGg0X87TFysjCJ8MxtZEIU4Ja/yXGeoECdA= golang.org/x/arch v0.13.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= diff --git a/pkg/converters/excel/excel_ooxml_file_imported_data_table.go b/pkg/converters/excel/excel_ooxml_file_imported_data_table.go new file mode 100644 index 00000000..4dc01a62 --- /dev/null +++ b/pkg/converters/excel/excel_ooxml_file_imported_data_table.go @@ -0,0 +1,211 @@ +package excel + +import ( + "bytes" + "fmt" + + "github.com/xuri/excelize/v2" + + "github.com/mayswind/ezbookkeeping/pkg/converters/datatable" + "github.com/mayswind/ezbookkeeping/pkg/errs" +) + +// excelOOXMLSheet defines the structure of excel (Office Open XML) file sheet +type excelOOXMLSheet struct { + sheetName string + allData [][]string +} + +// ExcelOOXMLFileImportedDataTable defines the structure of excel (Office Open XML) file data table +type ExcelOOXMLFileImportedDataTable struct { + sheets []*excelOOXMLSheet + headerLineColumnNames []string +} + +// ExcelOOXMLFileDataRow defines the structure of excel (Office Open XML) file data table row +type ExcelOOXMLFileDataRow struct { + sheet *excelOOXMLSheet + rowData []string + rowIndex int +} + +// ExcelOOXMLFileDataRowIterator defines the structure of excel (Office Open XML) file data table row iterator +type ExcelOOXMLFileDataRowIterator struct { + dataTable *ExcelOOXMLFileImportedDataTable + currentSheetIndex int + currentRowIndexInSheet int +} + +// DataRowCount returns the total count of data row +func (t *ExcelOOXMLFileImportedDataTable) DataRowCount() int { + totalDataRowCount := 0 + + for i := 0; i < len(t.sheets); i++ { + sheet := t.sheets[i] + + if len(sheet.allData) < 1 { + continue + } + + totalDataRowCount += len(sheet.allData) - 1 + } + + return totalDataRowCount +} + +// HeaderColumnNames returns the header column name list +func (t *ExcelOOXMLFileImportedDataTable) HeaderColumnNames() []string { + return t.headerLineColumnNames +} + +// DataRowIterator returns the iterator of data row +func (t *ExcelOOXMLFileImportedDataTable) DataRowIterator() datatable.ImportedDataRowIterator { + return &ExcelOOXMLFileDataRowIterator{ + dataTable: t, + currentSheetIndex: 0, + currentRowIndexInSheet: 0, + } +} + +// ColumnCount returns the total count of column in this data row +func (r *ExcelOOXMLFileDataRow) ColumnCount() int { + return len(r.rowData) +} + +// GetData returns the data in the specified column index +func (r *ExcelOOXMLFileDataRow) GetData(columnIndex int) string { + if columnIndex < 0 || columnIndex >= len(r.rowData) { + return "" + } + + return r.rowData[columnIndex] +} + +// HasNext returns whether the iterator does not reach the end +func (t *ExcelOOXMLFileDataRowIterator) HasNext() bool { + sheets := t.dataTable.sheets + + if t.currentSheetIndex >= len(sheets) { + return false + } + + currentSheet := sheets[t.currentSheetIndex] + + if t.currentRowIndexInSheet+1 < len(currentSheet.allData) { + return true + } + + for i := t.currentSheetIndex + 1; i < len(sheets); i++ { + sheet := sheets[i] + + if len(sheet.allData) <= 1 { + continue + } + + return true + } + + return false +} + +// CurrentRowId returns current index +func (t *ExcelOOXMLFileDataRowIterator) CurrentRowId() string { + return fmt.Sprintf("table#%d-row#%d", t.currentSheetIndex, t.currentRowIndexInSheet) +} + +// Next returns the next imported data row +func (t *ExcelOOXMLFileDataRowIterator) Next() datatable.ImportedDataRow { + sheets := t.dataTable.sheets + currentRowIndexInTable := t.currentRowIndexInSheet + + for i := t.currentSheetIndex; i < len(sheets); i++ { + sheet := sheets[i] + + if currentRowIndexInTable+1 < len(sheet.allData) { + t.currentRowIndexInSheet++ + currentRowIndexInTable = t.currentRowIndexInSheet + break + } + + t.currentSheetIndex++ + t.currentRowIndexInSheet = 0 + currentRowIndexInTable = 0 + } + + if t.currentSheetIndex >= len(sheets) { + return nil + } + + currentSheet := sheets[t.currentSheetIndex] + + if t.currentRowIndexInSheet >= len(currentSheet.allData) { + return nil + } + + return &ExcelOOXMLFileDataRow{ + sheet: currentSheet, + rowData: currentSheet.allData[t.currentRowIndexInSheet], + rowIndex: t.currentRowIndexInSheet, + } +} + +// CreateNewExcelOOXMLFileImportedDataTable returns excel (Office Open XML) data table by file binary data +func CreateNewExcelOOXMLFileImportedDataTable(data []byte) (*ExcelOOXMLFileImportedDataTable, error) { + reader := bytes.NewReader(data) + file, err := excelize.OpenReader(reader) + + defer file.Close() + + if err != nil { + return nil, err + } + + sheetNames := file.GetSheetList() + var headerRowItems []string + var sheets []*excelOOXMLSheet + + for i := 0; i < len(sheetNames); i++ { + sheetName := sheetNames[i] + allData, err := file.GetRows(sheetName) + + if err != nil { + return nil, err + } + + if allData == nil || len(allData) < 1 { + continue + } + + row := allData[0] + + if i == 0 { + for j := 0; j < len(row); j++ { + headerItem := row[j] + + if headerItem == "" { + break + } + + headerRowItems = append(headerRowItems, headerItem) + } + } else { + for j := 0; j < min(len(row), len(headerRowItems)); j++ { + headerItem := row[j] + + if headerItem != headerRowItems[j] { + return nil, errs.ErrFieldsInMultiTableAreDifferent + } + } + } + + sheets = append(sheets, &excelOOXMLSheet{ + sheetName: sheetName, + allData: allData, + }) + } + + return &ExcelOOXMLFileImportedDataTable{ + sheets: sheets, + headerLineColumnNames: headerRowItems, + }, nil +} diff --git a/pkg/converters/excel/excel_ooxml_file_imported_data_table_test.go b/pkg/converters/excel/excel_ooxml_file_imported_data_table_test.go new file mode 100644 index 00000000..d70ae52f --- /dev/null +++ b/pkg/converters/excel/excel_ooxml_file_imported_data_table_test.go @@ -0,0 +1,246 @@ +package excel + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/mayswind/ezbookkeeping/pkg/errs" +) + +func TestExcelOOXMLFileImportedDataTableDataRowCount(t *testing.T) { + testdata, err := os.ReadFile("../../../testdata/simple_excel_file.xlsx") + assert.Nil(t, err) + + datatable, err := CreateNewExcelOOXMLFileImportedDataTable(testdata) + assert.Nil(t, err) + assert.Equal(t, 2, datatable.DataRowCount()) +} + +func TestExcelOOXMLFileImportedDataTableDataRowCount_MultipleSheets(t *testing.T) { + testdata, err := os.ReadFile("../../../testdata/multiple_sheets_excel_file.xlsx") + assert.Nil(t, err) + + datatable, err := CreateNewExcelOOXMLFileImportedDataTable(testdata) + assert.Nil(t, err) + assert.Equal(t, 5, datatable.DataRowCount()) +} + +func TestExcelOOXMLFileImportedDataTableDataRowCount_OnlyHeaderLine(t *testing.T) { + testdata, err := os.ReadFile("../../../testdata/only_one_row_excel_file.xlsx") + assert.Nil(t, err) + + datatable, err := CreateNewExcelOOXMLFileImportedDataTable(testdata) + assert.Nil(t, err) + assert.Equal(t, 0, datatable.DataRowCount()) +} + +func TestExcelOOXMLFileImportedDataTableDataRowCount_EmptyContent(t *testing.T) { + testdata, err := os.ReadFile("../../../testdata/empty_excel_file.xlsx") + assert.Nil(t, err) + + datatable, err := CreateNewExcelOOXMLFileImportedDataTable(testdata) + assert.Nil(t, err) + assert.Equal(t, 0, datatable.DataRowCount()) +} + +func TestExcelOOXMLFileImportedDataTableHeaderColumnNames(t *testing.T) { + testdata, err := os.ReadFile("../../../testdata/simple_excel_file.xlsx") + assert.Nil(t, err) + + datatable, err := CreateNewExcelOOXMLFileImportedDataTable(testdata) + assert.EqualValues(t, []string{"A1", "B1", "C1"}, datatable.HeaderColumnNames()) +} + +func TestExcelOOXMLFileImportedDataTableHeaderColumnNames_EmptyContent(t *testing.T) { + testdata, err := os.ReadFile("../../../testdata/empty_excel_file.xlsx") + assert.Nil(t, err) + + datatable, err := CreateNewExcelOOXMLFileImportedDataTable(testdata) + assert.Nil(t, datatable.HeaderColumnNames()) +} + +func TestExcelOOXMLFileDataRowIterator(t *testing.T) { + testdata, err := os.ReadFile("../../../testdata/simple_excel_file.xlsx") + assert.Nil(t, err) + + datatable, err := CreateNewExcelOOXMLFileImportedDataTable(testdata) + iterator := datatable.DataRowIterator() + assert.True(t, iterator.HasNext()) + + // data row 1 + assert.NotNil(t, iterator.Next()) + assert.True(t, iterator.HasNext()) + + // data row 2 + assert.NotNil(t, iterator.Next()) + assert.False(t, iterator.HasNext()) + + // not existed data row 3 + assert.Nil(t, iterator.Next()) + assert.False(t, iterator.HasNext()) + + // not existed data row 4 + assert.Nil(t, iterator.Next()) + assert.False(t, iterator.HasNext()) +} + +func TestExcelOOXMLFileDataRowIterator_MultipleSheets(t *testing.T) { + testdata, err := os.ReadFile("../../../testdata/multiple_sheets_excel_file.xlsx") + assert.Nil(t, err) + + datatable, err := CreateNewExcelOOXMLFileImportedDataTable(testdata) + iterator := datatable.DataRowIterator() + assert.True(t, iterator.HasNext()) + + // sheet 1 data row 1 + assert.NotNil(t, iterator.Next()) + assert.True(t, iterator.HasNext()) + + // sheet 1 data row 2 + assert.NotNil(t, iterator.Next()) + assert.True(t, iterator.HasNext()) + + // sheet 3 data row 1 + assert.NotNil(t, iterator.Next()) + assert.True(t, iterator.HasNext()) + + // sheet 5 data row 1 + assert.NotNil(t, iterator.Next()) + assert.True(t, iterator.HasNext()) + + // sheet 5 data row 2 + assert.NotNil(t, iterator.Next()) + assert.False(t, iterator.HasNext()) + + // not existed data row + assert.Nil(t, iterator.Next()) + assert.False(t, iterator.HasNext()) + + // not existed data row + assert.Nil(t, iterator.Next()) + assert.False(t, iterator.HasNext()) +} + +func TestExcelOOXMLFileDataRowIterator_OnlyHeaderLine(t *testing.T) { + testdata, err := os.ReadFile("../../../testdata/only_one_row_excel_file.xlsx") + assert.Nil(t, err) + + datatable, err := CreateNewExcelOOXMLFileImportedDataTable(testdata) + iterator := datatable.DataRowIterator() + assert.False(t, iterator.HasNext()) + + // not existed data row 1 + assert.Nil(t, iterator.Next()) + assert.False(t, iterator.HasNext()) + + // not existed data row 2 + assert.Nil(t, iterator.Next()) + assert.False(t, iterator.HasNext()) +} + +func TestExcelOOXMLFileDataRowIterator_EmptyContent(t *testing.T) { + testdata, err := os.ReadFile("../../../testdata/empty_excel_file.xlsx") + assert.Nil(t, err) + + datatable, err := CreateNewExcelOOXMLFileImportedDataTable(testdata) + iterator := datatable.DataRowIterator() + assert.False(t, iterator.HasNext()) + + // not existed data row 1 + assert.Nil(t, iterator.Next()) + assert.False(t, iterator.HasNext()) + + // not existed data row 2 + assert.Nil(t, iterator.Next()) + assert.False(t, iterator.HasNext()) +} + +func TestExcelOOXMLFileDataRowColumnCount(t *testing.T) { + testdata, err := os.ReadFile("../../../testdata/simple_excel_file.xlsx") + assert.Nil(t, err) + + datatable, err := CreateNewExcelOOXMLFileImportedDataTable(testdata) + iterator := datatable.DataRowIterator() + + row1 := iterator.Next() + assert.EqualValues(t, 3, row1.ColumnCount()) + + row2 := iterator.Next() + assert.EqualValues(t, 3, row2.ColumnCount()) +} + +func TestExcelOOXMLFileDataRowGetData(t *testing.T) { + testdata, err := os.ReadFile("../../../testdata/simple_excel_file.xlsx") + assert.Nil(t, err) + + datatable, err := CreateNewExcelOOXMLFileImportedDataTable(testdata) + iterator := datatable.DataRowIterator() + + row1 := iterator.Next() + assert.Equal(t, "A2", row1.GetData(0)) + assert.Equal(t, "B2", row1.GetData(1)) + assert.Equal(t, "C2", row1.GetData(2)) + + row2 := iterator.Next() + assert.Equal(t, "A3", row2.GetData(0)) + assert.Equal(t, "B3", row2.GetData(1)) + assert.Equal(t, "C3", row2.GetData(2)) +} + +func TestExcelOOXMLFileDataRowGetData_GetNotExistedColumnData(t *testing.T) { + testdata, err := os.ReadFile("../../../testdata/simple_excel_file.xlsx") + assert.Nil(t, err) + + datatable, err := CreateNewExcelOOXMLFileImportedDataTable(testdata) + iterator := datatable.DataRowIterator() + + row1 := iterator.Next() + assert.Equal(t, "", row1.GetData(3)) +} + +func TestExcelOOXMLFileDataRowGetData_MultipleSheets(t *testing.T) { + testdata, err := os.ReadFile("../../../testdata/multiple_sheets_excel_file.xlsx") + assert.Nil(t, err) + + datatable, err := CreateNewExcelOOXMLFileImportedDataTable(testdata) + iterator := datatable.DataRowIterator() + + sheet1Row1 := iterator.Next() + assert.Equal(t, "1-A2", sheet1Row1.GetData(0)) + assert.Equal(t, "1-B2", sheet1Row1.GetData(1)) + assert.Equal(t, "1-C2", sheet1Row1.GetData(2)) + + sheet1Row2 := iterator.Next() + assert.Equal(t, "1-A3", sheet1Row2.GetData(0)) + assert.Equal(t, "1-B3", sheet1Row2.GetData(1)) + assert.Equal(t, "1-C3", sheet1Row2.GetData(2)) + + // skip empty sheet2 + + sheet3Row1 := iterator.Next() + assert.Equal(t, "3-A2", sheet3Row1.GetData(0)) + assert.Equal(t, "3-B2", sheet3Row1.GetData(1)) + assert.Equal(t, "", sheet3Row1.GetData(2)) + + // skip no data row sheet4 + + sheet5Row1 := iterator.Next() + assert.Equal(t, "5-A2", sheet5Row1.GetData(0)) + assert.Equal(t, "5-B2", sheet5Row1.GetData(1)) + assert.Equal(t, "5-C2", sheet5Row1.GetData(2)) + + sheet5Row2 := iterator.Next() + assert.Equal(t, "5-A3", sheet5Row2.GetData(0)) + assert.Equal(t, "5-B3", sheet5Row2.GetData(1)) + assert.Equal(t, "5-C3", sheet5Row2.GetData(2)) +} + +func TestCreateNewExcelOOXMLFileImportedDataTable_MultipleSheetsWithDifferentHeaders(t *testing.T) { + testdata, err := os.ReadFile("../../../testdata/multiple_sheets_with_different_header_row_excel_file.xlsx") + assert.Nil(t, err) + + _, err = CreateNewExcelOOXMLFileImportedDataTable(testdata) + assert.EqualError(t, err, errs.ErrFieldsInMultiTableAreDifferent.Message) +} diff --git a/pkg/converters/feidee/feidee_mymoney_elecloud_transaction_data_row_parser.go b/pkg/converters/feidee/feidee_mymoney_elecloud_transaction_data_row_parser.go new file mode 100644 index 00000000..b48759c7 --- /dev/null +++ b/pkg/converters/feidee/feidee_mymoney_elecloud_transaction_data_row_parser.go @@ -0,0 +1,71 @@ +package feidee + +import ( + "strings" + + "github.com/mayswind/ezbookkeeping/pkg/converters/datatable" + "github.com/mayswind/ezbookkeeping/pkg/errs" + "github.com/mayswind/ezbookkeeping/pkg/models" + "github.com/mayswind/ezbookkeeping/pkg/utils" +) + +var FEIDEE_MYMONEY_ELECLOUD_TRANSACTION_TYPE_MODIFY_BALANCE_NAME = "余额变更" +var FEIDEE_MYMONEY_ELECLOUD_TRANSACTION_TYPE_INCOME_NAME = "收入" +var FEIDEE_MYMONEY_ELECLOUD_TRANSACTION_TYPE_EXPENSE_NAME = "支出" + +var feideeMymoneyElecloudTransactionTypeNameMapping = map[string]models.TransactionType{ + FEIDEE_MYMONEY_ELECLOUD_TRANSACTION_TYPE_MODIFY_BALANCE_NAME: models.TRANSACTION_TYPE_MODIFY_BALANCE, + FEIDEE_MYMONEY_ELECLOUD_TRANSACTION_TYPE_INCOME_NAME: models.TRANSACTION_TYPE_INCOME, + FEIDEE_MYMONEY_ELECLOUD_TRANSACTION_TYPE_EXPENSE_NAME: models.TRANSACTION_TYPE_EXPENSE, + "转账": models.TRANSACTION_TYPE_TRANSFER, + "借入": models.TRANSACTION_TYPE_TRANSFER, + "借出": models.TRANSACTION_TYPE_TRANSFER, + "收债": models.TRANSACTION_TYPE_TRANSFER, + "还债": models.TRANSACTION_TYPE_TRANSFER, + "代付": models.TRANSACTION_TYPE_TRANSFER, + "报销": models.TRANSACTION_TYPE_TRANSFER, + "退款": models.TRANSACTION_TYPE_EXPENSE, +} + +// feideeMymoneyElecloudTransactionDataRowParser defines the structure of feidee mymoney (elecloud) transaction data row parser +type feideeMymoneyElecloudTransactionDataRowParser struct { +} + +// GetAddedColumns returns the added columns after converting the data row +func (p *feideeMymoneyElecloudTransactionDataRowParser) GetAddedColumns() []datatable.TransactionDataTableColumn { + return nil +} + +// Parse returns the converted transaction data row +func (p *feideeMymoneyElecloudTransactionDataRowParser) Parse(data map[datatable.TransactionDataTableColumn]string) (rowData map[datatable.TransactionDataTableColumn]string, rowDataValid bool, err error) { + rowData = make(map[datatable.TransactionDataTableColumn]string, len(data)) + + for column, value := range data { + rowData[column] = value + } + + rowData[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = strings.ReplaceAll(rowData[datatable.TRANSACTION_DATA_TABLE_AMOUNT], ",", "") // remove thousand separator + + if rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] == FEIDEE_MYMONEY_ELECLOUD_TRANSACTION_TYPE_MODIFY_BALANCE_NAME { + amount, err := utils.ParseAmount(rowData[datatable.TRANSACTION_DATA_TABLE_AMOUNT]) + + if err != nil { + return nil, false, errs.ErrAmountInvalid + } + + // balance modification transaction in feidee mymoney (elecloud) is not the opening balance transaction, it can be added many times + if amount >= 0 { + rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = FEIDEE_MYMONEY_ELECLOUD_TRANSACTION_TYPE_INCOME_NAME + } else { + rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = FEIDEE_MYMONEY_ELECLOUD_TRANSACTION_TYPE_EXPENSE_NAME + rowData[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = utils.FormatAmount(-amount) + } + } + + return rowData, true, nil +} + +// createFeideeMymoneyElecloudTransactionDataRowParser returns feidee mymoney (elecloud) transaction data row parser +func createFeideeMymoneyElecloudTransactionDataRowParser() datatable.TransactionDataRowParser { + return &feideeMymoneyElecloudTransactionDataRowParser{} +} diff --git a/pkg/converters/feidee/feidee_mymoney_elecloud_transaction_data_xlsx_file_importer.go b/pkg/converters/feidee/feidee_mymoney_elecloud_transaction_data_xlsx_file_importer.go new file mode 100644 index 00000000..0d9be1ec --- /dev/null +++ b/pkg/converters/feidee/feidee_mymoney_elecloud_transaction_data_xlsx_file_importer.go @@ -0,0 +1,46 @@ +package feidee + +import ( + "github.com/mayswind/ezbookkeeping/pkg/converters/converter" + "github.com/mayswind/ezbookkeeping/pkg/converters/datatable" + "github.com/mayswind/ezbookkeeping/pkg/converters/excel" + "github.com/mayswind/ezbookkeeping/pkg/core" + "github.com/mayswind/ezbookkeeping/pkg/models" +) + +var feideeMymoneyElecloudDataColumnNameMapping = map[datatable.TransactionDataTableColumn]string{ + datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME: "日期", + datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE: "交易类型", + datatable.TRANSACTION_DATA_TABLE_CATEGORY: "分类", + datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY: "子分类", + datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME: "账户1", + datatable.TRANSACTION_DATA_TABLE_ACCOUNT_CURRENCY: "账户币种", + datatable.TRANSACTION_DATA_TABLE_AMOUNT: "金额", + datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME: "账户2", + datatable.TRANSACTION_DATA_TABLE_DESCRIPTION: "备注", +} + +// feideeMymoneyElecloudTransactionDataXlsxFileImporter defines the structure of feidee mymoney (elecloud) xlsx importer for transaction data +type feideeMymoneyElecloudTransactionDataXlsxFileImporter struct { + converter.DataTableTransactionDataImporter +} + +// Initialize a feidee mymoney (elecloud) transaction data xlsx file importer singleton instance +var ( + FeideeMymoneyElecloudTransactionDataXlsxFileImporter = &feideeMymoneyElecloudTransactionDataXlsxFileImporter{} +) + +// ParseImportedData returns the imported data by parsing the feidee mymoney (elecloud) transaction xlsx data +func (c *feideeMymoneyElecloudTransactionDataXlsxFileImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, 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) { + dataTable, err := excel.CreateNewExcelOOXMLFileImportedDataTable(data) + + if err != nil { + return nil, nil, nil, nil, nil, nil, err + } + + transactionRowParser := createFeideeMymoneyElecloudTransactionDataRowParser() + transactionDataTable := datatable.CreateNewImportedTransactionDataTableWithRowParser(dataTable, feideeMymoneyElecloudDataColumnNameMapping, transactionRowParser) + dataTableImporter := converter.CreateNewSimpleImporter(feideeMymoneyElecloudTransactionTypeNameMapping) + + return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap) +} diff --git a/pkg/converters/feidee/feidee_mymoney_elecloud_transaction_data_xlsx_file_importer_test.go b/pkg/converters/feidee/feidee_mymoney_elecloud_transaction_data_xlsx_file_importer_test.go new file mode 100644 index 00000000..034f432e --- /dev/null +++ b/pkg/converters/feidee/feidee_mymoney_elecloud_transaction_data_xlsx_file_importer_test.go @@ -0,0 +1,117 @@ +package feidee + +import ( + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/mayswind/ezbookkeeping/pkg/core" + "github.com/mayswind/ezbookkeeping/pkg/models" + "github.com/mayswind/ezbookkeeping/pkg/utils" +) + +func TestFeideeMymoneyElecloudTransactionDataXlsxImporterParseImportedData_MinimumValidData(t *testing.T) { + converter := FeideeMymoneyElecloudTransactionDataXlsxFileImporter + context := core.NewNullContext() + + user := &models.User{ + Uid: 1234567890, + DefaultCurrency: "USD", + } + + testdata, err := os.ReadFile("../../../testdata/feidee_mymoney_elecloud_test_file.xlsx") + assert.Nil(t, err) + + allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := converter.ParseImportedData(context, user, testdata, 0, nil, nil, nil, nil, nil) + assert.Nil(t, err) + + assert.Equal(t, 7, len(allNewTransactions)) + assert.Equal(t, 2, len(allNewAccounts)) + assert.Equal(t, 3, len(allNewSubExpenseCategories)) + assert.Equal(t, 3, len(allNewSubIncomeCategories)) + assert.Equal(t, 1, len(allNewSubTransferCategories)) + assert.Equal(t, 0, len(allNewTags)) + + assert.Equal(t, int64(1234567890), allNewTransactions[0].Uid) + assert.Equal(t, models.TRANSACTION_DB_TYPE_INCOME, allNewTransactions[0].Type) + assert.Equal(t, "2024-09-01 00:00:00", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime), time.UTC)) + assert.Equal(t, int64(12345), allNewTransactions[0].Amount) + assert.Equal(t, "Test Account", allNewTransactions[0].OriginalSourceAccountName) + assert.Equal(t, "", allNewTransactions[0].OriginalCategoryName) + + assert.Equal(t, int64(1234567890), allNewTransactions[1].Uid) + assert.Equal(t, models.TRANSACTION_DB_TYPE_EXPENSE, allNewTransactions[1].Type) + assert.Equal(t, "2024-09-01 01:00:00", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[1].TransactionTime), time.UTC)) + assert.Equal(t, int64(12), allNewTransactions[1].Amount) + assert.Equal(t, "Test Account2", allNewTransactions[1].OriginalSourceAccountName) + assert.Equal(t, "", allNewTransactions[1].OriginalCategoryName) + + assert.Equal(t, int64(1234567890), allNewTransactions[2].Uid) + assert.Equal(t, models.TRANSACTION_DB_TYPE_INCOME, allNewTransactions[2].Type) + assert.Equal(t, "2024-09-01 01:23:45", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[2].TransactionTime), time.UTC)) + assert.Equal(t, int64(12), allNewTransactions[2].Amount) + assert.Equal(t, "Test Account", allNewTransactions[2].OriginalSourceAccountName) + assert.Equal(t, "Test Category", allNewTransactions[2].OriginalCategoryName) + + assert.Equal(t, int64(1234567890), allNewTransactions[3].Uid) + assert.Equal(t, models.TRANSACTION_DB_TYPE_EXPENSE, allNewTransactions[3].Type) + assert.Equal(t, "2024-09-01 12:34:56", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[3].TransactionTime), time.UTC)) + assert.Equal(t, int64(100), allNewTransactions[3].Amount) + assert.Equal(t, "Test Account2", allNewTransactions[3].OriginalSourceAccountName) + assert.Equal(t, "Test Category2", allNewTransactions[3].OriginalCategoryName) + + assert.Equal(t, int64(1234567890), allNewTransactions[4].Uid) + assert.Equal(t, models.TRANSACTION_DB_TYPE_TRANSFER_OUT, allNewTransactions[4].Type) + assert.Equal(t, "2024-09-01 23:59:59", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[4].TransactionTime), time.UTC)) + assert.Equal(t, int64(5), allNewTransactions[4].Amount) + assert.Equal(t, "Test Comment5", allNewTransactions[4].Comment) + assert.Equal(t, "Test Account", allNewTransactions[4].OriginalSourceAccountName) + assert.Equal(t, "Test Account2", allNewTransactions[4].OriginalDestinationAccountName) + assert.Equal(t, "", allNewTransactions[4].OriginalCategoryName) + + assert.Equal(t, int64(1234567890), allNewTransactions[5].Uid) + assert.Equal(t, models.TRANSACTION_DB_TYPE_INCOME, allNewTransactions[5].Type) + assert.Equal(t, "2024-09-10 00:00:00", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[5].TransactionTime), time.UTC)) + assert.Equal(t, int64(-654300), allNewTransactions[5].Amount) + assert.Equal(t, "Test Account2", allNewTransactions[5].OriginalSourceAccountName) + assert.Equal(t, "Test Category5", allNewTransactions[5].OriginalCategoryName) + + assert.Equal(t, int64(1234567890), allNewTransactions[6].Uid) + assert.Equal(t, models.TRANSACTION_DB_TYPE_EXPENSE, allNewTransactions[6].Type) + assert.Equal(t, "2024-09-11 05:06:00", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[6].TransactionTime), time.UTC)) + assert.Equal(t, int64(-112340), allNewTransactions[6].Amount) + assert.Equal(t, "Foo#\\r\\nBar", allNewTransactions[6].Comment) + assert.Equal(t, "Test Account", allNewTransactions[6].OriginalSourceAccountName) + assert.Equal(t, "Test Category4", allNewTransactions[6].OriginalCategoryName) + + assert.Equal(t, int64(1234567890), allNewAccounts[0].Uid) + assert.Equal(t, "Test Account2", allNewAccounts[0].Name) + assert.Equal(t, "CNY", allNewAccounts[0].Currency) + + assert.Equal(t, int64(1234567890), allNewAccounts[1].Uid) + assert.Equal(t, "Test Account", allNewAccounts[1].Name) + assert.Equal(t, "CNY", allNewAccounts[1].Currency) + + assert.Equal(t, int64(1234567890), allNewSubExpenseCategories[0].Uid) + assert.Equal(t, "", allNewSubExpenseCategories[0].Name) + + assert.Equal(t, int64(1234567890), allNewSubExpenseCategories[1].Uid) + assert.Equal(t, "Test Category4", allNewSubExpenseCategories[1].Name) + + assert.Equal(t, int64(1234567890), allNewSubExpenseCategories[2].Uid) + assert.Equal(t, "Test Category2", allNewSubExpenseCategories[2].Name) + + assert.Equal(t, int64(1234567890), allNewSubIncomeCategories[0].Uid) + assert.Equal(t, "", allNewSubIncomeCategories[0].Name) + + assert.Equal(t, int64(1234567890), allNewSubIncomeCategories[1].Uid) + assert.Equal(t, "Test Category5", allNewSubIncomeCategories[1].Name) + + assert.Equal(t, int64(1234567890), allNewSubIncomeCategories[2].Uid) + assert.Equal(t, "Test Category", allNewSubIncomeCategories[2].Name) + + assert.Equal(t, int64(1234567890), allNewSubTransferCategories[0].Uid) + assert.Equal(t, "", allNewSubTransferCategories[0].Name) +} diff --git a/pkg/converters/transaction_data_converters.go b/pkg/converters/transaction_data_converters.go index 1ba60b8c..3c14d6b7 100644 --- a/pkg/converters/transaction_data_converters.go +++ b/pkg/converters/transaction_data_converters.go @@ -54,6 +54,8 @@ func GetTransactionDataImporter(fileType string) (converter.TransactionDataImpor return feidee.FeideeMymoneyAppTransactionDataCsvFileImporter, nil } else if fileType == "feidee_mymoney_xls" { return feidee.FeideeMymoneyWebTransactionDataXlsFileImporter, nil + } else if fileType == "feidee_mymoney_elecloud_xlsx" { + return feidee.FeideeMymoneyElecloudTransactionDataXlsxFileImporter, nil } else if fileType == "alipay_app_csv" { return alipay.AlipayAppTransactionDataCsvFileImporter, nil } else if fileType == "alipay_web_csv" { diff --git a/src/consts/file.ts b/src/consts/file.ts index c21c6266..9755dcee 100644 --- a/src/consts/file.ts +++ b/src/consts/file.ts @@ -189,6 +189,15 @@ export const SUPPORTED_IMPORT_FILE_TYPES: ImportFileType[] = [ anchor: '如何获取随手记web版数据导出文件' } }, + { + type: 'feidee_mymoney_elecloud_xlsx', + name: 'Feidee MyMoney (Elecloud) Data Export File', + extensions: '.xlsx', + document: { + supportMultiLanguages: 'zh-Hans', + anchor: '如何获取随手记神象云账本数据导出文件' + } + }, { type: 'alipay_app_csv', name: 'Alipay (App) Transaction Flow File', diff --git a/src/locales/de.json b/src/locales/de.json index b5b83bc5..95a0ea85 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -1653,6 +1653,7 @@ "Firefly III Data Export File": "Firefly III-Datenexportdatei", "Feidee MyMoney (App) Data Export File": "Feidee MyMoney (App)-Datenexportdatei", "Feidee MyMoney (Web) Data Export File": "Feidee MyMoney (Web)-Datenexportdatei", + "Feidee MyMoney (Elecloud) Data Export File": "Feidee MyMoney (Elecloud) Data Export File", "Alipay (App) Transaction Flow File": "Alipay (App)-Transaktionsflussdatei", "Alipay (Web) Transaction Flow File": "Alipay (Web)-Transaktionsflussdatei", "WeChat Pay Billing File": "WeChat Pay-Abrechnungsdatei", diff --git a/src/locales/en.json b/src/locales/en.json index f88956d5..96c2067f 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1653,6 +1653,7 @@ "Firefly III Data Export File": "Firefly III Data Export File", "Feidee MyMoney (App) Data Export File": "Feidee MyMoney (App) Data Export File", "Feidee MyMoney (Web) Data Export File": "Feidee MyMoney (Web) Data Export File", + "Feidee MyMoney (Elecloud) Data Export File": "Feidee MyMoney (Elecloud) Data Export File", "Alipay (App) Transaction Flow File": "Alipay (App) Transaction Flow File", "Alipay (Web) Transaction Flow File": "Alipay (Web) Transaction Flow File", "WeChat Pay Billing File": "WeChat Pay Billing File", diff --git a/src/locales/es.json b/src/locales/es.json index edf54dc3..20a9664b 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -1653,6 +1653,7 @@ "Firefly III Data Export File": "Archivo de exportación de datos de Firefly III", "Feidee MyMoney (App) Data Export File": "Archivo de exportación de datos Feidee MyMoney (aplicación)", "Feidee MyMoney (Web) Data Export File": "Archivo de exportación de datos Feidee MyMoney (Web)", + "Feidee MyMoney (Elecloud) Data Export File": "Feidee MyMoney (Elecloud) Data Export File", "Alipay (App) Transaction Flow File": "Archivo de flujo de transacciones de Alipay (aplicación)", "Alipay (Web) Transaction Flow File": "Archivo de flujo de transacciones de Alipay (web)", "WeChat Pay Billing File": "Archivo de facturación de pago de WeChat", diff --git a/src/locales/ja.json b/src/locales/ja.json index 2942d52b..489d0a37 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -1653,6 +1653,7 @@ "Firefly III Data Export File": "Firefly III データエクスポートファイル", "Feidee MyMoney (App) Data Export File": "Feidee MyMoney (App) データベースファイル", "Feidee MyMoney (Web) Data Export File": "Feidee MyMoney (Web) データベースファイル", + "Feidee MyMoney (Elecloud) Data Export File": "Feidee MyMoney (Elecloud) Data Export File", "Alipay (App) Transaction Flow File": "Alipay (App) トランザクションフローファイル", "Alipay (Web) Transaction Flow File": "Alipay (Web) トランザクションフローファイル", "WeChat Pay Billing File": "WeChat Pay請求ファイル", diff --git a/src/locales/ru.json b/src/locales/ru.json index 4c0cf06e..b028a537 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -1653,6 +1653,7 @@ "Firefly III Data Export File": "Файл экспорта данных Firefly III", "Feidee MyMoney (App) Data Export File": "Файл экспорта данных Feidee MyMoney (приложение)", "Feidee MyMoney (Web) Data Export File": "Файл экспорта данных Feidee MyMoney (веб)", + "Feidee MyMoney (Elecloud) Data Export File": "Feidee MyMoney (Elecloud) Data Export File", "Alipay (App) Transaction Flow File": "Файл потока транзакций Alipay (приложение)", "Alipay (Web) Transaction Flow File": "Файл потока транзакций Alipay (веб)", "WeChat Pay Billing File": "Файл выставления счетов WeChat Pay", diff --git a/src/locales/vi.json b/src/locales/vi.json index 71b4a4e1..059259df 100644 --- a/src/locales/vi.json +++ b/src/locales/vi.json @@ -1653,6 +1653,7 @@ "Firefly III Data Export File": "Tệp xuất dữ liệu Firefly III", "Feidee MyMoney (App) Data Export File": "Tệp xuất dữ liệu Feidee MyMoney (Ứng dụng)", "Feidee MyMoney (Web) Data Export File": "Tệp xuất dữ liệu Feidee MyMoney (Web)", + "Feidee MyMoney (Elecloud) Data Export File": "Feidee MyMoney (Elecloud) Data Export File", "Alipay (App) Transaction Flow File": "Tệp luồng giao dịch Alipay (Ứng dụng)", "Alipay (Web) Transaction Flow File": "Tệp luồng giao dịch Alipay (Web)", "WeChat Pay Billing File": "Tệp thanh toán WeChat Pay", diff --git a/src/locales/zh_Hans.json b/src/locales/zh_Hans.json index f2b07914..4d7d0d3b 100644 --- a/src/locales/zh_Hans.json +++ b/src/locales/zh_Hans.json @@ -1653,6 +1653,7 @@ "Firefly III Data Export File": "Firefly III 数据导出文件", "Feidee MyMoney (App) Data Export File": "随手记 (App) 数据导出文件", "Feidee MyMoney (Web) Data Export File": "随手记 (Web版) 数据导出文件", + "Feidee MyMoney (Elecloud) Data Export File": "随手记 (神象云账本) 数据导出文件", "Alipay (App) Transaction Flow File": "支付宝 (App) 交易流水文件", "Alipay (Web) Transaction Flow File": "支付宝 (网页版) 交易流水文件", "WeChat Pay Billing File": "微信支付账单文件", diff --git a/testdata/empty_excel_file.xlsx b/testdata/empty_excel_file.xlsx new file mode 100644 index 00000000..bc5a9bfc Binary files /dev/null and b/testdata/empty_excel_file.xlsx differ diff --git a/testdata/feidee_mymoney_elecloud_test_file.xlsx b/testdata/feidee_mymoney_elecloud_test_file.xlsx new file mode 100644 index 00000000..a81c4a16 Binary files /dev/null and b/testdata/feidee_mymoney_elecloud_test_file.xlsx differ diff --git a/testdata/multiple_sheets_excel_file.xlsx b/testdata/multiple_sheets_excel_file.xlsx new file mode 100644 index 00000000..1f306314 Binary files /dev/null and b/testdata/multiple_sheets_excel_file.xlsx differ diff --git a/testdata/multiple_sheets_with_different_header_row_excel_file.xlsx b/testdata/multiple_sheets_with_different_header_row_excel_file.xlsx new file mode 100644 index 00000000..e7878f6e Binary files /dev/null and b/testdata/multiple_sheets_with_different_header_row_excel_file.xlsx differ diff --git a/testdata/only_one_row_excel_file.xlsx b/testdata/only_one_row_excel_file.xlsx new file mode 100644 index 00000000..28b04e77 Binary files /dev/null and b/testdata/only_one_row_excel_file.xlsx differ diff --git a/testdata/simple_excel_file.xlsx b/testdata/simple_excel_file.xlsx new file mode 100644 index 00000000..4a3bbfea Binary files /dev/null and b/testdata/simple_excel_file.xlsx differ diff --git a/third-party-dependencies.json b/third-party-dependencies.json index dd05a5c7..d4848b84 100644 --- a/third-party-dependencies.json +++ b/third-party-dependencies.json @@ -123,6 +123,12 @@ "url": "https://github.com/boombuler/barcode", "licenseUrl": "https://github.com/boombuler/barcode/blob/v1.0.2/LICENSE" }, + { + "name": "Excelize", + "copyright": "Copyright (c) 2016-2024 The excelize Authors. Copyright (c) 2011-2017 Geoffrey J. Teale", + "url": "https://github.com/qax-os/excelize", + "licenseUrl": "https://github.com/qax-os/excelize/blob/v2.9.0/LICENSE" + }, { "name": "xls", "url": "https://github.com/extrame/xls",