已验证 提交 49234fb9 编写于 作者: xurime's avatar xurime

Ref #1199, this support applies partial built-in language number format code

- Remove the `Lang` field in the `Style` data type
- Rename field name `ShortDateFmtCode` to `ShortDatePattern` in the `Options` data type
- Rename field name `LongDateFmtCode` to `LongDatePattern` in the `Options` data type
- Rename field name `LongTimeFmtCode` to `LongTimePattern` in the `Options` data type
- Apply built-in language number format code number when creating a new style
- Checking and returning error if the date and time pattern was invalid
- Add new `Options` field `CultureInfo` and new exported data type `CultureName`
- Add new culture name types enumeration for country code
- Update unit tests
- Move built-in number format code and currency number format code definition source code
- Remove the built-in language number format code mapping with Unicode values
- Fix incorrect number formatted result for date and time with 12 hours at AM
上级 dfdd97c0
...@@ -1336,15 +1336,6 @@ func (f *File) getCellStringFunc(sheet, cell string, fn func(x *xlsxWorksheet, c ...@@ -1336,15 +1336,6 @@ func (f *File) getCellStringFunc(sheet, cell string, fn func(x *xlsxWorksheet, c
return "", nil return "", nil
} }
// applyBuiltInNumFmt provides a function to returns a value after formatted
// with built-in number format code, or specified sort date format code.
func (f *File) applyBuiltInNumFmt(c *xlsxC, fmtCode string, numFmtID int, date1904 bool, cellType CellType) string {
if numFmtID == 14 && f.options != nil && f.options.ShortDateFmtCode != "" {
fmtCode = f.options.ShortDateFmtCode
}
return format(c.V, fmtCode, date1904, cellType, f.options)
}
// formattedValue provides a function to returns a value after formatted. If // formattedValue provides a function to returns a value after formatted. If
// it is possible to apply a format to the cell value, it will do so, if not // it is possible to apply a format to the cell value, it will do so, if not
// then an error will be returned, along with the raw value of the cell. // then an error will be returned, along with the raw value of the cell.
...@@ -1374,7 +1365,7 @@ func (f *File) formattedValue(c *xlsxC, raw bool, cellType CellType) (string, er ...@@ -1374,7 +1365,7 @@ func (f *File) formattedValue(c *xlsxC, raw bool, cellType CellType) (string, er
if wb != nil && wb.WorkbookPr != nil { if wb != nil && wb.WorkbookPr != nil {
date1904 = wb.WorkbookPr.Date1904 date1904 = wb.WorkbookPr.Date1904
} }
if fmtCode, ok := builtInNumFmt[numFmtID]; ok { if fmtCode, ok := f.getBuiltInNumFmtCode(numFmtID); ok {
return f.applyBuiltInNumFmt(c, fmtCode, numFmtID, date1904, cellType), err return f.applyBuiltInNumFmt(c, fmtCode, numFmtID, date1904, cellType), err
} }
if styleSheet.NumFmts == nil { if styleSheet.NumFmts == nil {
......
...@@ -60,7 +60,7 @@ type File struct { ...@@ -60,7 +60,7 @@ type File struct {
// the spreadsheet from non-UTF-8 encoding. // the spreadsheet from non-UTF-8 encoding.
type charsetTranscoderFn func(charset string, input io.Reader) (rdr io.Reader, err error) type charsetTranscoderFn func(charset string, input io.Reader) (rdr io.Reader, err error)
// Options define the options for open and reading spreadsheet. // Options define the options for o`pen and reading spreadsheet.
// //
// MaxCalcIterations specifies the maximum iterations for iterative // MaxCalcIterations specifies the maximum iterations for iterative
// calculation, the default value is 0. // calculation, the default value is 0.
...@@ -80,26 +80,30 @@ type charsetTranscoderFn func(charset string, input io.Reader) (rdr io.Reader, e ...@@ -80,26 +80,30 @@ type charsetTranscoderFn func(charset string, input io.Reader) (rdr io.Reader, e
// should be less than or equal to UnzipSizeLimit, the default value is // should be less than or equal to UnzipSizeLimit, the default value is
// 16MB. // 16MB.
// //
// ShortDateFmtCode specifies the short date number format code. In the // ShortDatePattern specifies the short date number format code. In the
// spreadsheet applications, date formats display date and time serial numbers // spreadsheet applications, date formats display date and time serial numbers
// as date values. Date formats that begin with an asterisk (*) respond to // as date values. Date formats that begin with an asterisk (*) respond to
// changes in regional date and time settings that are specified for the // changes in regional date and time settings that are specified for the
// operating system. Formats without an asterisk are not affected by operating // operating system. Formats without an asterisk are not affected by operating
// system settings. The ShortDateFmtCode used for specifies apply date formats // system settings. The ShortDatePattern used for specifies apply date formats
// that begin with an asterisk. // that begin with an asterisk.
// //
// LongDateFmtCode specifies the long date number format code. // LongDatePattern specifies the long date number format code.
// //
// LongTimeFmtCode specifies the long time number format code. // LongTimePattern specifies the long time number format code.
//
// CultureInfo specifies the country code for applying built-in language number
// format code these effect by the system's local language settings.
type Options struct { type Options struct {
MaxCalcIterations uint MaxCalcIterations uint
Password string Password string
RawCellValue bool RawCellValue bool
UnzipSizeLimit int64 UnzipSizeLimit int64
UnzipXMLSizeLimit int64 UnzipXMLSizeLimit int64
ShortDateFmtCode string ShortDatePattern string
LongDateFmtCode string LongDatePattern string
LongTimeFmtCode string LongTimePattern string
CultureInfo CultureName
} }
// OpenFile take the name of an spreadsheet file and returns a populated // OpenFile take the name of an spreadsheet file and returns a populated
...@@ -162,7 +166,7 @@ func (f *File) checkOpenReaderOptions() error { ...@@ -162,7 +166,7 @@ func (f *File) checkOpenReaderOptions() error {
if f.options.UnzipXMLSizeLimit > f.options.UnzipSizeLimit { if f.options.UnzipXMLSizeLimit > f.options.UnzipSizeLimit {
return ErrOptionsUnzipSizeLimit return ErrOptionsUnzipSizeLimit
} }
return nil return f.checkDateTimePattern()
} }
// OpenReader read data stream from io.Reader and return a populated // OpenReader read data stream from io.Reader and return a populated
......
...@@ -752,7 +752,7 @@ func TestSetCellStyleNumberFormat(t *testing.T) { ...@@ -752,7 +752,7 @@ func TestSetCellStyleNumberFormat(t *testing.T) {
expected := [][]string{ expected := [][]string{
{"37947.7500001", "37948", "37947.75", "37,948", "37,947.75", "3794775%", "3794775.00%", "3.79E+04", "37947.7500001", "37947.7500001", "11-22-03", "22-Nov-03", "22-Nov", "Nov-03", "6:00 PM", "6:00:00 PM", "18:00", "18:00:00", "11/22/03 18:00", "37,948 ", "37,948 ", "37,947.75 ", "37,947.75 ", "37947.7500001", "37947.7500001", "37947.7500001", "37947.7500001", "00:00", "910746:00:00", "00:00.0", "37947.7500001", "37947.7500001"}, {"37947.7500001", "37948", "37947.75", "37,948", "37,947.75", "3794775%", "3794775.00%", "3.79E+04", "37947.7500001", "37947.7500001", "11-22-03", "22-Nov-03", "22-Nov", "Nov-03", "6:00 PM", "6:00:00 PM", "18:00", "18:00:00", "11/22/03 18:00", "37,948 ", "37,948 ", "37,947.75 ", "37,947.75 ", "37947.7500001", "37947.7500001", "37947.7500001", "37947.7500001", "00:00", "910746:00:00", "00:00.0", "37947.7500001", "37947.7500001"},
{"-37947.7500001", "-37948", "-37947.75", "-37,948", "-37,947.75", "-3794775%", "-3794775.00%", "-3.79E+04", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "(37,948)", "(37,948)", "(37,947.75)", "(37,947.75)", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001"}, {"-37947.7500001", "-37948", "-37947.75", "-37,948", "-37,947.75", "-3794775%", "-3794775.00%", "-3.79E+04", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "(37,948)", "(37,948)", "(37,947.75)", "(37,947.75)", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001"},
{"0.007", "0", "0.01", "0", "0.01", "1%", "0.70%", "7.00E-03", "0.007", "0.007", "12-30-99", "30-Dec-99", "30-Dec", "Dec-99", "0:10 AM", "0:10:05 AM", "00:10", "00:10:05", "12/30/99 00:10", "0 ", "0 ", "0.01 ", "0.01 ", "0.007", "0.007", "0.007", "0.007", "10:05", "0:10:05", "10:04.8", "0.007", "0.007"}, {"0.007", "0", "0.01", "0", "0.01", "1%", "0.70%", "7.00E-03", "0.007", "0.007", "12-30-99", "30-Dec-99", "30-Dec", "Dec-99", "12:10 AM", "12:10:05 AM", "00:10", "00:10:05", "12/30/99 00:10", "0 ", "0 ", "0.01 ", "0.01 ", "0.007", "0.007", "0.007", "0.007", "10:05", "0:10:05", "10:04.8", "0.007", "0.007"},
{"2.1", "2", "2.10", "2", "2.10", "210%", "210.00%", "2.10E+00", "2.1", "2.1", "01-01-00", "1-Jan-00", "1-Jan", "Jan-00", "2:24 AM", "2:24:00 AM", "02:24", "02:24:00", "1/1/00 02:24", "2 ", "2 ", "2.10 ", "2.10 ", "2.1", "2.1", "2.1", "2.1", "24:00", "50:24:00", "24:00.0", "2.1", "2.1"}, {"2.1", "2", "2.10", "2", "2.10", "210%", "210.00%", "2.10E+00", "2.1", "2.1", "01-01-00", "1-Jan-00", "1-Jan", "Jan-00", "2:24 AM", "2:24:00 AM", "02:24", "02:24:00", "1/1/00 02:24", "2 ", "2 ", "2.10 ", "2.10 ", "2.1", "2.1", "2.1", "2.1", "24:00", "50:24:00", "24:00.0", "2.1", "2.1"},
{"String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String"}, {"String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String"},
} }
...@@ -811,19 +811,19 @@ func TestSetCellStyleCurrencyNumberFormat(t *testing.T) { ...@@ -811,19 +811,19 @@ func TestSetCellStyleCurrencyNumberFormat(t *testing.T) {
assert.NoError(t, f.SetCellValue("Sheet1", "A1", 42920.5)) assert.NoError(t, f.SetCellValue("Sheet1", "A1", 42920.5))
assert.NoError(t, f.SetCellValue("Sheet1", "A2", 42920.5)) assert.NoError(t, f.SetCellValue("Sheet1", "A2", 42920.5))
_, err = f.NewStyle(&Style{NumFmt: 26, Lang: "zh-tw"}) _, err = f.NewStyle(&Style{NumFmt: 26})
assert.NoError(t, err) assert.NoError(t, err)
style, err := f.NewStyle(&Style{NumFmt: 27}) style, err := f.NewStyle(&Style{NumFmt: 27})
assert.NoError(t, err) assert.NoError(t, err)
assert.NoError(t, f.SetCellStyle("Sheet1", "A1", "A1", style)) assert.NoError(t, f.SetCellStyle("Sheet1", "A1", "A1", style))
style, err = f.NewStyle(&Style{NumFmt: 31, Lang: "ko-kr"}) style, err = f.NewStyle(&Style{NumFmt: 31})
assert.NoError(t, err) assert.NoError(t, err)
assert.NoError(t, f.SetCellStyle("Sheet1", "A2", "A2", style)) assert.NoError(t, f.SetCellStyle("Sheet1", "A2", "A2", style))
style, err = f.NewStyle(&Style{NumFmt: 71, Lang: "th-th"}) style, err = f.NewStyle(&Style{NumFmt: 71})
assert.NoError(t, err) assert.NoError(t, err)
assert.NoError(t, f.SetCellStyle("Sheet1", "A2", "A2", style)) assert.NoError(t, f.SetCellStyle("Sheet1", "A2", "A2", style))
...@@ -831,6 +831,41 @@ func TestSetCellStyleCurrencyNumberFormat(t *testing.T) { ...@@ -831,6 +831,41 @@ func TestSetCellStyleCurrencyNumberFormat(t *testing.T) {
}) })
} }
func TestSetCellStyleLangNumberFormat(t *testing.T) {
rawCellValues := [][]string{{"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}}
for lang, expected := range map[CultureName][][]string{
CultureNameUnknown: rawCellValues,
CultureNameEnUS: {{"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"0:00:00"}, {"0:00:00"}, {"0:00:00"}, {"0:00:00"}, {"45162"}, {"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}},
CultureNameZhCN: {{"2023年8月"}, {"8月24日"}, {"8月24日"}, {"8/24/23"}, {"2023年8月24日"}, {"0时00分"}, {"0时00分00秒"}, {"上午12时00分"}, {"上午12时00分00秒"}, {"2023年8月"}, {"2023年8月"}, {"8月24日"}, {"2023年8月"}, {"8月24日"}, {"8月24日"}, {"上午12时00分"}, {"上午12时00分00秒"}, {"2023年8月"}, {"8月24日"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}},
} {
f, err := prepareTestBook5(Options{CultureInfo: lang})
assert.NoError(t, err)
rows, err := f.GetRows("Sheet1")
assert.NoError(t, err)
assert.Equal(t, expected, rows)
assert.NoError(t, f.Close())
}
// Test apply language number format code with date and time pattern
for lang, expected := range map[CultureName][][]string{
CultureNameEnUS: {{"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"00:00:00"}, {"00:00:00"}, {"00:00:00"}, {"00:00:00"}, {"45162"}, {"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}},
CultureNameZhCN: {{"2023年8月"}, {"8月24日"}, {"8月24日"}, {"2023-8-24"}, {"2023年8月24日"}, {"00:00:00"}, {"00:00:00"}, {"上午12时00分"}, {"上午12时00分00秒"}, {"2023年8月"}, {"2023年8月"}, {"8月24日"}, {"2023年8月"}, {"8月24日"}, {"8月24日"}, {"上午12时00分"}, {"上午12时00分00秒"}, {"2023年8月"}, {"8月24日"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}},
} {
f, err := prepareTestBook5(Options{CultureInfo: lang, ShortDatePattern: "yyyy-M-d", LongTimePattern: "hh:mm:ss"})
assert.NoError(t, err)
rows, err := f.GetRows("Sheet1")
assert.NoError(t, err)
assert.Equal(t, expected, rows)
assert.NoError(t, f.Close())
}
// Test open workbook with invalid date and time pattern options
_, err := OpenFile(filepath.Join("test", "Book1.xlsx"), Options{LongDatePattern: "0.00"})
assert.Equal(t, ErrUnsupportedNumberFormat, err)
_, err = OpenFile(filepath.Join("test", "Book1.xlsx"), Options{LongTimePattern: "0.00"})
assert.Equal(t, ErrUnsupportedNumberFormat, err)
_, err = OpenFile(filepath.Join("test", "Book1.xlsx"), Options{ShortDatePattern: "0.00"})
assert.Equal(t, ErrUnsupportedNumberFormat, err)
}
func TestSetCellStyleCustomNumberFormat(t *testing.T) { func TestSetCellStyleCustomNumberFormat(t *testing.T) {
f := NewFile() f := NewFile()
assert.NoError(t, f.SetCellValue("Sheet1", "A1", 42920.5)) assert.NoError(t, f.SetCellValue("Sheet1", "A1", 42920.5))
...@@ -1612,6 +1647,31 @@ func prepareTestBook4() (*File, error) { ...@@ -1612,6 +1647,31 @@ func prepareTestBook4() (*File, error) {
return f, nil return f, nil
} }
func prepareTestBook5(opts Options) (*File, error) {
f := NewFile(opts)
var rowNum int
for _, idxRange := range [][]int{{27, 36}, {50, 81}} {
for numFmtIdx := idxRange[0]; numFmtIdx <= idxRange[1]; numFmtIdx++ {
rowNum++
styleID, err := f.NewStyle(&Style{NumFmt: numFmtIdx})
if err != nil {
return f, err
}
cell, err := CoordinatesToCellName(1, rowNum)
if err != nil {
return f, err
}
if err := f.SetCellValue("Sheet1", cell, 45162); err != nil {
return f, err
}
if err := f.SetCellStyle("Sheet1", cell, cell, styleID); err != nil {
return f, err
}
}
}
return f, nil
}
func fillCells(f *File, sheet string, colCount, rowCount int) error { func fillCells(f *File, sheet string, colCount, rowCount int) error {
for col := 1; col <= colCount; col++ { for col := 1; col <= colCount; col++ {
for row := 1; row <= rowCount; row++ { for row := 1; row <= rowCount; row++ {
......
此差异已折叠。
...@@ -1072,9 +1072,9 @@ func TestNumFmt(t *testing.T) { ...@@ -1072,9 +1072,9 @@ func TestNumFmt(t *testing.T) {
{"43543.503206018519", "[$-F400]h:mm:ss AM/PM", "12:04:37"}, {"43543.503206018519", "[$-F400]h:mm:ss AM/PM", "12:04:37"},
} { } {
result := format(item[0], item[1], false, CellTypeNumber, &Options{ result := format(item[0], item[1], false, CellTypeNumber, &Options{
ShortDateFmtCode: "yyyy/m/d", ShortDatePattern: "yyyy/m/d",
LongDateFmtCode: "yyyy\"\"M\"\"d\"\"", LongDatePattern: "yyyy\"\"M\"\"d\"\"",
LongTimeFmtCode: "H:mm:ss", LongTimePattern: "H:mm:ss",
}) })
assert.Equal(t, item[2], result, item) assert.Equal(t, item[2], result, item)
} }
......
...@@ -1118,7 +1118,7 @@ func TestNumberFormats(t *testing.T) { ...@@ -1118,7 +1118,7 @@ func TestNumberFormats(t *testing.T) {
} }
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestNumberFormats.xlsx"))) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestNumberFormats.xlsx")))
f = NewFile(Options{ShortDateFmtCode: "yyyy/m/d"}) f = NewFile(Options{ShortDatePattern: "yyyy/m/d"})
assert.NoError(t, f.SetCellValue("Sheet1", "A1", 43543.503206018519)) assert.NoError(t, f.SetCellValue("Sheet1", "A1", 43543.503206018519))
numFmt14, err := f.NewStyle(&Style{NumFmt: 14}) numFmt14, err := f.NewStyle(&Style{NumFmt: 14})
assert.NoError(t, err) assert.NoError(t, err)
......
此差异已折叠。
...@@ -291,9 +291,7 @@ func TestNewStyle(t *testing.T) { ...@@ -291,9 +291,7 @@ func TestNewStyle(t *testing.T) {
// Test create currency custom style // Test create currency custom style
f.Styles.NumFmts = nil f.Styles.NumFmts = nil
styleID, err = f.NewStyle(&Style{ styleID, err = f.NewStyle(&Style{
Lang: "ko-kr",
NumFmt: 32, // must not be in currencyNumFmt NumFmt: 32, // must not be in currencyNumFmt
}) })
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, 3, styleID) assert.Equal(t, 3, styleID)
...@@ -330,14 +328,14 @@ func TestNewStyle(t *testing.T) { ...@@ -330,14 +328,14 @@ func TestNewStyle(t *testing.T) {
f = NewFile() f = NewFile()
f.Styles.NumFmts = nil f.Styles.NumFmts = nil
f.Styles.CellXfs.Xf = nil f.Styles.CellXfs.Xf = nil
style4, err := f.NewStyle(&Style{NumFmt: 160, Lang: "unknown"}) style4, err := f.NewStyle(&Style{NumFmt: 160})
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, 0, style4) assert.Equal(t, 0, style4)
f = NewFile() f = NewFile()
f.Styles.NumFmts = nil f.Styles.NumFmts = nil
f.Styles.CellXfs.Xf = nil f.Styles.CellXfs.Xf = nil
style5, err := f.NewStyle(&Style{NumFmt: 160, Lang: "zh-cn"}) style5, err := f.NewStyle(&Style{NumFmt: 160})
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, 0, style5) assert.Equal(t, 0, style5)
......
...@@ -371,6 +371,5 @@ type Style struct { ...@@ -371,6 +371,5 @@ type Style struct {
NumFmt int NumFmt int
DecimalPlaces int DecimalPlaces int
CustomNumFmt *string CustomNumFmt *string
Lang string
NegRed bool NegRed bool
} }
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册