Excel 中自定义数字格式代码

四个区段

完整的自定义的格式代码分为四个区段,中间以”;”间隔,每个区段中的码对应不同类型的内容,如下所示:
对正数应用的内容 ; 对负数应用的内容 ; 对零值应用的内容 ; 对文本应用的内容。
但在实际应用中,区段数是可以小于4个的,结构含义如下所示:

  • 区段数为 1 :作用于所有类型的格式
  • 区段数为 2 :对正数和零值应用的格式 ; 对负数应用的格式
  • 区段数为 3 :对正数应用的格式 ; 对负数应用的格式 ; 对零值应用的格式

(PS:在区段为 2 时,如果第二个区段以”@”开头,第二个区段为对文本应用的内容)

常用自定义格式代码符号及其含义

三个数字占位符

  • #: 显示单元格中原有的数字,但不显示无意义的零值。
  • 0: 显示单元格中原有的数字,当数字位数少于代码的位数时,显示无意义的零值。
  • ?: 与0作用类似,但用空格代替无意义的零值
    如图所示:
    QQ20240420193720.png
    QQ20240420193741.png
    QQ20240420193754.png

数字定义相关

  • .: 小数点
  • %: 百分数
  • ,: 千位分隔符
  • E: 科学记数符号

颜色设置

  • [颜色]: 中括号中的内容为对应的颜色,可以是中文也可以是英文,如[RED][红色]都可以。

条件设置

  • [条件]: 中括号中的内容为具体条件,由><=>=<=<>跟具体数值所构成。若单元格的数字若不在限定的条件之内,则无法正常显示。

格式转换

  • [DBNuml]: 显示中文简体数字,如123显示为一百二十三
  • [DBNum2]: 显示中文繁体数字,如123显示为壹佰贰拾参
  • [DBNum3]: 显示全角的阿拉伯数字与中文简体单位的结合,如123显示为1百2十3

地区设置

  • [$-语言地区标识/标签]: 为指定单元格应用对应地区的时间、日期定义,如[$-404][$-zh-TW]都是指代台湾地区。
    (PS: WPS 不支持这种自定义格式代码,EXCEL 2013 版仅支持语言地区标识。)

其他

  • "": 可显示双引号之间的文本。
  • !: 强制显示!或\之后的一个字符(有点转义字符的意思)。
  • \: 同上。
  • *: 重复下一个字符。
  • _: 留出一个字符宽度的空格。
  • @: 文本占位符,显示单元格中原有的文本。

与日期时间格式相关的代码符号

  • aaa: 使用中文简写显示星期几,如:
  • aaaa: 使用中文全称显示星期几,如:星期一
  • d: 使用没有前导零的数字来显示日期,如: 1
  • dd: 使用有前导零的数字来显示日期,如: 01
  • ddd: 使用英文缩写显示星期几,如: Sun
  • dddd: 使用英文全称显示星期几,如: Sunday
  • m: 使用没有前导零的数字来显示月份或分钟。
  • mm: 使用有前导零的数字来显示月份或分钟
  • mmm: 使用英文缩写显示月份
  • mmmm: 使用英文全拼显示月份
  • mmmmm: 使用英文首字母显示月份
  • y: 使用两位数字显示公历年份,如: 01
  • yy: 同上
  • yyyy: 使用四位数字显示公历年份,如:1901
  • h: 使用没有前导零的数字来显示小时,如: 1
  • hh: 使用有前导零的数字来显示小时,如: 01
  • S: 使用没有前导零的数字来显示秒,如: 1
  • SS: 使用有前导零的数字来显示秒,如: 01
  • [h][m][s]: 显示超出进制的小时数、分数、秒数。
  • AM/PM: 使用英文上下午显示 12 进制的时间。
  • AP: 同上。
  • 上午/下午: 使用中文上下午显示 12 进制的时间。

Excelize中设置数字格式表达式以及相关源码分析

示例代码

在 Excelize 中,我们可以通过使用 Style 来为指定单元格设置数字格式,下面是示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package main

import (
"fmt"

"github.com/xuri/excelize/v2"
)

