From 5fe30eb4565980503acbabd1dad47ac3ec26d5fd Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 31 Jul 2023 00:08:10 +0800 Subject: [PATCH] This closes #1590, add the Japanese calendar number format support - The `GetFormControl` now support to get text, rich-text and font format of the form controls - Update the unit tests and the documentation --- cell.go | 15 ++++- cell_test.go | 8 +++ go.mod | 2 +- go.sum | 4 +- numfmt.go | 179 +++++++++++++++++++++++++++++-------------------- numfmt_test.go | 12 ++++ vml.go | 114 ++++++++++++++++++++++++++++--- vmlDrawing.go | 51 ++++++++++++-- vml_test.go | 53 +++++++++++++-- xmlStyles.go | 5 +- 10 files changed, 346 insertions(+), 97 deletions(-) diff --git a/cell.go b/cell.go index 2de8e85..5842aee 100644 --- a/cell.go +++ b/cell.go @@ -1368,15 +1368,24 @@ func (f *File) formattedValue(c *xlsxC, raw bool, cellType CellType) (string, er if fmtCode, ok := f.getBuiltInNumFmtCode(numFmtID); ok { return f.applyBuiltInNumFmt(c, fmtCode, numFmtID, date1904, cellType), err } + return f.applyNumFmt(c, styleSheet, numFmtID, date1904, cellType), err +} + +// applyNumFmt provides a function to returns formatted cell value with custom +// number format code. +func (f *File) applyNumFmt(c *xlsxC, styleSheet *xlsxStyleSheet, numFmtID int, date1904 bool, cellType CellType) string { if styleSheet.NumFmts == nil { - return c.V, err + return c.V } for _, xlsxFmt := range styleSheet.NumFmts.NumFmt { if xlsxFmt.NumFmtID == numFmtID { - return format(c.V, xlsxFmt.FormatCode, date1904, cellType, f.options), err + if xlsxFmt.FormatCode16 != "" { + return format(c.V, xlsxFmt.FormatCode16, date1904, cellType, f.options) + } + return format(c.V, xlsxFmt.FormatCode, date1904, cellType, f.options) } } - return c.V, err + return c.V } // prepareCellStyle provides a function to prepare style index of cell in diff --git a/cell_test.go b/cell_test.go index 7b53d86..1770e91 100644 --- a/cell_test.go +++ b/cell_test.go @@ -927,6 +927,14 @@ func TestFormattedValueNilWorkbookPr(t *testing.T) { assert.Equal(t, "43528", result) } +func TestApplyNumFmt(t *testing.T) { + f := NewFile() + assert.Equal(t, "\u4EE4\u548C\u5143年9月1日", f.applyNumFmt(&xlsxC{V: "43709"}, + &xlsxStyleSheet{NumFmts: &xlsxNumFmts{NumFmt: []*xlsxNumFmt{ + {NumFmtID: 164, FormatCode16: "[$-ja-JP-x-gannen,80]ggge\"年\"m\"月\"d\"日\";@"}, + }}}, 164, false, CellTypeNumber)) +} + func TestSharedStringsError(t *testing.T) { f, err := OpenFile(filepath.Join("test", "Book1.xlsx"), Options{UnzipXMLSizeLimit: 128}) assert.NoError(t, err) diff --git a/go.mod b/go.mod index 8a6347e..9395c13 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/richardlehane/mscfb v1.0.4 github.com/stretchr/testify v1.8.0 github.com/xuri/efp v0.0.0-20230422071738-01f4e37c47e9 - github.com/xuri/nfp v0.0.0-20230723160540-a7d120392641 + github.com/xuri/nfp v0.0.0-20230730012209-aee513b45ff4 golang.org/x/crypto v0.11.0 golang.org/x/image v0.5.0 golang.org/x/net v0.12.0 diff --git a/go.sum b/go.sum index 7bc9eb6..74da2d5 100644 --- a/go.sum +++ b/go.sum @@ -17,8 +17,8 @@ github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PK github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/xuri/efp v0.0.0-20230422071738-01f4e37c47e9 h1:ge5g8vsTQclA5lXDi+PuiAFw5GMIlMHOB/5e1hsf96E= github.com/xuri/efp v0.0.0-20230422071738-01f4e37c47e9/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= -github.com/xuri/nfp v0.0.0-20230723160540-a7d120392641 h1:1SQuQwUorWlROdGAbsAJrMInj02yCUsYFNi/MzTJ6cA= -github.com/xuri/nfp v0.0.0-20230723160540-a7d120392641/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= +github.com/xuri/nfp v0.0.0-20230730012209-aee513b45ff4 h1:7TXNzvlvE0E/oLDazWm2Xip72G9Su+jRzvziSxwO6Ww= +github.com/xuri/nfp v0.0.0-20230730012209-aee513b45ff4/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= diff --git a/numfmt.go b/numfmt.go index 0acbfcf..59a7154 100644 --- a/numfmt.go +++ b/numfmt.go @@ -26,6 +26,7 @@ import ( type languageInfo struct { apFmt string tags []string + useGannen bool localMonth func(t time.Time, abbr int) string } @@ -768,71 +769,85 @@ var ( "vai-Vaii-LR", "vai-Latn-LR", "vai-Latn", "vo", "vo-001", "vun", "vun-TZ", "wae", "wae-CH", "wal", "wae-ET", "yav", "yav-CM", "yo-BJ", "dje", "dje-NE", }, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, - "C09": {tags: []string{"en-AU"}, localMonth: localMonthsNameEnglish, apFmt: strings.ToLower(nfp.AmPm[0])}, - "2829": {tags: []string{"en-BZ"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, - "1009": {tags: []string{"en-CA"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, - "2409": {tags: []string{"en-029"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, - "3C09": {tags: []string{"en-HK"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, - "4009": {tags: []string{"en-IN"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, - "1809": {tags: []string{"en-IE"}, localMonth: localMonthsNameEnglish, apFmt: strings.ToLower(nfp.AmPm[0])}, - "2009": {tags: []string{"en-JM"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, - "4409": {tags: []string{"en-MY"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, - "1409": {tags: []string{"en-NZ"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, - "3409": {tags: []string{"en-PH"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, - "4809": {tags: []string{"en-SG"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, - "1C09": {tags: []string{"en-ZA"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, - "2C09": {tags: []string{"en-TT"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, - "4C09": {tags: []string{"en-AE"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, - "809": {tags: []string{"en-GB"}, localMonth: localMonthsNameEnglish, apFmt: strings.ToLower(nfp.AmPm[0])}, - "409": {tags: []string{"en-US"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, - "3009": {tags: []string{"en-ZW"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, - "C": {tags: []string{"fr"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0]}, - "7": {tags: []string{"de"}, localMonth: localMonthsNameGerman, apFmt: nfp.AmPm[0]}, - "C07": {tags: []string{"de-AT"}, localMonth: localMonthsNameAustria, apFmt: nfp.AmPm[0]}, - "407": {tags: []string{"de-DE"}, localMonth: localMonthsNameGerman, apFmt: nfp.AmPm[0]}, - "3C": {tags: []string{"ga"}, localMonth: localMonthsNameIrish, apFmt: apFmtIrish}, - "83C": {tags: []string{"ga-IE"}, localMonth: localMonthsNameIrish, apFmt: apFmtIrish}, - "10": {tags: []string{"it"}, localMonth: localMonthsNameItalian, apFmt: nfp.AmPm[0]}, - "11": {tags: []string{"ja"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese}, - "411": {tags: []string{"ja-JP"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese}, - "12": {tags: []string{"ko"}, localMonth: localMonthsNameKorean, apFmt: apFmtKorean}, - "412": {tags: []string{"ko-KR"}, localMonth: localMonthsNameKorean, apFmt: apFmtKorean}, - "7C50": {tags: []string{"mn-Mong"}, localMonth: localMonthsNameTraditionalMongolian, apFmt: nfp.AmPm[0]}, - "850": {tags: []string{"mn-Mong-CN"}, localMonth: localMonthsNameTraditionalMongolian, apFmt: nfp.AmPm[0]}, - "C50": {tags: []string{"mn-Mong-MN"}, localMonth: localMonthsNameTraditionalMongolian, apFmt: nfp.AmPm[0]}, - "19": {tags: []string{"ru"}, localMonth: localMonthsNameRussian, apFmt: nfp.AmPm[0]}, - "819": {tags: []string{"ru-MD"}, localMonth: localMonthsNameRussian, apFmt: nfp.AmPm[0]}, - "419": {tags: []string{"ru-RU"}, localMonth: localMonthsNameRussian, apFmt: nfp.AmPm[0]}, - "A": {tags: []string{"es"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, - "2C0A": {tags: []string{"es-AR"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, - "200A": {tags: []string{"es-VE"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, - "400A": {tags: []string{"es-BO"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, - "340A": {tags: []string{"es-CL"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, - "240A": {tags: []string{"es-CO"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, - "140A": {tags: []string{"es-CR"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, - "5C0A": {tags: []string{"es-CU"}, localMonth: localMonthsNameSpanish, apFmt: apFmtCuba}, - "1C0A": {tags: []string{"es-DO"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, - "300A": {tags: []string{"es-EC"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, - "440A": {tags: []string{"es-SV"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, - "1E": {tags: []string{"th"}, localMonth: localMonthsNameThai, apFmt: nfp.AmPm[0]}, - "41E": {tags: []string{"th-TH"}, localMonth: localMonthsNameThai, apFmt: nfp.AmPm[0]}, - "51": {tags: []string{"bo"}, localMonth: localMonthsNameTibetan, apFmt: apFmtTibetan}, - "451": {tags: []string{"bo-CN"}, localMonth: localMonthsNameTibetan, apFmt: apFmtTibetan}, - "1F": {tags: []string{"tr"}, localMonth: localMonthsNameTurkish, apFmt: apFmtTurkish}, - "41F": {tags: []string{"tr-TR"}, localMonth: localMonthsNameTurkish, apFmt: apFmtTurkish}, - "52": {tags: []string{"cy"}, localMonth: localMonthsNameWelsh, apFmt: apFmtWelsh}, - "452": {tags: []string{"cy-GB"}, localMonth: localMonthsNameWelsh, apFmt: apFmtWelsh}, - "2A": {tags: []string{"vi"}, localMonth: localMonthsNameVietnamese, apFmt: apFmtVietnamese}, - "42A": {tags: []string{"vi-VN"}, localMonth: localMonthsNameVietnamese, apFmt: apFmtVietnamese}, - "88": {tags: []string{"wo"}, localMonth: localMonthsNameWolof, apFmt: apFmtWolof}, - "488": {tags: []string{"wo-SN"}, localMonth: localMonthsNameWolof, apFmt: apFmtWolof}, - "34": {tags: []string{"xh"}, localMonth: localMonthsNameXhosa, apFmt: nfp.AmPm[0]}, - "434": {tags: []string{"xh-ZA"}, localMonth: localMonthsNameXhosa, apFmt: nfp.AmPm[0]}, - "78": {tags: []string{"ii"}, localMonth: localMonthsNameYi, apFmt: apFmtYi}, - "478": {tags: []string{"ii-CN"}, localMonth: localMonthsNameYi, apFmt: apFmtYi}, - "35": {tags: []string{"zu"}, localMonth: localMonthsNameZulu, apFmt: nfp.AmPm[0]}, - "435": {tags: []string{"zu-ZA"}, localMonth: localMonthsNameZulu, apFmt: nfp.AmPm[0]}, + "C09": {tags: []string{"en-AU"}, localMonth: localMonthsNameEnglish, apFmt: strings.ToLower(nfp.AmPm[0])}, + "2829": {tags: []string{"en-BZ"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, + "1009": {tags: []string{"en-CA"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, + "2409": {tags: []string{"en-029"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, + "3C09": {tags: []string{"en-HK"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, + "4009": {tags: []string{"en-IN"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, + "1809": {tags: []string{"en-IE"}, localMonth: localMonthsNameEnglish, apFmt: strings.ToLower(nfp.AmPm[0])}, + "2009": {tags: []string{"en-JM"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, + "4409": {tags: []string{"en-MY"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, + "1409": {tags: []string{"en-NZ"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, + "3409": {tags: []string{"en-PH"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, + "4809": {tags: []string{"en-SG"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, + "1C09": {tags: []string{"en-ZA"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, + "2C09": {tags: []string{"en-TT"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, + "4C09": {tags: []string{"en-AE"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, + "809": {tags: []string{"en-GB"}, localMonth: localMonthsNameEnglish, apFmt: strings.ToLower(nfp.AmPm[0])}, + "409": {tags: []string{"en-US"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, + "3009": {tags: []string{"en-ZW"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, + "C": {tags: []string{"fr"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0]}, + "7": {tags: []string{"de"}, localMonth: localMonthsNameGerman, apFmt: nfp.AmPm[0]}, + "C07": {tags: []string{"de-AT"}, localMonth: localMonthsNameAustria, apFmt: nfp.AmPm[0]}, + "407": {tags: []string{"de-DE"}, localMonth: localMonthsNameGerman, apFmt: nfp.AmPm[0]}, + "3C": {tags: []string{"ga"}, localMonth: localMonthsNameIrish, apFmt: apFmtIrish}, + "83C": {tags: []string{"ga-IE"}, localMonth: localMonthsNameIrish, apFmt: apFmtIrish}, + "10": {tags: []string{"it"}, localMonth: localMonthsNameItalian, apFmt: nfp.AmPm[0]}, + "11": {tags: []string{"ja"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese}, + "411": {tags: []string{"ja-JP"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese}, + "800411": {tags: []string{"ja-JP"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese}, + "JP-X-GANNEN,80": {tags: []string{"ja-JP"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese, useGannen: true}, + "12": {tags: []string{"ko"}, localMonth: localMonthsNameKorean, apFmt: apFmtKorean}, + "412": {tags: []string{"ko-KR"}, localMonth: localMonthsNameKorean, apFmt: apFmtKorean}, + "7C50": {tags: []string{"mn-Mong"}, localMonth: localMonthsNameTraditionalMongolian, apFmt: nfp.AmPm[0]}, + "850": {tags: []string{"mn-Mong-CN"}, localMonth: localMonthsNameTraditionalMongolian, apFmt: nfp.AmPm[0]}, + "C50": {tags: []string{"mn-Mong-MN"}, localMonth: localMonthsNameTraditionalMongolian, apFmt: nfp.AmPm[0]}, + "19": {tags: []string{"ru"}, localMonth: localMonthsNameRussian, apFmt: nfp.AmPm[0]}, + "819": {tags: []string{"ru-MD"}, localMonth: localMonthsNameRussian, apFmt: nfp.AmPm[0]}, + "419": {tags: []string{"ru-RU"}, localMonth: localMonthsNameRussian, apFmt: nfp.AmPm[0]}, + "A": {tags: []string{"es"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, + "2C0A": {tags: []string{"es-AR"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, + "200A": {tags: []string{"es-VE"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, + "400A": {tags: []string{"es-BO"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, + "340A": {tags: []string{"es-CL"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, + "240A": {tags: []string{"es-CO"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, + "140A": {tags: []string{"es-CR"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, + "5C0A": {tags: []string{"es-CU"}, localMonth: localMonthsNameSpanish, apFmt: apFmtCuba}, + "1C0A": {tags: []string{"es-DO"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, + "300A": {tags: []string{"es-EC"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, + "440A": {tags: []string{"es-SV"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, + "1E": {tags: []string{"th"}, localMonth: localMonthsNameThai, apFmt: nfp.AmPm[0]}, + "41E": {tags: []string{"th-TH"}, localMonth: localMonthsNameThai, apFmt: nfp.AmPm[0]}, + "51": {tags: []string{"bo"}, localMonth: localMonthsNameTibetan, apFmt: apFmtTibetan}, + "451": {tags: []string{"bo-CN"}, localMonth: localMonthsNameTibetan, apFmt: apFmtTibetan}, + "1F": {tags: []string{"tr"}, localMonth: localMonthsNameTurkish, apFmt: apFmtTurkish}, + "41F": {tags: []string{"tr-TR"}, localMonth: localMonthsNameTurkish, apFmt: apFmtTurkish}, + "52": {tags: []string{"cy"}, localMonth: localMonthsNameWelsh, apFmt: apFmtWelsh}, + "452": {tags: []string{"cy-GB"}, localMonth: localMonthsNameWelsh, apFmt: apFmtWelsh}, + "2A": {tags: []string{"vi"}, localMonth: localMonthsNameVietnamese, apFmt: apFmtVietnamese}, + "42A": {tags: []string{"vi-VN"}, localMonth: localMonthsNameVietnamese, apFmt: apFmtVietnamese}, + "88": {tags: []string{"wo"}, localMonth: localMonthsNameWolof, apFmt: apFmtWolof}, + "488": {tags: []string{"wo-SN"}, localMonth: localMonthsNameWolof, apFmt: apFmtWolof}, + "34": {tags: []string{"xh"}, localMonth: localMonthsNameXhosa, apFmt: nfp.AmPm[0]}, + "434": {tags: []string{"xh-ZA"}, localMonth: localMonthsNameXhosa, apFmt: nfp.AmPm[0]}, + "78": {tags: []string{"ii"}, localMonth: localMonthsNameYi, apFmt: apFmtYi}, + "478": {tags: []string{"ii-CN"}, localMonth: localMonthsNameYi, apFmt: apFmtYi}, + "35": {tags: []string{"zu"}, localMonth: localMonthsNameZulu, apFmt: nfp.AmPm[0]}, + "435": {tags: []string{"zu-ZA"}, localMonth: localMonthsNameZulu, apFmt: nfp.AmPm[0]}, } + // japaneseEraYears list the Japanese era name periods. + japaneseEraYears = []time.Time{ + time.Date(1868, time.August, 8, 0, 0, 0, 0, time.UTC), + time.Date(1912, time.June, 30, 0, 0, 0, 0, time.UTC), + time.Date(1926, time.November, 25, 0, 0, 0, 0, time.UTC), + time.Date(1989, time.January, 8, 0, 0, 0, 0, time.UTC), + time.Date(2019, time.April, 1, 0, 0, 0, 0, time.UTC), + } + // japaneseEraNames list the Japanese era name for the Japanese emperor reign calendar. + japaneseEraNames = []string{"\u660E\u6CBB", "\u5927\u6B63", "\u662D\u548C", "\u5E73\u6210", "\u4EE4\u548C"} + // japaneseEraYear list the Japanese era name symbols. + japaneseEraSymbols = []string{"M", "T", "S", "H", "R"} // monthNamesBangla list the month names in the Bangla. monthNamesBangla = []string{ "\u099C\u09BE\u09A8\u09C1\u09AF\u09BC\u09BE\u09B0\u09C0", @@ -1329,7 +1344,9 @@ func (nf *numberFormat) dateTimeHandler() string { if changeNumFmtCode, err := nf.currencyLanguageHandler(token); err != nil || changeNumFmtCode { return nf.value } - nf.result += nf.currencyString + if !supportedLanguageInfo[nf.localCode].useGannen { + nf.result += nf.currencyString + } } if token.TType == nfp.TokenTypeDateTimes { nf.dateTimesHandler(i, token) @@ -1752,15 +1769,35 @@ func (nf *numberFormat) dateTimesHandler(i int, token nfp.Token) { // yearsHandler will be handling years in the date and times types tokens for a // number format expression. func (nf *numberFormat) yearsHandler(token nfp.Token) { - years := strings.Contains(strings.ToUpper(token.TValue), "Y") - if years && len(token.TValue) <= 2 { - nf.result += strconv.Itoa(nf.t.Year())[2:] - return - } - if years && len(token.TValue) > 2 { + if strings.Contains(strings.ToUpper(token.TValue), "Y") { + if len(token.TValue) <= 2 { + nf.result += strconv.Itoa(nf.t.Year())[2:] + return + } nf.result += strconv.Itoa(nf.t.Year()) return } + if strings.Contains(strings.ToUpper(token.TValue), "G") { + for i := len(japaneseEraYears) - 1; i > 0; i-- { + if y := japaneseEraYears[i]; nf.t.After(y) { + switch len(token.TValue) { + case 1: + nf.result += japaneseEraSymbols[i] + case 2: + nf.result += japaneseEraNames[i][:3] + default: + nf.result += japaneseEraNames[i] + } + year := nf.t.Year() - y.Year() + 1 + if year == 1 && len(token.TValue) > 1 && supportedLanguageInfo[nf.localCode].useGannen { + nf.result += "\u5143" + break + } + nf.result += strconv.Itoa(year) + break + } + } + } } // daysHandler will be handling days in the date and times types tokens for a diff --git a/numfmt_test.go b/numfmt_test.go index cdd3f2e..05646a0 100644 --- a/numfmt_test.go +++ b/numfmt_test.go @@ -420,6 +420,18 @@ func TestNumFmt(t *testing.T) { {"44835.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM", "\u0e15 01 2022 4:32 AM"}, {"44866.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM", "\u0e1e 01 2022 4:32 AM"}, {"44896.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM", "\u0e18 01 2022 4:32 AM"}, + {"43709", "[$-411]ge\"年\"m\"月\"d\"日\";@", "R1年9月1日"}, + {"43709", "[$-411]gge\"年\"m\"月\"d\"日\";@", "\u4EE41年9月1日"}, + {"43709", "[$-411]ggge\"年\"m\"月\"d\"日\";@", "\u4EE4\u548C1年9月1日"}, + {"43709", "[$-ja-JP-x-gannen,80]ge\"年\"m\"月\"d\"日\";@", "R1年9月1日"}, + {"43709", "[$-ja-JP-x-gannen,80]gge\"年\"m\"月\"d\"日\";@", "\u4EE4\u5143年9月1日"}, + {"43709", "[$-ja-JP-x-gannen,80]ggge\"年\"m\"月\"d\"日\";@", "\u4EE4\u548C\u5143年9月1日"}, + {"43466.189571759256", "[$-411]ge\"年\"m\"月\"d\"日\";@", "H31年1月1日"}, + {"43466.189571759256", "[$-411]gge\"年\"m\"月\"d\"日\";@", "\u5E7331年1月1日"}, + {"43466.189571759256", "[$-411]ggge\"年\"m\"月\"d\"日\";@", "\u5E73\u621031年1月1日"}, + {"44896.18957170139", "[$-411]ge\"年\"m\"月\"d\"日\";@", "R4年12月1日"}, + {"44896.18957170139", "[$-411]gge\"年\"m\"月\"d\"日\";@", "\u4EE44年12月1日"}, + {"44896.18957170139", "[$-411]ggge\"年\"m\"月\"d\"日\";@", "\u4EE4\u548C4年12月1日"}, {"44562.189571759256", "[$-51]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f21 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, {"44593.189571759256", "[$-51]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f22 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, {"44621.18957170139", "[$-51]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f23 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, diff --git a/vml.go b/vml.go index 540e36b..844149e 100644 --- a/vml.go +++ b/vml.go @@ -420,9 +420,15 @@ func (f *File) DeleteFormControl(sheet, cell string) error { for i, sp := range vml.Shape { var shapeVal decodeShapeVal if err = xml.Unmarshal([]byte(fmt.Sprintf("%s", sp.Val)), &shapeVal); err == nil && - shapeVal.ClientData.ObjectType != "Note" && shapeVal.ClientData.Column == col-1 && shapeVal.ClientData.Row == row-1 { - vml.Shape = append(vml.Shape[:i], vml.Shape[i+1:]...) - break + shapeVal.ClientData.ObjectType != "Note" && shapeVal.ClientData.Anchor != "" { + leftCol, topRow, err := extractAnchorCell(shapeVal.ClientData.Anchor) + if err != nil { + return err + } + if leftCol == col-1 && topRow == row-1 { + vml.Shape = append(vml.Shape[:i], vml.Shape[i+1:]...) + break + } } } f.VMLDrawing[drawingVML] = vml @@ -454,7 +460,7 @@ func (f *File) decodeVMLDrawingReader(path string) (*decodeVmlDrawing, error) { c, ok := f.Pkg.Load(path) if ok && c != nil { f.DecodeVMLDrawing[path] = new(decodeVmlDrawing) - if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(c.([]byte)))). + if err := f.xmlNewDecoder(bytes.NewReader(bytesReplace(namespaceStrictToTransitional(c.([]byte)), []byte("
\r\n"), []byte("

