mirror of
https://github.com/unidoc/unioffice.git
synced 2025-04-29 13:49:10 +08:00

* MATCH, IFS, MAXA, MINA * OFFSET fixed * ISBLANK, ISERR, ISERROR, ISEVEN ,ISFORMULA, ISNONTEXT, ISNUMBER, ISODD, ISTEXT * ISLEAPYEAR, ISLOGICAL, ISNA, ISREF * FIND, FINDB * SEARCH, SEARCHB * CONCAT, CONCATENATE * YEAR, YEARFRAC * CONCAT is fixed, now TRUE and FALSE are concatenated instead of 1 and 0 in case of boolean results * NOW, TODAY, TIME, TIMEVALUE * DATE * DATEDIF
723 lines
17 KiB
Go
723 lines
17 KiB
Go
// Copyright 2017 FoxyUtils ehf. All rights reserved.
|
|
//
|
|
// Use of this source code is governed by the terms of the Affero GNU General
|
|
// Public License version 3.0 as published by the Free Software Foundation and
|
|
// appearing in the file LICENSE included in the packaging of this file. A
|
|
// commercial license can be purchased on https://unidoc.io.
|
|
|
|
package format
|
|
|
|
import (
|
|
"bytes"
|
|
"math"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/unidoc/unioffice"
|
|
)
|
|
|
|
// constants used when formatting generic values to determine when to start
|
|
// rounding
|
|
const maxGeneric = 1e11
|
|
const minGeneric = 1e-10
|
|
|
|
// Format is a parsed number format.
|
|
type Format struct {
|
|
Whole []Token
|
|
Fractional []Token
|
|
Exponent []Token
|
|
IsExponential bool
|
|
|
|
isFraction bool
|
|
isPercent bool
|
|
isGeneral bool
|
|
hasThousands bool
|
|
skipNext bool
|
|
seenDecimal bool
|
|
|
|
denom int64
|
|
denomDigits int
|
|
}
|
|
|
|
// FmtType is the type of a format token.
|
|
//go:generate stringer -type=FmtType
|
|
type FmtType byte
|
|
|
|
// Format type constants.
|
|
const (
|
|
FmtTypeLiteral FmtType = iota
|
|
FmtTypeDigit
|
|
FmtTypeDigitOpt
|
|
FmtTypeComma
|
|
FmtTypeDecimal
|
|
FmtTypePercent
|
|
FmtTypeDollar
|
|
FmtTypeDigitOptThousands
|
|
FmtTypeUnderscore
|
|
FmtTypeDate
|
|
FmtTypeTime
|
|
FmtTypeFraction
|
|
FmtTypeText
|
|
)
|
|
|
|
// Token is a format token in the Excel format string.
|
|
type Token struct {
|
|
Type FmtType
|
|
Literal byte
|
|
DateTime string
|
|
}
|
|
|
|
// AddToken adds a format token to the format.
|
|
func (f *Format) AddToken(t FmtType, l []byte) {
|
|
if f.skipNext {
|
|
f.skipNext = false
|
|
return
|
|
}
|
|
switch t {
|
|
case FmtTypeDecimal:
|
|
f.seenDecimal = true
|
|
case FmtTypeUnderscore:
|
|
f.skipNext = true
|
|
case FmtTypeText:
|
|
f.Whole = append(f.Whole, Token{Type: t})
|
|
case FmtTypeDate, FmtTypeTime:
|
|
f.Whole = append(f.Whole, Token{Type: t, DateTime: string(l)})
|
|
case FmtTypePercent:
|
|
f.isPercent = true
|
|
t = FmtTypeLiteral
|
|
l = []byte{'%'}
|
|
fallthrough
|
|
case FmtTypeDigitOpt:
|
|
fallthrough
|
|
case FmtTypeLiteral, FmtTypeDigit, FmtTypeDollar, FmtTypeComma:
|
|
if l == nil {
|
|
l = []byte{0}
|
|
}
|
|
for _, c := range l {
|
|
if f.IsExponential {
|
|
f.Exponent = append(f.Exponent, Token{Type: t, Literal: c})
|
|
} else if !f.seenDecimal {
|
|
f.Whole = append(f.Whole, Token{Type: t, Literal: c})
|
|
} else {
|
|
f.Fractional = append(f.Fractional, Token{Type: t, Literal: c})
|
|
}
|
|
}
|
|
case FmtTypeDigitOptThousands:
|
|
f.hasThousands = true
|
|
case FmtTypeFraction:
|
|
sp := strings.Split(string(l), "/")
|
|
if len(sp) == 2 {
|
|
f.isFraction = true
|
|
f.denom, _ = strconv.ParseInt(sp[1], 10, 64)
|
|
|
|
// also count the placeholder digits in the denominator for things
|
|
// like ?/? and ?/??
|
|
for _, c := range sp[1] {
|
|
if c == '?' || c == '0' {
|
|
f.denomDigits++
|
|
}
|
|
}
|
|
// TODO: if anyone cares, parse and use the numerator format.
|
|
}
|
|
default:
|
|
unioffice.Log("unsupported ph type in parse %v", t)
|
|
}
|
|
}
|
|
|
|
// Number is used to format a number with a format string. If the format
|
|
// string is empty, then General number formatting is used which attempts to mimic
|
|
// Excel's general formatting.
|
|
func Number(v float64, f string) string {
|
|
if f == "" || f == "General" {
|
|
return NumberGeneric(v)
|
|
}
|
|
fmts := Parse(f)
|
|
if len(fmts) == 1 {
|
|
return number(v, fmts[0], false)
|
|
} else if len(fmts) > 1 && v < 0 {
|
|
return number(v, fmts[1], true)
|
|
} else if len(fmts) > 2 && v == 0 {
|
|
return number(v, fmts[2], false)
|
|
}
|
|
return number(v, fmts[0], false)
|
|
}
|
|
|
|
// Value formats a value as a number or string depending on if it appears to be
|
|
// a number or string.
|
|
func Value(v string, f string) string {
|
|
if IsNumber(v) {
|
|
v, _ := strconv.ParseFloat(v, 64)
|
|
return Number(v, f)
|
|
}
|
|
return String(v, f)
|
|
}
|
|
|
|
// String returns the string formatted according to the type. In format strings
|
|
// this is the fourth item, where '@' is used as a placeholder for text.
|
|
func String(v string, f string) string {
|
|
fmts := Parse(f)
|
|
var fm Format
|
|
if len(fmts) == 1 {
|
|
fm = fmts[0]
|
|
} else if len(fmts) == 4 {
|
|
fm = fmts[3]
|
|
}
|
|
fmtHasText := false
|
|
for _, w := range fm.Whole {
|
|
if w.Type == FmtTypeText {
|
|
fmtHasText = true
|
|
}
|
|
}
|
|
// no text placeholder in the format (@), so just return the original string
|
|
if !fmtHasText {
|
|
return v
|
|
}
|
|
b := bytes.Buffer{}
|
|
for _, w := range fm.Whole {
|
|
// these appear to be the only formats that matter for string
|
|
switch w.Type {
|
|
case FmtTypeLiteral:
|
|
b.WriteByte(w.Literal)
|
|
case FmtTypeText:
|
|
b.WriteString(v)
|
|
}
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func reverse(b []byte) []byte {
|
|
for i := 0; i < len(b)/2; i++ {
|
|
j := len(b) - 1 - i
|
|
b[i], b[j] = b[j], b[i]
|
|
}
|
|
return b
|
|
}
|
|
|
|
func number(vOrig float64, f Format, isNeg bool) string {
|
|
if f.isGeneral {
|
|
return NumberGeneric(vOrig)
|
|
}
|
|
buf := make([]byte, 0, 20)
|
|
wasNeg := math.Signbit(vOrig)
|
|
v := math.Abs(vOrig)
|
|
|
|
fractNum := int64(0)
|
|
exp := int64(0)
|
|
if f.IsExponential {
|
|
for v >= 10 {
|
|
exp++
|
|
v /= 10
|
|
}
|
|
for v < 1 {
|
|
exp--
|
|
v *= 10
|
|
}
|
|
} else if f.isPercent {
|
|
// percent symbol implies multiplying the value by 100
|
|
v *= 100
|
|
} else if f.isFraction {
|
|
// if a denominator is not specified, we calculate it based on the number
|
|
// of digits we are allowed
|
|
if f.denom == 0 {
|
|
maxDenom := math.Pow(10, float64(f.denomDigits))
|
|
bestDenom, bestDenomError := 1.0, 1.0
|
|
_ = bestDenom
|
|
for i := 1.0; i < maxDenom; i++ {
|
|
_, rem := math.Modf(v * float64(i))
|
|
if rem < bestDenomError {
|
|
bestDenomError = rem
|
|
bestDenom = i
|
|
if rem == 0 {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
f.denom = int64(bestDenom)
|
|
}
|
|
|
|
fractNum = int64(v*float64(f.denom) + 0.5)
|
|
if len(f.Whole) > 0 && fractNum > f.denom {
|
|
fractNum = int64(v*float64(f.denom)) % f.denom
|
|
// subtract the portion we're displaying as a fraction
|
|
v -= float64(fractNum) / float64(f.denom)
|
|
} else {
|
|
v -= float64(fractNum) / float64(f.denom)
|
|
// This handles cases of '# ?/?' where we don't have a whole number
|
|
// portion. The logic would print a '0' for the whole number portion
|
|
// below, so we just clear f.Whole here to prevent anything from
|
|
// being printed.
|
|
if math.Abs(v) < 1 {
|
|
isOnlyOpt := true
|
|
for _, v := range f.Whole {
|
|
// don't care about '#'
|
|
if v.Type == FmtTypeDigitOpt {
|
|
continue
|
|
}
|
|
// or spaces
|
|
if v.Type == FmtTypeLiteral && v.Literal == ' ' {
|
|
continue
|
|
}
|
|
isOnlyOpt = false
|
|
}
|
|
if isOnlyOpt {
|
|
f.Whole = nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// round up now as this avoids rounding up on just the decimal portion which
|
|
// is complicated due to a possible carry over into the whole number portion
|
|
rndUp := 1
|
|
for _, ph := range f.Fractional {
|
|
if ph.Type == FmtTypeDigit || ph.Type == FmtTypeDigitOpt {
|
|
rndUp++
|
|
}
|
|
}
|
|
v += 5 * math.Pow10(-rndUp)
|
|
|
|
// split into whole and decimal portions
|
|
pre, post := math.Modf(v)
|
|
buf = append(buf, formatWholeNumber(pre, vOrig, f)...)
|
|
buf = append(buf, formatFractional(post, vOrig, f)...)
|
|
buf = append(buf, formatExponential(exp, f)...)
|
|
|
|
// fractions are special, the whole number portion is handled above (if
|
|
// len(f.whole) > 0). This is for the fractional portion, or in the case if
|
|
// no whole portion, the numerator will be greater than the denominator.
|
|
if f.isFraction {
|
|
buf = strconv.AppendInt(buf, fractNum, 10)
|
|
buf = append(buf, '/')
|
|
buf = strconv.AppendInt(buf, f.denom, 10)
|
|
}
|
|
// if the number was negative, but this isn't a 'negative' format, then
|
|
// we need to prepend a negative sign
|
|
if !isNeg && wasNeg {
|
|
return "-" + string(buf)
|
|
}
|
|
return string(buf)
|
|
}
|
|
|
|
func formatWholeNumber(pre, vOrig float64, f Format) []byte {
|
|
if len(f.Whole) == 0 {
|
|
return nil
|
|
}
|
|
epoch := time.Date(1899, 12, 30, 0, 0, 0, 0, time.UTC)
|
|
t := epoch.Add(time.Duration(vOrig * float64(24*time.Hour)))
|
|
t = asLocal(t)
|
|
|
|
raw := strconv.AppendFloat(nil, pre, 'f', -1, 64)
|
|
op := make([]byte, 0, len(raw))
|
|
consumed := 0
|
|
lastIdx := 1
|
|
lfor:
|
|
for i := len(f.Whole) - 1; i >= 0; i-- {
|
|
bidx := len(raw) - 1 - consumed
|
|
ph := f.Whole[i]
|
|
switch ph.Type {
|
|
// '0' consumes a digit or prints a '0' if there is no digit
|
|
case FmtTypeDigit:
|
|
if bidx >= 0 {
|
|
op = append(op, raw[bidx])
|
|
consumed++
|
|
lastIdx = i
|
|
} else {
|
|
op = append(op, '0')
|
|
}
|
|
// '#' consumes a digit or prints nothing
|
|
case FmtTypeDigitOpt:
|
|
if bidx >= 0 {
|
|
op = append(op, raw[bidx])
|
|
consumed++
|
|
lastIdx = i
|
|
} else {
|
|
// we don't skip everything, just #/,/?. This is used so
|
|
// that formats like (#,###) with '1' turn into '(1)' and
|
|
// not '1)'
|
|
for j := i; j >= 0; j-- {
|
|
c := f.Whole[j]
|
|
if c.Type == FmtTypeLiteral {
|
|
op = append(op, c.Literal)
|
|
}
|
|
}
|
|
break lfor
|
|
}
|
|
case FmtTypeDollar:
|
|
for i := consumed; i < len(raw); i++ {
|
|
op = append(op, raw[len(raw)-1-i])
|
|
consumed++
|
|
}
|
|
op = append(op, '$')
|
|
case FmtTypeComma:
|
|
if !f.hasThousands {
|
|
op = append(op, ',')
|
|
}
|
|
case FmtTypeLiteral:
|
|
op = append(op, ph.Literal)
|
|
case FmtTypeDate:
|
|
op = append(op, reverse(dDate(t, ph.DateTime))...)
|
|
case FmtTypeTime:
|
|
op = append(op, reverse(dTime(t, vOrig, ph.DateTime))...)
|
|
default:
|
|
unioffice.Log("unsupported type in whole %v", ph)
|
|
}
|
|
}
|
|
|
|
buf := reverse(op)
|
|
// didn't consume all of the number characters, so insert the rest where
|
|
// we were last inserting
|
|
if consumed < len(raw) && (consumed != 0 || f.seenDecimal) {
|
|
rem := len(raw) - consumed
|
|
o := make([]byte, len(buf)+rem)
|
|
copy(o, buf[0:lastIdx])
|
|
copy(o[lastIdx:], raw[0:])
|
|
copy(o[lastIdx+rem:], buf[lastIdx:])
|
|
buf = o
|
|
}
|
|
if f.hasThousands {
|
|
b := bytes.Buffer{}
|
|
nonTerm := 0
|
|
for i := len(buf) - 1; i >= 0; i-- {
|
|
if !(buf[i] >= '0' && buf[i] <= '9') {
|
|
nonTerm++
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
for i := 0; i < len(buf); i++ {
|
|
idx := (len(buf) - i - nonTerm)
|
|
if idx%3 == 0 && idx != 0 && i != 0 {
|
|
b.WriteByte(',')
|
|
}
|
|
b.WriteByte(buf[i])
|
|
|
|
}
|
|
buf = b.Bytes()
|
|
}
|
|
return buf
|
|
}
|
|
|
|
func formatFractional(post, vOrig float64, f Format) []byte {
|
|
if len(f.Fractional) == 0 {
|
|
return nil
|
|
}
|
|
|
|
raw := strconv.AppendFloat(nil, post, 'f', -1, 64)
|
|
if len(raw) > 2 {
|
|
raw = raw[2:] // skip the decimal portion (ie. '0.')
|
|
} else {
|
|
raw = nil
|
|
}
|
|
|
|
op := make([]byte, 0, len(raw))
|
|
op = append(op, '.')
|
|
consumed := 0
|
|
lforPost:
|
|
for i := 0; i < len(f.Fractional); i++ {
|
|
bidx := i
|
|
ph := f.Fractional[i]
|
|
switch ph.Type {
|
|
// '0' consumes a digit or prints a '0' if there is no digit
|
|
case FmtTypeDigit:
|
|
if bidx < len(raw) {
|
|
op = append(op, raw[bidx])
|
|
consumed++
|
|
} else {
|
|
op = append(op, '0')
|
|
}
|
|
// '#' consumes a digit or prints nothing
|
|
case FmtTypeDigitOpt:
|
|
if bidx >= 0 {
|
|
op = append(op, raw[bidx])
|
|
consumed++
|
|
} else {
|
|
break lforPost
|
|
}
|
|
case FmtTypeLiteral:
|
|
op = append(op, ph.Literal)
|
|
default:
|
|
unioffice.Log("unsupported type in fractional %v", ph)
|
|
}
|
|
}
|
|
// remaining digits are truncated
|
|
return op
|
|
}
|
|
|
|
func absi64(i int64) int64 {
|
|
if i < 0 {
|
|
return -i
|
|
}
|
|
return i
|
|
}
|
|
|
|
func formatExponential(exp int64, f Format) []byte {
|
|
if !f.IsExponential || len(f.Exponent) == 0 {
|
|
return nil
|
|
}
|
|
|
|
raw := strconv.AppendInt(nil, absi64(exp), 10)
|
|
|
|
op := make([]byte, 0, len(raw)+2)
|
|
op = append(op, 'E')
|
|
if exp >= 0 {
|
|
op = append(op, '+')
|
|
} else {
|
|
op = append(op, '-')
|
|
exp *= -1
|
|
}
|
|
consumed := 0
|
|
lexfor:
|
|
for i := len(f.Exponent) - 1; i >= 0; i-- {
|
|
bidx := len(raw) - 1 - consumed
|
|
ph := f.Exponent[i]
|
|
switch ph.Type {
|
|
// '0' consumes a digit or prints a '0' if there is no digit
|
|
case FmtTypeDigit:
|
|
if bidx >= 0 {
|
|
op = append(op, raw[bidx])
|
|
consumed++
|
|
} else {
|
|
op = append(op, '0')
|
|
}
|
|
// '#' consumes a digit or prints nothing
|
|
case FmtTypeDigitOpt:
|
|
if bidx >= 0 {
|
|
op = append(op, raw[bidx])
|
|
consumed++
|
|
} else {
|
|
for j := i; j >= 0; j-- {
|
|
c := f.Exponent[j]
|
|
if c.Type == FmtTypeLiteral {
|
|
op = append(op, c.Literal)
|
|
}
|
|
}
|
|
break lexfor
|
|
}
|
|
case FmtTypeLiteral:
|
|
op = append(op, ph.Literal)
|
|
default:
|
|
unioffice.Log("unsupported type in exp %v", ph)
|
|
}
|
|
}
|
|
// remaining non-consumed digits in the exponent
|
|
if consumed < len(raw) {
|
|
op = append(op, raw[len(raw)-consumed-1:consumed-1]...)
|
|
}
|
|
|
|
reverse(op[2:])
|
|
return op
|
|
}
|
|
|
|
// NumberGeneric formats the number with the generic format which attemps to
|
|
// mimic Excel's general formatting.
|
|
func NumberGeneric(v float64) string {
|
|
if math.Abs(v) >= maxGeneric || math.Abs(v) <= minGeneric && v != 0 {
|
|
return formatExpNumberGeneric(v)
|
|
}
|
|
|
|
b := make([]byte, 0, 15)
|
|
b = strconv.AppendFloat(b, v, 'f', -1, 64)
|
|
if len(b) > 11 {
|
|
ntlDigit := b[11] - '0'
|
|
// if after truncating, do we need to round?
|
|
if ntlDigit >= 5 && ntlDigit <= 9 {
|
|
b[10]++
|
|
b = b[0:11]
|
|
b = performCarries(b)
|
|
}
|
|
b = b[0:11]
|
|
} else if len(b) == 11 {
|
|
// Excel rounds up a series of consecutive 9's if the total length is 11 digits
|
|
if b[len(b)-1] == '9' {
|
|
b[len(b)-1]++
|
|
b = performCarries(b)
|
|
}
|
|
}
|
|
|
|
b = trimTrailingZeros(b)
|
|
return string(b)
|
|
}
|
|
|
|
func formatExpNumberGeneric(v float64) string {
|
|
s1 := strconv.FormatFloat(v, 'E', -1, 64)
|
|
s2 := strconv.FormatFloat(v, 'E', 5, 64)
|
|
if len(s1) < len(s2) {
|
|
return strconv.FormatFloat(v, 'E', 2, 64)
|
|
}
|
|
return s2
|
|
}
|
|
|
|
// trim any trailing zeros for numbers like '1.23000000' => '1.23' while not
|
|
// triming zeros on things like '10000'
|
|
func trimTrailingZeros(b []byte) []byte {
|
|
end := len(b)
|
|
sawDecimal := false
|
|
sawDigit := false
|
|
for i := len(b) - 1; i >= 0; i-- {
|
|
if b[i] == '0' && !sawDigit && !sawDecimal {
|
|
end = i
|
|
} else if b[i] == '.' {
|
|
sawDecimal = true
|
|
} else {
|
|
sawDigit = true
|
|
}
|
|
}
|
|
if sawDecimal && sawDigit {
|
|
if b[end-1] == '.' {
|
|
end--
|
|
}
|
|
return b[0:end]
|
|
}
|
|
return b
|
|
}
|
|
|
|
func performCarries(b []byte) []byte {
|
|
// perform any required carries caused by rounding
|
|
for i := len(b) - 1; i > 0; i-- {
|
|
if b[i] == '9'+1 {
|
|
b[i] = '0'
|
|
if b[i-1] == '.' {
|
|
i--
|
|
}
|
|
b[i-1]++
|
|
}
|
|
}
|
|
// perform carry on initial digit by prepending a 1
|
|
if b[0] == '9'+1 {
|
|
b[0] = '0'
|
|
copy(b[1:], b[0:])
|
|
b[0] = '1'
|
|
}
|
|
return b
|
|
}
|
|
|
|
// dDate formats a time with an Excel format date string.
|
|
func dDate(t time.Time, f string) []byte {
|
|
ret := []byte{}
|
|
beg := 0
|
|
for i := 0; i < len(f); i++ {
|
|
var s string
|
|
// split on '/'
|
|
if f[i] == '/' {
|
|
s = string(f[beg:i])
|
|
beg = i + 1
|
|
} else if i == len(f)-1 {
|
|
s = string(f[beg : i+1])
|
|
} else {
|
|
continue
|
|
}
|
|
// Mon Jan 2 15:04:05 MST 2006
|
|
switch s {
|
|
case "yy":
|
|
ret = t.AppendFormat(ret, "06")
|
|
case "yyyy":
|
|
ret = t.AppendFormat(ret, "2006")
|
|
case "m":
|
|
ret = t.AppendFormat(ret, "1")
|
|
case "mm":
|
|
ret = t.AppendFormat(ret, "01")
|
|
case "mmm":
|
|
ret = t.AppendFormat(ret, "Jan")
|
|
case "mmmm":
|
|
ret = t.AppendFormat(ret, "January")
|
|
case "mmmmm":
|
|
switch t.Month() {
|
|
case time.January, time.July, time.June:
|
|
ret = append(ret, 'J')
|
|
case time.February:
|
|
ret = append(ret, 'M')
|
|
case time.March, time.May:
|
|
ret = append(ret, 'M')
|
|
case time.April, time.August:
|
|
ret = append(ret, 'A')
|
|
case time.September:
|
|
ret = append(ret, 'S')
|
|
case time.October:
|
|
ret = append(ret, 'O')
|
|
case time.November:
|
|
ret = append(ret, 'N')
|
|
case time.December:
|
|
ret = append(ret, 'D')
|
|
}
|
|
case "d":
|
|
ret = t.AppendFormat(ret, "2")
|
|
case "dd":
|
|
ret = t.AppendFormat(ret, "02")
|
|
case "ddd":
|
|
ret = t.AppendFormat(ret, "Mon")
|
|
case "dddd":
|
|
ret = t.AppendFormat(ret, "Monday")
|
|
default:
|
|
unioffice.Log("unsupported date format %s", s)
|
|
}
|
|
if f[i] == '/' {
|
|
ret = append(ret, '/')
|
|
}
|
|
}
|
|
return ret
|
|
}
|
|
|
|
// dTime formats a time with an Excel format time string.
|
|
func dTime(t time.Time, v float64, f string) []byte {
|
|
ret := []byte{}
|
|
beg := 0
|
|
for i := 0; i < len(f); i++ {
|
|
var s string
|
|
// split on ':'
|
|
if f[i] == ':' {
|
|
s = string(f[beg:i])
|
|
beg = i + 1
|
|
} else if i == len(f)-1 {
|
|
s = string(f[beg : i+1])
|
|
} else {
|
|
continue
|
|
}
|
|
|
|
// Mon Jan 2 15:04:05 MST 2006
|
|
switch s {
|
|
case "d":
|
|
ret = t.AppendFormat(ret, "2")
|
|
case "h":
|
|
ret = t.AppendFormat(ret, "3")
|
|
case "hh":
|
|
ret = t.AppendFormat(ret, "15")
|
|
case "m":
|
|
ret = t.AppendFormat(ret, "4")
|
|
case "mm":
|
|
ret = t.AppendFormat(ret, "04")
|
|
case "s":
|
|
ret = t.Round(time.Second).AppendFormat(ret, "5")
|
|
case "s.0":
|
|
ret = t.Round(time.Second/10).AppendFormat(ret, "5.0")
|
|
case "s.00":
|
|
ret = t.Round(time.Second/100).AppendFormat(ret, "5.00")
|
|
case "s.000":
|
|
ret = t.Round(time.Second/1000).AppendFormat(ret, "5.000")
|
|
case "ss":
|
|
ret = t.Round(time.Second).AppendFormat(ret, "05")
|
|
case "ss.0":
|
|
ret = t.Round(time.Second/10).AppendFormat(ret, "05.0")
|
|
case "ss.00":
|
|
ret = t.Round(time.Second/100).AppendFormat(ret, "05.00")
|
|
case "ss.000":
|
|
ret = t.Round(time.Second/1000).AppendFormat(ret, "05.000")
|
|
case "AM/PM":
|
|
ret = t.AppendFormat(ret, "PM")
|
|
case "[h]":
|
|
ret = strconv.AppendInt(ret, int64(v*24), 10)
|
|
case "[m]":
|
|
ret = strconv.AppendInt(ret, int64(v*24*60), 10)
|
|
case "[s]":
|
|
ret = strconv.AppendInt(ret, int64(v*24*60*60), 10)
|
|
case "":
|
|
default:
|
|
unioffice.Log("unsupported time format %s", s)
|
|
}
|
|
if f[i] == ':' {
|
|
ret = append(ret, ':')
|
|
}
|
|
}
|
|
return ret
|
|
}
|