excelize/numfmt.go

357 lines
10 KiB
Go

// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
// Package excelize providing a set of functions that allow you to write to
// and read from XLSX / XLSM / XLTM files. Supports reading and writing
// spreadsheet documents generated by Microsoft Excel™ 2007 and later. Supports
// complex components by high compatibility, and provided streaming API for
// generating or reading data from a worksheet with huge amounts of data. This
// library needs Go version 1.15 or later.
package excelize
import (
"fmt"
"strconv"
"strings"
"time"
"github.com/xuri/nfp"
)
// supportedTokenTypes list the supported number format token types currently.
var supportedTokenTypes = []string{
nfp.TokenTypeCurrencyLanguage,
nfp.TokenTypeDateTimes,
nfp.TokenTypeElapsedDateTimes,
nfp.TokenTypeGeneral,
nfp.TokenTypeLiteral,
nfp.TokenSubTypeLanguageInfo,
}
// numberFormat directly maps the number format parser runtime required
// fields.
type numberFormat struct {
section []nfp.Section
t time.Time
sectionIdx int
isNumberic, hours, seconds bool
number float64
ap, afterPoint, beforePoint, localCode, result, value, valueSectionType string
}
// prepareNumberic split the number into two before and after parts by a
// decimal point.
func (nf *numberFormat) prepareNumberic(value string) {
prec := 0
if nf.isNumberic, prec = isNumeric(value); !nf.isNumberic {
return
}
nf.beforePoint, nf.afterPoint = value[:len(value)-prec-1], value[len(value)-prec:]
}
// format provides a function to return a string parse by number format
// expression. If the given number format is not supported, this will return
// the original cell value.
func format(value, numFmt string) string {
p := nfp.NumberFormatParser()
nf := numberFormat{section: p.Parse(numFmt), value: value}
nf.number, nf.valueSectionType = nf.getValueSectionType(value)
nf.prepareNumberic(value)
for i, section := range nf.section {
nf.sectionIdx = i
if section.Type != nf.valueSectionType {
continue
}
switch section.Type {
case nfp.TokenSectionPositive:
return nf.positiveHandler()
case nfp.TokenSectionNegative:
return nf.negativeHandler()
case nfp.TokenSectionZero:
return nf.zeroHandler()
default:
return nf.textHandler()
}
}
return value
}
// positiveHandler will be handling positive selection for a number format
// expression.
func (nf *numberFormat) positiveHandler() (result string) {
nf.t, nf.hours, nf.seconds = timeFromExcelTime(nf.number, false), false, false
for i, token := range nf.section[nf.sectionIdx].Items {
if inStrSlice(supportedTokenTypes, token.TType, true) == -1 || token.TType == nfp.TokenTypeGeneral {
result = fmt.Sprint(nf.number)
return
}
if token.TType == nfp.TokenTypeCurrencyLanguage {
if err := nf.currencyLanguageHandler(i, token); err != nil {
result = fmt.Sprint(nf.number)
return
}
}
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
}
}
result = nf.result
return
}
// currencyLanguageHandler will be handling currency and language types tokens for a number
// format expression.
func (nf *numberFormat) currencyLanguageHandler(i int, token nfp.Token) (err error) {
for _, part := range token.Parts {
if inStrSlice(supportedTokenTypes, part.Token.TType, true) == -1 {
err = ErrUnsupportedNumberFormat
return
}
if nf.localCode = part.Token.TValue; nf.localCode != "409" {
err = ErrUnsupportedNumberFormat
return
}
}
return
}
// dateTimesHandler will be handling date and times types tokens for a number
// format expression.
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(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.hours && !nf.secondsNext(i) {
nf.result += strconv.Itoa(int(nf.t.Month()))
return
}
if l == 2 && !nf.hours && !nf.secondsNext(i) {
nf.result += fmt.Sprintf("%02d", int(nf.t.Month()))
return
}
if l == 3 {
nf.result += nf.t.Month().String()[:3]
return
}
if l == 4 || l > 5 {
nf.result += nf.t.Month().String()
return
}
if l == 5 {
nf.result += nf.t.Month().String()[:1]
return
}
}
nf.yearsHandler(i, token)
nf.daysHandler(i, token)
nf.hoursHandler(i, token)
nf.minutesHandler(token)
nf.secondsHandler(token)
}
// yearsHandler will be handling years in the date and times types tokens for a
// number format expression.
func (nf *numberFormat) yearsHandler(i int, 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 {
nf.result += strconv.Itoa(nf.t.Year())
return
}
}
// daysHandler will be handling days in the date and times types tokens for a
// number format expression.
func (nf *numberFormat) daysHandler(i int, token nfp.Token) {
if strings.Contains(strings.ToUpper(token.TValue), "D") {
switch len(token.TValue) {
case 1:
nf.result += strconv.Itoa(nf.t.Day())
return
case 2:
nf.result += fmt.Sprintf("%02d", nf.t.Day())
return
case 3:
nf.result += nf.t.Weekday().String()[:3]
return
default:
nf.result += nf.t.Weekday().String()
return
}
}
}
// hoursHandler will be handling hours in the date and times types tokens for a
// number format expression.
func (nf *numberFormat) hoursHandler(i int, token nfp.Token) {
nf.hours = strings.Contains(strings.ToUpper(token.TValue), "H")
if nf.hours {
h := nf.t.Hour()
ap, ok := nf.apNext(i)
if ok {
nf.ap = ap[0]
if h > 12 {
h -= 12
nf.ap = ap[1]
}
}
if nf.ap != "" && nf.hoursNext(i) == -1 && h > 12 {
h -= 12
}
switch len(token.TValue) {
case 1:
nf.result += strconv.Itoa(h)
return
default:
nf.result += fmt.Sprintf("%02d", h)
return
}
}
}
// minutesHandler will be handling minutes in the date and times types tokens
// for a number format expression.
func (nf *numberFormat) minutesHandler(token nfp.Token) {
if strings.Contains(strings.ToUpper(token.TValue), "M") {
nf.hours = false
switch len(token.TValue) {
case 1:
nf.result += strconv.Itoa(nf.t.Minute())
return
default:
nf.result += fmt.Sprintf("%02d", nf.t.Minute())
return
}
}
}
// secondsHandler will be handling seconds in the date and times types tokens
// for a number format expression.
func (nf *numberFormat) secondsHandler(token nfp.Token) {
nf.seconds = strings.Contains(strings.ToUpper(token.TValue), "S")
if nf.seconds {
switch len(token.TValue) {
case 1:
nf.result += strconv.Itoa(nf.t.Second())
return
default:
nf.result += fmt.Sprintf("%02d", nf.t.Second())
return
}
}
}
// elapsedDateTimesHandler will be handling elapsed date and times types tokens
// for a number format expression.
func (nf *numberFormat) elapsedDateTimesHandler(token nfp.Token) {
if strings.Contains(strings.ToUpper(token.TValue), "H") {
nf.result += fmt.Sprintf("%.f", nf.t.Sub(excel1900Epoc).Hours())
return
}
if strings.Contains(strings.ToUpper(token.TValue), "M") {
nf.result += fmt.Sprintf("%.f", nf.t.Sub(excel1900Epoc).Minutes())
return
}
if strings.Contains(strings.ToUpper(token.TValue), "S") {
nf.result += fmt.Sprintf("%.f", nf.t.Sub(excel1900Epoc).Seconds())
return
}
}
// hoursNext detects if a token of type hours exists after a given tokens list.
func (nf *numberFormat) hoursNext(i int) int {
tokens := nf.section[nf.sectionIdx].Items
for idx := i + 1; idx < len(tokens); idx++ {
if tokens[idx].TType == nfp.TokenTypeDateTimes {
if strings.Contains(strings.ToUpper(tokens[idx].TValue), "H") {
t := timeFromExcelTime(nf.number, false)
return t.Hour()
}
}
}
return -1
}
// apNext detects if a token of type AM/PM exists after a given tokens list.
func (nf *numberFormat) apNext(i int) ([]string, bool) {
tokens := nf.section[nf.sectionIdx].Items
for idx := i + 1; idx < len(tokens); idx++ {
if tokens[idx].TType == nfp.TokenTypeDateTimes {
if strings.Contains(strings.ToUpper(tokens[idx].TValue), "H") {
return nil, false
}
if i := inStrSlice(nfp.AmPm, tokens[idx].TValue, false); i != -1 {
return strings.Split(tokens[idx].TValue, "/"), true
}
}
}
return nil, false
}
// secondsNext detects if a token of type seconds exists after a given tokens
// list.
func (nf *numberFormat) secondsNext(i int) bool {
tokens := nf.section[nf.sectionIdx].Items
for idx := i + 1; idx < len(tokens); idx++ {
if tokens[idx].TType == nfp.TokenTypeDateTimes {
return strings.Contains(strings.ToUpper(tokens[idx].TValue), "S")
}
}
return false
}
// negativeHandler will be handling negative selection for a number format
// expression.
func (nf *numberFormat) negativeHandler() string {
return fmt.Sprint(nf.number)
}
// zeroHandler will be handling zero selection for a number format expression.
func (nf *numberFormat) zeroHandler() string {
return fmt.Sprint(nf.number)
}
// textHandler will be handling text selection for a number format expression.
func (nf *numberFormat) textHandler() string {
return fmt.Sprint(nf.value)
}
// getValueSectionType returns its applicable number format expression section
// based on the given value.
func (nf *numberFormat) getValueSectionType(value string) (float64, string) {
number, err := strconv.ParseFloat(value, 64)
if err != nil {
return number, nfp.TokenSectionText
}
if number > 0 {
return number, nfp.TokenSectionPositive
}
if number < 0 {
return number, nfp.TokenSectionNegative
}
return number, nfp.TokenSectionZero
}