func main() {
f := excelize.NewFile()

// 自定义单元格数字格式
numfmt := "yyyy/mm/d"
style1, err := f.NewStyle(&excelize.Style{
CustomNumFmt: &numfmt,
})
if err != nil {
fmt.Println(err)
return
}
f.SetCellStyle("Sheet1", "D2", "D2", style1)
f.SetCellValue("Sheet1", "D2", 45405)

// 使用内置的单元格数字格式
style2, err := f.NewStyle(&excelize.Style{
NumFmt: 30, // 对应格式为 "m/d/yy"
})
if err != nil {
fmt.Println(err)
return
}
f.SetCellStyle("Sheet1", "A2", "A2", style2)
f.SetCellValue("Sheet1", "A2", 45405)

if err := f.SaveAs("./Book1.xlsx"); err != nil {
fmt.Println(err)
}
}

结果如图所示:
QQ20240420212640.png

源码分析

这里我们只专注于与 numfmt 相关的源码。

NewStyle

函数部分内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func (f *File) NewStyle(style *Style) (int, error) {
// ...
fs, err = parseFormatStyleSet(style)
if err != nil {
return cellXfsID, err
}
// ...
f.mu.Lock()
s, err := f.stylesReader()
if err != nil {
f.mu.Unlock()
return cellXfsID, err
}
f.mu.Unlock()
s.mu.Lock()
defer s.mu.Unlock()
// ...

numFmtID := newNumFmt(s, fs)

//...

applyAlignment, alignment := fs.Alignment != nil, newAlignment(fs)
applyProtection, protection := fs.Protection != nil, newProtection(fs)
return setCellXfs(s, fontID, numFmtID, fillID, borderID, applyAlignment, applyProtection, alignment, protection)
}