\r\n"), -1))). Decode(f.DecodeVMLDrawing[path]); err != nil && err != io.EOF { return nil, err } @@ -574,6 +580,9 @@ func formCtrlText(opts *vmlOptions) []vmlFont { if run.Font.Underline == "single" { fnt.Content = "" + fnt.Content + "" } + if run.Font.Underline == "double" { + fnt.Content = "" + fnt.Content + "" + } if run.Font.Italic { fnt.Content = "" + fnt.Content + "" } @@ -765,8 +774,8 @@ func (f *File) addFormCtrlShape(preset formCtrlPreset, col, row int, anchor stri ObjectType: preset.objectType, Anchor: anchor, AutoFill: preset.autoFill, - Row: row - 1, - Column: col - 1, + Row: intPtr(row - 1), + Column: intPtr(col - 1), TextHAlign: preset.textHAlign, TextVAlign: preset.textVAlign, NoThreeD: preset.noThreeD, @@ -885,8 +894,8 @@ func (f *File) addDrawingVML(dataID int, drawingVML string, opts *vmlOptions) er } // GetFormControls retrieves all form controls in a worksheet by a given -// worksheet name. Note that, this function does not support getting the width, -// height, text, rich text, and format currently. +// worksheet name. Note that, this function does not support getting the width +// and height of the form controls currently. func (f *File) GetFormControls(sheet string) ([]FormControl, error) { var formControls []FormControl // Read sheet data @@ -949,9 +958,18 @@ func extractFormControl(clientData string) (FormControl, error) { return formControl, err } for formCtrlType, preset := range formCtrlPresets { - if shapeVal.ClientData.ObjectType == preset.objectType { + if shapeVal.ClientData.ObjectType == preset.objectType && shapeVal.ClientData.Anchor != "" { + formControl.Paragraph = extractVMLFont(shapeVal.TextBox.Div.Font) + if len(formControl.Paragraph) > 0 && formControl.Paragraph[0].Font == nil { + formControl.Text = formControl.Paragraph[0].Text + formControl.Paragraph = formControl.Paragraph[1:] + } formControl.Type = formCtrlType - if formControl.Cell, err = CoordinatesToCellName(shapeVal.ClientData.Column+1, shapeVal.ClientData.Row+1); err != nil { + col, row, err := extractAnchorCell(shapeVal.ClientData.Anchor) + if err != nil { + return formControl, err + } + if formControl.Cell, err = CoordinatesToCellName(col+1, row+1); err != nil { return formControl, err } formControl.Macro = shapeVal.ClientData.FmlaMacro @@ -967,3 +985,79 @@ func extractFormControl(clientData string) (FormControl, error) { } return formControl, err } + +// extractAnchorCell extract left-top cell coordinates from given VML anchor +// comma-separated list values. +func extractAnchorCell(anchor string) (int, int, error) { + var ( + leftCol, topRow int + err error + pos = strings.Split(anchor, ",") + ) + if len(pos) != 8 { + return leftCol, topRow, ErrParameterInvalid + } + leftCol, err = strconv.Atoi(strings.TrimSpace(pos[0])) + if err != nil { + return leftCol, topRow, ErrColumnNumber + } + topRow, err = strconv.Atoi(strings.TrimSpace(pos[2])) + return leftCol, topRow, err +} + +// extractVMLFont extract rich-text and font format from given VML font element. +func extractVMLFont(font []decodeVMLFont) []RichTextRun { + var runs []RichTextRun + extractU := func(u *decodeVMLFontU, run *RichTextRun) { + if u == nil { + return + } + run.Text += u.Val + if run.Font == nil { + run.Font = &Font{} + } + run.Font.Underline = "single" + if u.Class == "font1" { + run.Font.Underline = "double" + } + } + extractI := func(i *decodeVMLFontI, run *RichTextRun) { + if i == nil { + return + } + extractU(i.U, run) + run.Text += i.Val + if run.Font == nil { + run.Font = &Font{} + } + run.Font.Italic = true + } + extractB := func(b *decodeVMLFontB, run *RichTextRun) { + if b == nil { + return + } + extractI(b.I, run) + run.Text += b.Val + if run.Font == nil { + run.Font = &Font{} + } + run.Font.Bold = true + } + for _, fnt := range font { + var run RichTextRun + extractB(fnt.B, &run) + extractI(fnt.I, &run) + extractU(fnt.U, &run) + run.Text += fnt.Val + if fnt.Face != "" || fnt.Size > 0 || fnt.Color != "" { + if run.Font == nil { + run.Font = &Font{} + } + run.Font.Family = fnt.Face + run.Font.Size = float64(fnt.Size / 20) + run.Font.Color = fnt.Color + } + runs = append(runs, run) + } + return runs +} diff --git a/vmlDrawing.go b/vmlDrawing.go index 76e9011..7ebedb7 100644 --- a/vmlDrawing.go +++ b/vmlDrawing.go @@ -137,8 +137,8 @@ type xClientData struct { FmlaMacro string `xml:"x:FmlaMacro,omitempty"` TextHAlign string `xml:"x:TextHAlign,omitempty"` TextVAlign string `xml:"x:TextVAlign,omitempty"` - Row int `xml:"x:Row"` - Column int `xml:"x:Column"` + Row *int `xml:"x:Row"` + Column *int `xml:"x:Column"` Checked int `xml:"x:Checked,omitempty"` FmlaLink string `xml:"x:FmlaLink,omitempty"` NoThreeD *string `xml:"x:NoThreeD"` @@ -185,16 +185,59 @@ type decodeShape struct { // decodeShapeVal defines the structure used to parse the sub-element of the // shape in the file xl/drawings/vmlDrawing%d.vml. type decodeShapeVal struct { + TextBox decodeVMLTextBox `xml:"textbox"` ClientData decodeVMLClientData `xml:"ClientData"` } +// decodeVMLFontU defines the structure used to parse the u element in the VML. +type decodeVMLFontU struct { + Class string `xml:"class,attr"` + Val string `xml:",chardata"` +} + +// decodeVMLFontI defines the structure used to parse the i element in the VML. +type decodeVMLFontI struct { + U *decodeVMLFontU `xml:"u"` + Val string `xml:",chardata"` +} + +// decodeVMLFontB defines the structure used to parse the b element in the VML. +type decodeVMLFontB struct { + I *decodeVMLFontI `xml:"i"` + U *decodeVMLFontU `xml:"u"` + Val string `xml:",chardata"` +} + +// decodeVMLFont defines the structure used to parse the font element in the VML. +type decodeVMLFont struct { + Face string `xml:"face,attr,omitempty"` + Size uint `xml:"size,attr,omitempty"` + Color string `xml:"color,attr,omitempty"` + B *decodeVMLFontB `xml:"b"` + I *decodeVMLFontI `xml:"i"` + U *decodeVMLFontU `xml:"u"` + Val string `xml:",chardata"` +} + +// decodeVMLDiv defines the structure used to parse the div element in the VML. +type decodeVMLDiv struct { + Font []decodeVMLFont `xml:"font"` +} + +// decodeVMLTextBox defines the structure used to parse the v:textbox element in +// the file xl/drawings/vmlDrawing%d.vml. +type decodeVMLTextBox struct { + Div decodeVMLDiv `xml:"div"` +} + // decodeVMLClientData defines the structure used to parse the x:ClientData // element in the file xl/drawings/vmlDrawing%d.vml. type decodeVMLClientData struct { ObjectType string `xml:"ObjectType,attr"` + Anchor string FmlaMacro string - Column int - Row int + Column *int + Row *int Checked int FmlaLink string Val uint diff --git a/vml_test.go b/vml_test.go index 52a7dad..34d7f25 100644 --- a/vml_test.go +++ b/vml_test.go @@ -164,7 +164,7 @@ func TestFormControl(t *testing.T) { }, { Cell: "A1", Type: FormControlButton, Macro: "Button1_Click", - Width: 140, Height: 60, Text: "Button 1\r\n", + Width: 140, Height: 60, Text: "Button 1\n", Paragraph: []RichTextRun{ { Font: &Font{ @@ -234,6 +234,8 @@ func TestFormControl(t *testing.T) { assert.Equal(t, formCtrl.IncChange, result[i].IncChange) assert.Equal(t, formCtrl.Horizontally, result[i].Horizontally) assert.Equal(t, formCtrl.CellLink, result[i].CellLink) + assert.Equal(t, formCtrl.Text, result[i].Text) + assert.Equal(t, len(formCtrl.Paragraph), len(result[i].Paragraph)) } assert.NoError(t, f.SetSheetProps("Sheet1", &SheetPropsOptions{CodeName: stringPtr("Sheet1")})) file, err := os.ReadFile(filepath.Join("test", "vbaProject.bin")) @@ -249,7 +251,8 @@ func TestFormControl(t *testing.T) { assert.Len(t, result, 11) // Test add from control to a worksheet which already contains form controls assert.NoError(t, f.AddFormControl("Sheet1", FormControl{ - Cell: "D4", Type: FormControlButton, Macro: "Button1_Click", Text: "Button 2", + Cell: "D4", Type: FormControlButton, Macro: "Button1_Click", + Paragraph: []RichTextRun{{Font: &Font{Underline: "double"}, Text: "Button 2"}}, })) // Test get from controls after add form controls result, err = f.GetFormControls("Sheet1") @@ -297,6 +300,11 @@ func TestFormControl(t *testing.T) { assert.NoError(t, err) f.Pkg.Store("xl/drawings/vmlDrawing1.vml", MacintoshCyrillicCharset) assert.Error(t, f.DeleteFormControl("Sheet1", "A1"), "XML syntax error on line 1: invalid UTF-8") + // Test delete form controls with invalid shape anchor + f.DecodeVMLDrawing["xl/drawings/vmlDrawing1.vml"] = &decodeVmlDrawing{ + Shape: []decodeShape{{Type: "#_x0000_t201", Val: "0"}}, + } + assert.Equal(t, ErrParameterInvalid, f.DeleteFormControl("Sheet1", "A1")) assert.NoError(t, f.Close()) // Test delete form control on a worksheet without form control f = NewFile() @@ -320,9 +328,39 @@ func TestFormControl(t *testing.T) { formControls, err = f.GetFormControls("Sheet1") assert.NoError(t, err) assert.Len(t, formControls, 0) + // Test get form controls with bold font format + f.DecodeVMLDrawing["xl/drawings/vmlDrawing1.vml"] = &decodeVmlDrawing{ + Shape: []decodeShape{{Type: "#_x0000_t201", Val: "
Text
0,0,0,0,0,0,0,0"}}, + } + formControls, err = f.GetFormControls("Sheet1") + assert.NoError(t, err) + assert.True(t, formControls[0].Paragraph[0].Font.Bold) + // Test get form controls with italic font format + f.DecodeVMLDrawing["xl/drawings/vmlDrawing1.vml"] = &decodeVmlDrawing{ + Shape: []decodeShape{{Type: "#_x0000_t201", Val: "
Text
0,0,0,0,0,0,0,0"}}, + } + formControls, err = f.GetFormControls("Sheet1") + assert.NoError(t, err) + assert.True(t, formControls[0].Paragraph[0].Font.Italic) + // Test get form controls with font format + f.DecodeVMLDrawing["xl/drawings/vmlDrawing1.vml"] = &decodeVmlDrawing{ + Shape: []decodeShape{{Type: "#_x0000_t201", Val: "
Text
0,0,0,0,0,0,0,0"}}, + } + formControls, err = f.GetFormControls("Sheet1") + assert.NoError(t, err) + assert.Equal(t, "Calibri", formControls[0].Paragraph[0].Font.Family) + assert.Equal(t, 14.0, formControls[0].Paragraph[0].Font.Size) + assert.Equal(t, "#777777", formControls[0].Paragraph[0].Font.Color) + // Test get form controls with italic font format + f.DecodeVMLDrawing["xl/drawings/vmlDrawing1.vml"] = &decodeVmlDrawing{ + Shape: []decodeShape{{Type: "#_x0000_t201", Val: "
Text
0,0,0,0,0,0,0,0"}}, + } + formControls, err = f.GetFormControls("Sheet1") + assert.NoError(t, err) + assert.True(t, formControls[0].Paragraph[0].Font.Italic) // Test get form controls with invalid column number f.DecodeVMLDrawing["xl/drawings/vmlDrawing1.vml"] = &decodeVmlDrawing{ - Shape: []decodeShape{{Type: "#_x0000_t201", Val: fmt.Sprintf("%d", MaxColumns)}}, + Shape: []decodeShape{{Type: "#_x0000_t201", Val: fmt.Sprintf("%d,0,0,0,0,0,0,0", MaxColumns)}}, } formControls, err = f.GetFormControls("Sheet1") assert.Equal(t, err, ErrColumnNumber) @@ -343,11 +381,18 @@ func TestFormControl(t *testing.T) { assert.Len(t, formControls, 0) // Test get form controls with invalid column number f.VMLDrawing["xl/drawings/vmlDrawing1.vml"] = &vmlDrawing{ - Shape: []xlsxShape{{Type: "#_x0000_t201", Val: fmt.Sprintf("%d", MaxColumns)}}, + Shape: []xlsxShape{{Type: "#_x0000_t201", Val: fmt.Sprintf("%d,0,0,0,0,0,0,0", MaxColumns)}}, } formControls, err = f.GetFormControls("Sheet1") assert.Equal(t, err, ErrColumnNumber) assert.Len(t, formControls, 0) + // Test get form controls with invalid shape anchor + f.VMLDrawing["xl/drawings/vmlDrawing1.vml"] = &vmlDrawing{ + Shape: []xlsxShape{{Type: "#_x0000_t201", Val: "x,0,0,0,0,0,0,0"}}, + } + formControls, err = f.GetFormControls("Sheet1") + assert.Equal(t, ErrColumnNumber, err) + assert.Len(t, formControls, 0) // Test get form controls with comment (Note) shape type f.VMLDrawing["xl/drawings/vmlDrawing1.vml"] = &vmlDrawing{ Shape: []xlsxShape{{Type: "#_x0000_t201", Val: ""}}, diff --git a/xmlStyles.go b/xmlStyles.go index 3a56f6f..80d9959 100644 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -300,8 +300,9 @@ type xlsxNumFmts struct { // format properties which indicate how to format and render the numeric value // of a cell. type xlsxNumFmt struct { - NumFmtID int `xml:"numFmtId,attr"` - FormatCode string `xml:"formatCode,attr,omitempty"` + NumFmtID int `xml:"numFmtId,attr"` + FormatCode string `xml:"formatCode,attr,omitempty"` + FormatCode16 string `xml:"http://schemas.microsoft.com/office/spreadsheetml/2015/02/main formatCode16,attr,omitempty"` } // xlsxStyleColors directly maps the colors' element. Color information