21
0
xls/format.go

226 lines
6.0 KiB
Go
Raw Normal View History

2015-03-19 10:39:41 +01:00
package xls
2018-04-05 15:28:01 +02:00
import (
"regexp"
"strconv"
"strings"
"time"
)
// Excel styles can reference number formats that are built-in, all of which
// have an id less than 164. This is a possibly incomplete list comprised of as
// many of them as I could find.
var builtInNumFmt = map[uint16]string{
0: "general",
1: "0",
2: "0.00",
3: "#,##0",
4: "#,##0.00",
9: "0%",
10: "0.00%",
11: "0.00e+00",
12: "# ?/?",
13: "# ??/??",
2018-05-15 07:42:40 +02:00
14: "mm-dd-yyyy",
15: "d-mmm-yyyy",
2018-04-05 15:28:01 +02:00
16: "d-mmm",
2018-05-15 07:42:40 +02:00
17: "mmm-yyyy",
2018-04-05 15:28:01 +02:00
18: "h:mm am/pm",
19: "h:mm:ss am/pm",
20: "h:mm",
21: "h:mm:ss",
2018-05-15 07:42:40 +02:00
22: "m/d/yyyy h:mm",
2018-04-05 15:28:01 +02:00
37: "#,##0 ;(#,##0)",
38: "#,##0 ;[red](#,##0)",
39: "#,##0.00;(#,##0.00)",
40: "#,##0.00;[red](#,##0.00)",
41: `_(* #,##0_);_(* \(#,##0\);_(* "-"_);_(@_)`,
42: `_("$"* #,##0_);_("$* \(#,##0\);_("$"* "-"_);_(@_)`,
43: `_(* #,##0.00_);_(* \(#,##0.00\);_(* "-"??_);_(@_)`,
44: `_("$"* #,##0.00_);_("$"* \(#,##0.00\);_("$"* "-"??_);_(@_)`,
45: "mm:ss",
46: "[h]:mm:ss",
47: "mmss.0",
48: "##0.0e+0",
49: "@",
58: time.RFC3339,
}
// Excel date time mapper to go system
var dateTimeMapper = []struct{ xls, golang string }{
{"yyyy", "2006"},
{"yy", "06"},
{"mmmm", "%%%%"},
{"dddd", "&&&&"},
{"dd", "02"},
{"d", "2"},
{"mmm", "Jan"},
{"mmss", "0405"},
{"ss", "05"},
{"mm:", "04:"},
{":mm", ":04"},
{"mm", "01"},
{"am/pm", "pm"},
{"m/", "1/"},
{"%%%%", "January"},
{"&&&&", "Monday"},
}
// Format value interface
2015-03-19 10:39:41 +01:00
type Format struct {
2015-03-25 07:47:26 +01:00
Head struct {
Index uint16
Size uint16
}
2018-04-05 15:28:01 +02:00
Raw []string
bts int
vType int
}
// Prepare format meta data
func (f *Format) Prepare() {
var regexColor = regexp.MustCompile("^\\[[a-zA-Z]+\\]")
var regexFraction = regexp.MustCompile("#\\,?#*")
for k, v := range f.Raw {
// In Excel formats, "_" is used to add spacing, which we can't do in HTML
v = strings.Replace(v, "_", "", -1)
// Some non-number characters are escaped with \, which we don't need
v = strings.Replace(v, "\\", "", -1)
// Some non-number strings are quoted, so we'll get rid of the quotes, likewise any positional * symbols
v = strings.Replace(v, "*", "", -1)
v = strings.Replace(v, "\"", "", -1)
// strip ()
v = strings.Replace(v, "(", "", -1)
v = strings.Replace(v, ")", "", -1)
// strip color information
v = regexColor.ReplaceAllString(v, "")
// Strip #
v = regexFraction.ReplaceAllString(v, "")
if 0 == f.vType {
if regexp.MustCompile("^(\\[\\$[A-Z]*-[0-9A-F]*\\])*[hmsdy]").MatchString(v) {
f.vType = TYPE_DATETIME
} else if strings.HasSuffix(v, "%") {
f.vType = TYPE_PERCENTAGE
} else if strings.HasPrefix(v, "$") || strings.HasPrefix(v, "¥") {
f.vType = TYPE_CURRENCY
}
}
f.Raw[k] = strings.Trim(v, "\r\n\t ")
}
if 0 == f.vType {
f.vType = TYPE_NUMERIC
}
if TYPE_NUMERIC == f.vType || TYPE_CURRENCY == f.vType || TYPE_PERCENTAGE == f.vType {
if t := strings.SplitN(f.Raw[0], ".", 2); 2 == len(t) {
2018-04-05 15:28:01 +02:00
f.bts = strings.Count(t[1], "")
if f.bts > 0 {
f.bts = f.bts - 1
}
} else if t := strings.Index(f.Raw[0], "General"); t > 0 {
f.bts = -1
2018-04-05 15:28:01 +02:00
}
}
}
// String format content to spec string
// see http://www.openoffice.org/sc/excelfileformat.pdf Page #174
func (f *Format) String(v float64) string {
var ret string
switch f.vType {
case TYPE_NUMERIC:
if 0 == f.bts && nil != f.Raw && "general" == f.Raw[0] {
f.bts = -1
}
2018-04-18 17:33:50 +02:00
ret = strconv.FormatFloat(v, 'f', f.bts, 64)
2018-04-05 15:28:01 +02:00
case TYPE_CURRENCY:
2018-04-18 17:33:50 +02:00
ret = strconv.FormatFloat(v, 'f', f.bts, 64)
2018-04-05 15:28:01 +02:00
case TYPE_PERCENTAGE:
if 0 == f.bts {
ret = strconv.FormatInt(int64(v)*100, 10) + "%"
} else {
ret = strconv.FormatFloat(v*100, 'f', f.bts, 64) + "%"
}
case TYPE_DATETIME:
ret = parseTime(v, f.Raw[0])
default:
ret = strconv.FormatFloat(v, 'f', -1, 64)
}
return ret
}
// ByteToUint32 Read 32-bit unsigned integer
func ByteToUint32(b []byte) uint32 {
return uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24
}
// ByteToUint16 Read 16-bit unsigned integer
func ByteToUint16(b []byte) uint16 {
return (uint16(b[0]) | (uint16(b[1]) << 8))
}
// parseTime provides function to returns a string parsed using time.Time.
// Replace Excel placeholders with Go time placeholders. For example, replace
// yyyy with 2006. These are in a specific order, due to the fact that m is used
// in month, minute, and am/pm. It would be easier to fix that with regular
// expressions, but if it's possible to keep this simple it would be easier to
// maintain. Full-length month and days (e.g. March, Tuesday) have letters in
// them that would be replaced by other characters below (such as the 'h' in
// March, or the 'd' in Tuesday) below. First we convert them to arbitrary
// characters unused in Excel Date formats, and then at the end, turn them to
// what they should actually be.
// Based off: http://www.ozgrid.com/Excel/CustomFormats.htm
func parseTime(v float64, f string) string {
var val time.Time
if 0 == v {
val = time.Now()
} else {
val = timeFromExcelTime(v, false)
}
// It is the presence of the "am/pm" indicator that determines if this is
// a 12 hour or 24 hours time format, not the number of 'h' characters.
if is12HourTime(f) {
f = strings.Replace(f, "hh", "03", 1)
f = strings.Replace(f, "h", "3", 1)
} else {
f = strings.Replace(f, "hh", "15", 1)
f = strings.Replace(f, "h", "15", 1)
}
for _, repl := range dateTimeMapper {
f = strings.Replace(f, repl.xls, repl.golang, 1)
}
// If the hour is optional, strip it out, along with the possible dangling
// colon that would remain.
if val.Hour() < 1 {
f = strings.Replace(f, "]:", "]", 1)
f = strings.Replace(f, "[03]", "", 1)
f = strings.Replace(f, "[3]", "", 1)
f = strings.Replace(f, "[15]", "", 1)
} else {
f = strings.Replace(f, "[3]", "3", 1)
f = strings.Replace(f, "[15]", "15", 1)
}
return val.Format(f)
}
// is12HourTime checks whether an Excel time format string is a 12 hours form.
func is12HourTime(format string) bool {
return strings.Contains(format, "am/pm") || strings.Contains(format, "AM/PM") || strings.Contains(format, "a/p") || strings.Contains(format, "A/P")
2015-03-19 10:39:41 +01:00
}