分析:

  1. parseFormatStyleSet函数用于解析单元格的格式设置和条件格式,里面对 style.CustomNumFmt 进行了条件判断:如果 style.CustomNumFmt 不为 nil 且其指向的字符串长度为0,函数返回 err 为 ErrCustomNumFmt 错误。

  2. stylesReader方法在对xl/styles.xml进行解析后将具体内容反序列化到 f.Styles 之中。
    f.Styles 为 *xlsxStyleSheet , 结构体的部分相关定义如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // NumFmts: 一个指向 xlsxNumFmts 结构体的指针,表示自定义数字格式的集合。
    type xlsxStyleSheet struct {
    // ...
    NumFmts *xlsxNumFmts `xml:"numFmts"`
    // ...
    }

    // Count:一个整数类型的字段,指示了 NumFmt 切片中元素的数量。
    // NumFmt:一个指向 xlsxNumFmt 结构体切片的指针,表示自定义数字格式的集合,切片中的每一个元素都代表一个自定义数字格式。
    type xlsxNumFmts struct {
    Count int `xml:"count,attr"`
    NumFmt []*xlsxNumFmt `xml:"numFmt"`
    }

    // NumFmtID:一个整数类型的字段,表示自定义数字格式的唯一的ID,用于在XML中引用该格式。
    // FormatCode:一个字符串类型的字段,表示自定义数字格式表达式的内容。
    // FormatCode16:。。。其实不是很懂这是啥。
    type xlsxNumFmt struct {
    NumFmtID int `xml:"numFmtId,attr"`
    FormatCode string `xml:"formatCode,attr"`
    FormatCode16 string `xml:"http://schemas.microsoft.com/office/spreadsheetml/2015/02/main formatCode16,attr,omitempty"`
    }
  3. newNumFmt函数用于在Excel的样式表(xlsxStyleSheet)中创建一个新的自定义数字格式(xlsxNumFmt)并返回其ID。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    func newNumFmt(styleSheet *xlsxStyleSheet, style *Style) int {
    // 初始化默认的自定义数字格式代码 dp 为 "0",并且设置默认的自定义数字格式ID numFmtID 为 164(货币格式的开头)。
    dp, numFmtID := "0", 164

    // 如果 style.DecimalPlaces (小数点位数)不为 nil 并且大于 0,则在 dp 中添加小数点和相应数量的 0。
    if style.DecimalPlaces != nil && *style.DecimalPlaces > 0 {
    dp += "."
    for i := 0; i < *style.DecimalPlaces; i++ {
    dp += "0"
    }
    }

    // 处理自定义数字格式
    // 如果 style.CustomNumFmt 不为 nil,则尝试获取自定义数字格式的ID。如果自定义数字格式已经存在于样式表中,返回其ID;
    // 否则,调用 setCustomNumFmt 函数来设置新的自定义数字格式,并返回其ID。
    if style.CustomNumFmt != nil {
    if customNumFmtID := getCustomNumFmtID(styleSheet, style); customNumFmtID != -1 {
    return customNumFmtID
    }
    return setCustomNumFmt(styleSheet, style)
    }

    // 处理使用内置的数字格式
    // 检查是否已经有一个内置的数字格式与 style.NumFmt 匹配,有就直接返回
    if _, ok := builtInNumFmt[style.NumFmt]; !ok {
    // 不匹配尝试获取货币格式
    fc, currency := currencyNumFmt[style.NumFmt]

    if !currency { // 不是货币格式就去看是不是地区时间数字格式
    return setLangNumFmt(style) // 是直接返回 style.NumFmt,不是返回 0
    }

    // 是货币格式进行以下处理
    // 如果 style.DecimalPlaces 不为 nil,则替换内置货币格式中的 "0.00" 为之前计算的 dp。
    if style.DecimalPlaces != nil {
    fc = strings.ReplaceAll(fc, "0.00", dp)
    }

    // 如果 style.NegRed 为 true,则在格式代码后面添加红色负数的格式。
    if style.NegRed {
    fc = fc + ";[Red]" + fc
    }

    // 如果 styleSheet.NumFmts 为 nil,则初始化一个新的 xlsxNumFmts 结构体。
    // 否则,获取最后一个 styleSheet.NumFmts.NumFmt 的 ID,并为新格式分配一个比最后一个 ID 大的 ID。
    if styleSheet.NumFmts == nil {
    styleSheet.NumFmts = &xlsxNumFmts{NumFmt: []*xlsxNumFmt{}}
    } else {
    numFmtID = styleSheet.NumFmts.NumFmt[len(styleSheet.NumFmts.NumFmt)-1].NumFmtID + 1
    }

    // 创建一个新的 xlsxNumFmts 结构体并追加到 styleSheet.NumFmts.NumFmt 中
    styleSheet.NumFmts.NumFmt = append(styleSheet.NumFmts.NumFmt, &xlsxNumFmt{
    FormatCode: fc, NumFmtID: numFmtID,
    })

    styleSheet.NumFmts.Count++
    return numFmtID
    }
    return style.NumFmt
    }

    func getCustomNumFmtID(styleSheet *xlsxStyleSheet, style *Style) (customNumFmtID int) {
    customNumFmtID = -1
    if styleSheet.NumFmts == nil {
    return
    }
    for _, numFmt := range styleSheet.NumFmts.NumFmt {
    // 如果 styleSheet.NumFmts.NumFmt 中已经有了相同的数字格式,返回这个的 ID
    if style.CustomNumFmt != nil && numFmt.FormatCode == *style.CustomNumFmt {
    customNumFmtID = numFmt.NumFmtID
    return
    }
    }
    return
    }

    func setCustomNumFmt(styleSheet *xlsxStyleSheet, style *Style) int {
    nf := xlsxNumFmt{NumFmtID: 163, FormatCode: *style.CustomNumFmt}
    if styleSheet.NumFmts == nil {
    styleSheet.NumFmts = &xlsxNumFmts{}
    }

    // 找到 styleSheet.NumFmts.NumFmt中所有 ID 的最大值 , 让 nf 的 ID 成为最大值
    for _, numFmt := range styleSheet.NumFmts.NumFmt {
    if numFmt != nil && nf.NumFmtID < numFmt.NumFmtID {
    nf.NumFmtID = numFmt.NumFmtID
    }
    }
    nf.NumFmtID++

    // 追加元素
    styleSheet.NumFmts.NumFmt = append(styleSheet.NumFmts.NumFmt, &nf)
    styleSheet.NumFmts.Count = len(styleSheet.NumFmts.NumFmt)
    return nf.NumFmtID
    }

    // 查看是不是地区时间数字格式
    func setLangNumFmt(style *Style) int {
    if isLangNumFmt(style.NumFmt) {
    return style.NumFmt
    }
    return 0
    }

    func isLangNumFmt(ID int) bool {
    return (27 <= ID && ID <= 36) || (50 <= ID && ID <= 62) || (67 <= ID && ID <= 81)
    }
  4. setCellXfs设置描述单元格的所有格式到 Xf 结构体中,然后style.CellXfs.Xf = append(style.CellXfs.Xf, xf)进行内容追加,最后return style.CellXfs.Count - 1返回 style 的 ID 。

其他函数关系就不太大了,主要还是 NewStyle 。

excelize 中 numfmt 相关的分析

前置知识 - nfp 的介绍

nfp就是 numfmt parser ,这个包被大量用于 numfmt 之中用于数字解析,这边就简单介绍一下nfp吧。

主要的方法

很显然,最主要的方法肯定就是负责解析的方法,就是下面这个:

1
2
3
4
5
6
func (ps *Parser) Parse(numFmt string) []Section {
ps.NumFmt = strings.TrimSpace(numFmt)
ps.Runes = []rune(ps.NumFmt)
ps.Tokens = ps.getTokens()
return ps.Tokens.Sections
}

而里面的getTokens方法就是这个解析方法的逻辑主体。

具体原理分析

nfp的解析,就是根据传入的numFmt数字格式代码将其按”;”拆分为 4 个Section(也就是前文说的四个区段)分别进行解析。Section是由TypeItems两部分组成,Type就对应不同区段的类型,Items就是存储解析numFmt参数得到的结果集。Section结构体定义如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
type Section struct {
Type string
Items []Token
}
```
其中`Items`由`Token`结构体构成,该结构体有三个字段:`TValue`,`TType`,`Parts`,如下所示:
```go
type Token struct {
TValue string
TType string
Parts []Part
}

TValue对应的是Token的值,TType就是这个Token的类型,Part则是对Token的子部分的一个映射,由TokenValue构成。
Part结构体定义如下:

1
2
3
4
type Part struct {
Token Token
Value string
}

解析结果示例

numfmt 对应传入的参数,result 对应最终解析出来的结果。

  1. 示例一:
    1
    2
    3
    4
    5
    6
    {
    numfmt: "0%",
    result: "[{Positive [{0 ZeroPlaceHolder []} {% Percent []}]}]",
    }

    解析结果中, `Positive`就是 Type ,`[{0 ZeroPlaceHolder []} {% Percent []}]`就是 Items . `{0 ZeroPlaceHolder []}`与`{% Percent []}`就是解析出来的两个 Token .用第一个 Token 进行分析,其中`0`为 TValue ,`ZeroPlaceHolder`为 TType ,`[]`为 Parts,不过这里的 Parts 为空.
  2. 示例二:
    1
    2
    3
    4
    5
    6
    {
    numfmt: `[DBNum1][$-zh-CN]h"时"mm"分";"====="@@@"--"@"----"`,
    result: "[{Positive [{[DBNum1] SwitchArgument []} {[$-zh-CN] CurrencyLanguage [{{zh-CN LanguageInfo []} }]} {h DateTimes []} {时 Literal []} {mm DateTimes []} {分 Literal []}]} {Text [{===== Literal []} {@ TextPlaceHolder []} {@ TextPlaceHolder []} {@ TextPlaceHolder []} {-- Literal []} {@ TextPlaceHolder []} {---- Literal []}]}]",
    }

    具体的结果构成分析参照示例一,这边主要是看一下 Parts 这一部分, 如:`[$-zh-CN]`对应的就是`{[$-zh-CN] CurrencyLanguage [{{zh-CN LanguageInfo []}}]}`,`[{{zh-CN LanguageInfo []} }]`就是 Parts 的存储内容。所以其实 Parts 就是负责存储对数字格式表达式中的条件格式的解析结果。
    更多 Parts 的示例如下:
    1
    2
    3
    [<>50] - {<>50 Condition [{{<> Operator []} } {{50 Operand []} }]}
    [$ANG] - {[$ANG] CurrencyLanguage [{{ANG CurrencyString []} }]}
    [$RD$-1C0A] - {[$RD$-1C0A] CurrencyLanguage [{{RD$ CurrencyString []} } {{1C0A LanguageInfo []} }]}

函数的部分关系图示

如图:
whiteboard_exported_image 1.png
whiteboard_exported_image0.png

源码分析

这部分主要是集中在 numfmt 的 format 函数的分析,结合 demo 自上而下走一遍。
demo 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func main() {
f := excelize.NewFile(excelize.Options{
CultureInfo: excelize.CultureNameEnUS,
})

// 使用内置的单元格格式
style2, err := f.NewStyle(&excelize.Style{
NumFmt: 27,
})
if err != nil {
fmt.Println(err)
return
}
f.SetCellStyle("Sheet1", "A2", "A2", style2)
f.SetCellValue("Sheet1", "A2", 45405)

a, err := f.GetCellValue("Sheet1", "A2")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(a) // 输出 4/23/24

if err := f.SaveAs("./Book1.xlsx"); err != nil {
fmt.Println(err)
}
}

GetCellValue

1
2
3
4
5
6
7
8
9
10
func (f *File) GetCellValue(sheet, cell string, opts ...Options) (string, error) {
return f.getCellStringFunc(sheet, cell, func(x *xlsxWorksheet, c *xlsxC) (string, bool, error) {
sst, err := f.sharedStringsReader()
if err != nil {
return "", true, err
}
val, err := c.getValueFrom(f, sst, f.getOptions(opts...).RawCellValue)
return val, true, err
})
}

getCellStringFunc主要是从工作流中获取工作表信息,从中提取指定的目标单元格的行列相关信息,执行传入的函数。

getValueFrom

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func (c *xlsxC) getValueFrom(f *File, d *xlsxSST, raw bool) (string, error) {
switch c.T {
// ...
default:
if isNum, precision, decimal := isNumeric(c.V); isNum && !raw {
if precision > 15 {
c.V = strconv.FormatFloat(decimal, 'G', 15, 64)
} else {
c.V = strconv.FormatFloat(decimal, 'f', -1, 64)
}
}
return f.formattedValue(c, raw, CellTypeNumber)
}
}

getValueFrom判断传入的值的类型去选择不同的处理方法,最终返回一个值。
这边走的是 default ,调用 formattedValue 方法。

formattedValue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
func (f *File) formattedValue(c *xlsxC, raw bool, cellType CellType) (string, error) {
// ...
styleSheet, err := f.stylesReader()
if err != nil {
return c.V, err
}
// ...
var numFmtID int
if styleSheet.CellXfs.Xf[c.S].NumFmtID != nil {
numFmtID = *styleSheet.CellXfs.Xf[c.S].NumFmtID
}
date1904 := false
wb, err := f.workbookReader()
if err != nil {
return c.V, err
}
if wb != nil && wb.WorkbookPr != nil {
date1904 = wb.WorkbookPr.Date1904
}
// 如果是自定义数字格式表达式的
if fmtCode, ok := styleSheet.getCustomNumFmtCode(numFmtID); ok {
return format(c.V, fmtCode, date1904, cellType, f.options), err
}
// 使用内置的数字格式表达式的
if fmtCode, ok := f.getBuiltInNumFmtCode(numFmtID); ok {
return f.applyBuiltInNumFmt(c, fmtCode, numFmtID, date1904, cellType), err
}
return c.V, err
}

// 从 NumFmt 切片中获取对应的数字格式表达式
func (ss *xlsxStyleSheet) getCustomNumFmtCode(numFmtID int) (string, bool) {
if ss.NumFmts == nil {
return "", false
}
for _, xlsxFmt := range ss.NumFmts.NumFmt {
if xlsxFmt.NumFmtID == numFmtID {
if xlsxFmt.FormatCode16 != "" {
return xlsxFmt.FormatCode16, true
}
return xlsxFmt.FormatCode, true
}
}
return "", false
}

// 获取内置定义的数字格式表达式(根据地区进行区分)
func (f *File) getBuiltInNumFmtCode(numFmtID int) (string, bool) {
if fmtCode, ok := builtInNumFmt[numFmtID]; ok {
return fmtCode, true
}
if isLangNumFmt(numFmtID) {
if f.options.CultureInfo == CultureNameEnUS {
return f.langNumFmtFuncEnUS(numFmtID), true
}
if f.options.CultureInfo == CultureNameZhCN {
return f.langNumFmtFuncZhCN(numFmtID), true
}
}
return "", false
}

// 对部分 numfmtID 进行处理
func (f *File) applyBuiltInNumFmt(c *xlsxC, fmtCode string, numFmtID int, date1904 bool, cellType CellType) string {
if f.options != nil && f.options.ShortDatePattern != "" {
if numFmtID == 14 {
fmtCode = f.options.ShortDatePattern
}
if numFmtID == 22 {
fmtCode = fmt.Sprintf("%s hh:mm", f.options.ShortDatePattern)
}
}
return format(c.V, fmtCode, date1904, cellType, f.options)
}

formattedValue提供了一个函数用于在格式化后返回一个值。通过查找对应单元格的 numfmtID 去获取对应单元格的数字格式表达式并进行数值的处理。

format

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
func format(value, numFmt string, date1904 bool, cellType CellType, opts *Options) string {
p := nfp.NumberFormatParser() // NumberFormatParser提供了将Excel数字格式解析为符号流的函数。
// 将传入的 numfmt 进行解析
nf := numberFormat{opts: opts, section: p.Parse(numFmt), value: value, date1904: date1904, cellType: cellType}
// 判断传入的 value 交给哪个部分的 section 进行解析
// 不是数字和日期就定位为text, nf.number就是将value转为float64
nf.number, nf.valueSectionType = nf.getValueSectionType(value)
nf.prepareNumberic(value)
// 开始读取section
for i, section := range nf.section {
nf.sectionIdx = i
if section.Type != nf.valueSectionType {
continue
}
if nf.isNumeric {
switch section.Type {
case nfp.TokenSectionPositive:
return nf.alignmentHandler(nf.positiveHandler())
case nfp.TokenSectionNegative:
return nf.alignmentHandler(nf.negativeHandler())
default:
return nf.alignmentHandler(nf.zeroHandler())
}
}
return nf.alignmentHandler(nf.textHandler())
}
return value
}

format提供了一个函数来返回一个通过数字格式表达式解析的结果,如果给定的数字格式不被支持,这将返回原始的单元格值。
demo 的示例进入positiveHandler

positiveHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
func (nf *numberFormat) positiveHandler() string {
var fmtNum bool
for _, token := range nf.section[nf.sectionIdx].Items { // 获取token
if token.TType == nfp.TokenTypeGeneral {
return strconv.FormatFloat(nf.number, 'G', 10, 64)
}
// 查看token是否是数字类型
if inStrSlice(supportedNumberTokenTypes, token.TType, true) != -1 {
fmtNum = true
}
// 查看token是否是时间类型
if inStrSlice(supportedDateTimeTokenTypes, token.TType, true) != -1 {
// 遇到了数字类型的 token 或者 nf.number 是负数,直接返回单元格的值
if fmtNum || nf.number < 0 {
return nf.value
}
var useDateTimeTokens bool
for _, token := range nf.section[nf.sectionIdx].Items {
// 如果 token 的类型在支持的时间类型 token 列表中
if inStrSlice(supportedDateTimeTokenTypes, token.TType, false) != -1 {
// 如果之前使用了时间类型的 token 并且 nf.useMillisecond 为 true,直接返回单元格的值
if useDateTimeTokens && nf.useMillisecond {
return nf.value
}
useDateTimeTokens = true
}
// 如果 token 的类型在支持的数字类型 token 列表中,直接返回单元格的值
if inStrSlice(supportedNumberTokenTypes, token.TType, false) != -1 {
if token.TType == nfp.TokenTypeZeroPlaceHolder {
nf.useMillisecond = true
continue
}
return nf.value
}
}
// dateTimeHandler处理正数的数据和时间数字格式表达式
return nf.dateTimeHandler()
}
}
// numberHandler处理正数值的数字格式表达式
return nf.numberHandler()
}

这里传入的 section 的具体内容如下:

1
2
3
{
"[{Positive [{M DateTimes []} {/ Literal []} {d DateTimes []} {/ Literal []} {yy DateTimes []}]}]"
}

所以这边进入dateTimeHandler进行下一步的处理。

dateTimeHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
func (nf *numberFormat) dateTimeHandler() string {
// timeFromExcelTime用于将excelTime的表示形式(存储为浮点数)转换为time.Time。
nf.t, nf.hours, nf.seconds = timeFromExcelTime(nf.number, nf.date1904), false, false
if !nf.useMillisecond {
nf.t = nf.t.Add(time.Duration(math.Round(float64(nf.t.Nanosecond())/1e9)) * time.Second)
}
for i, token := range nf.section[nf.sectionIdx].Items {
// 判断是否是货币格式
if token.TType == nfp.TokenTypeCurrencyLanguage {
if changeNumFmtCode, err := nf.currencyLanguageHandler(token); err != nil || changeNumFmtCode {
return nf.value
} // 不支持的类型,转换后的日期时间类型都直接 return
// 只有是货币类型才到这
nf.result += nf.currencyString
}
// 下面都是各种 token 的判断
if token.TType == nfp.TokenTypeDateTimes {
nf.dateTimesHandler(i, token)
}
if token.TType == nfp.TokenTypeElapsedDateTimes {
nf.elapsedDateTimesHandler(token)
}
if token.TType == nfp.TokenTypeLiteral {
nf.result += token.TValue
continue
}
if token.TType == nfp.TokenTypeDecimalPoint {
nf.result += "."
}
if token.TType == nfp.TokenTypeSwitchArgument {
nf.switchArgument = token.TValue
}
if token.TType == nfp.TokenTypeZeroPlaceHolder {
zeroHolderLen := len(token.TValue)
if zeroHolderLen > 3 {
zeroHolderLen = 3
}
nf.result += fmt.Sprintf("%03d", nf.t.Nanosecond()/1e6)[:zeroHolderLen]
}
}
return nf.printSwitchArgument(nf.result)
}

因为 token 只由 DateTimes 与 Literal 两个类型构成,所以这边只调用了dateTimesHandler

dateTimesHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
func (nf *numberFormat) dateTimesHandler(i int, token nfp.Token) {
if idx := inStrSlice(nfp.AmPm, strings.ToUpper(token.TValue), false); idx != -1 {
if nf.ap == "" {
nextHours := nf.hoursNext(i)
aps := strings.Split(nf.localAmPm(token.TValue), "/")
nf.ap = aps[0]
if nextHours >= 12 {
nf.ap = aps[1]
}
}
nf.result += nf.ap
return
}
// 对月份的处理
if strings.Contains(strings.ToUpper(token.TValue), "M") {
l := len(token.TValue)
if l == 1 && nf.isMonthToken(i) {
nf.result += strconv.Itoa(int(nf.t.Month()))
return
}
if l == 2 && nf.isMonthToken(i) {
nf.result += fmt.Sprintf("%02d", int(nf.t.Month()))
return
}
if l == 3 {
nf.result += nf.localMonthsName(3)
return
}
if l == 4 || l > 5 {
nf.result += nf.localMonthsName(4)
return
}
if l == 5 {
nf.result += nf.localMonthsName(5)
return
}
}
nf.yearsHandler(token)
nf.daysHandler(token)
nf.hoursHandler(i, token)
nf.minutesHandler(token)
nf.secondsHandler(token)
}

// 对日的处理
func (nf *numberFormat) daysHandler(token nfp.Token) {
info, l := supportedLanguageInfo[nf.localCode], len(token.TValue)
weekdayNames, weekdayNamesAbbr := info.weekdayNames, info.weekdayNamesAbbr
if len(weekdayNames) != 7 {
weekdayNames = weekdayNamesEnglish
}
if len(weekdayNamesAbbr) != 7 {
weekdayNamesAbbr = weekdayNamesEnglishAbbr
}
if strings.Contains(strings.ToUpper(token.TValue), "A") {
if l == 3 {
nf.result += weekdayNamesAbbr[nf.t.Weekday()]
}
if l > 3 {
nf.result += weekdayNames[nf.t.Weekday()]
}
return
}
if strings.Contains(strings.ToUpper(token.TValue), "D") {
switch l {
case 1:
nf.result += strconv.Itoa(nf.t.Day())
case 2:
nf.result += fmt.Sprintf("%02d", nf.t.Day())
case 3:
nf.result += weekdayNamesAbbr[nf.t.Weekday()]
default:
nf.result += weekdayNames[nf.t.Weekday()]
}
}
}

// 对年的处理
func (nf *numberFormat) yearsHandler(token nfp.Token) {
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") {
i, year := eraYear(nf.t)
if year == -1 {
return
}
nf.useGannen = supportedLanguageInfo[nf.localCode].useGannen
switch len(token.TValue) {
case 1:
nf.useGannen = false
nf.result += japaneseEraSymbols[i]
case 2:
nf.result += japaneseEraNames[i][:3]
default:
nf.result += japaneseEraNames[i]
}
return
}
if strings.Contains(strings.ToUpper(token.TValue), "E") {
_, year := eraYear(nf.t)
if year == -1 {
nf.result += strconv.Itoa(nf.t.Year())
return
}
if year == 1 && nf.useGannen {
nf.result += "\u5143"
return
}
if len(token.TValue) == 1 && !nf.useGannen {
nf.result += strconv.Itoa(year)
return
}
if len(token.TValue) == 2 {
nf.result += fmt.Sprintf("%02d", year)
}
}
}

dateTimesHandler按照 am/pm 、月、年、日、时、分、秒将 token 进行区分处理
这边只涉及了月、日、年。
通过将解析的结果按顺序进行拼接,得到的 result 就是最后的结果,返回即可。

参考链接

  1. https://zhuanlan.zhihu.com/p/600048173