20
0

Add Formula & Format support

This commit is contained in:
chen.s.g 2018-04-05 21:28:01 +08:00
parent d1d6f84447
commit 574bf55ec4
11 changed files with 786 additions and 222 deletions

View File

@ -1,8 +1,9 @@
# xls
[![GoDoc](https://godoc.org/github.com/extrame/xls?status.svg)](https://godoc.org/github.com/extrame/xls)
[![GoDoc](https://godoc.org/github.com/csg800/xls?status.svg)](https://godoc.org/github.com/csg800/xls)
Pure Golang xls library writen by [Rongshu Tech(chinese)](http://www.rongshu.tech).
Add Formula & Format support by [chen.s.g] (http://www.imohe.com)
Thanks for contributions from Tamás Gulácsi, sergeilem.

View File

@ -46,6 +46,10 @@ type HyperLink struct {
IsUrl bool
}
func (h *HyperLink) Debug(wb *WorkBook) {
fmt.Printf("hyper link col dump:%#+v\n", h)
}
//get the hyperlink string, use the public variable Url to get the original Url
func (h *HyperLink) String(wb *WorkBook) []string {
res := make([]string, h.LastColB-h.FristColB+1)

329
col.go
View File

@ -1,17 +1,31 @@
package xls
import (
"errors"
"fmt"
"math"
"strconv"
"time"
"github.com/extrame/goyymmdd"
)
var ErrIsInt = errors.New("is int")
/* Data types */
const TYPE_STRING2 = 1
const TYPE_STRING = 2
const TYPE_FORMULA = 3
const TYPE_NUMERIC = 4
const TYPE_BOOL = 5
const TYPE_NULL = 6
const TYPE_INLINE = 7
const TYPE_ERROR = 8
const TYPE_DATETIME = 9
const TYPE_PERCENTAGE = 10
const TYPE_CURRENCY = 11
//content type
type contentHandler interface {
Debug(wb *WorkBook)
String(*WorkBook) []string
FirstCol() uint16
LastCol() uint16
@ -26,6 +40,10 @@ type Coler interface {
Row() uint16
}
func (c *Col) Debug(wb *WorkBook) {
fmt.Printf("col dump:%#+v\n", c)
}
func (c *Col) Row() uint16 {
return c.RowB
}
@ -42,39 +60,12 @@ func (c *Col) String(wb *WorkBook) []string {
return []string{"default"}
}
type XfRk struct {
Index uint16
Rk RK
}
func (xf *XfRk) String(wb *WorkBook) string {
idx := int(xf.Index)
if len(wb.Xfs) > idx {
fNo := wb.Xfs[idx].formatNo()
if fNo >= 164 { // user defined format
if formatter := wb.Formats[fNo]; formatter != nil {
i, f, isFloat := xf.Rk.number()
if !isFloat {
f = float64(i)
}
t := timeFromExcelTime(f, wb.dateMode == 1)
return yymmdd.Format(t, formatter.str)
}
// see http://www.openoffice.org/sc/excelfileformat.pdf Page #174
} else if 14 <= fNo && fNo <= 17 || fNo == 22 || 27 <= fNo && fNo <= 36 || 50 <= fNo && fNo <= 58 { // jp. date format
i, f, isFloat := xf.Rk.number()
if !isFloat {
f = float64(i)
}
t := timeFromExcelTime(f, wb.dateMode == 1)
return t.Format(time.RFC3339) //TODO it should be international
}
}
return xf.Rk.String()
}
type RK uint32
func (rk RK) Debug(wb *WorkBook) {
fmt.Printf("rk dump:%#+v\n", rk)
}
func (rk RK) number() (intNum int64, floatNum float64, isFloat bool) {
multiplied := rk & 1
isInt := rk & 2
@ -97,22 +88,40 @@ func (rk RK) number() (intNum int64, floatNum float64, isFloat bool) {
return int64(val), 0, false
}
func (rk RK) String() string {
func (rk RK) float() float64 {
var i, f, isFloat = rk.number()
if !isFloat {
f = float64(i)
}
return f
}
func (rk RK) String(wb *WorkBook) string {
i, f, isFloat := rk.number()
if isFloat {
return strconv.FormatFloat(f, 'f', -1, 64)
}
return strconv.FormatInt(i, 10)
}
var ErrIsInt = fmt.Errorf("is int")
type XfRk struct {
Index uint16
Rk RK
}
func (rk RK) Float() (float64, error) {
_, f, isFloat := rk.number()
if !isFloat {
return 0, ErrIsInt
func (xf *XfRk) Debug(wb *WorkBook) {
fmt.Printf("xfrk dump:%#+v\n", wb.Xfs[xf.Index])
xf.Rk.Debug(wb)
}
func (xf *XfRk) String(wb *WorkBook) string {
if val, ok := wb.Format(xf.Index, xf.Rk.float()); ok {
return val
}
return f, nil
return xf.Rk.String(wb)
}
type MulrkCol struct {
@ -121,16 +130,24 @@ type MulrkCol struct {
LastColB uint16
}
func (c *MulrkCol) Debug(wb *WorkBook) {
fmt.Printf("mulrk dump:%#+v\n", c)
for _, v := range c.Xfrks {
v.Debug(wb)
}
}
func (c *MulrkCol) LastCol() uint16 {
return c.LastColB
}
func (c *MulrkCol) String(wb *WorkBook) []string {
var res = make([]string, len(c.Xfrks))
for i := 0; i < len(c.Xfrks); i++ {
xfrk := c.Xfrks[i]
res[i] = xfrk.String(wb)
for i, v := range c.Xfrks {
res[i] = v.String(wb)
}
return res
}
@ -140,6 +157,10 @@ type MulBlankCol struct {
LastColB uint16
}
func (c *MulBlankCol) Debug(wb *WorkBook) {
fmt.Printf("mul blank dump:%#+v\n", c)
}
func (c *MulBlankCol) LastCol() uint16 {
return c.LastColB
}
@ -154,23 +175,209 @@ type NumberCol struct {
Float float64
}
func (c *NumberCol) Debug(wb *WorkBook) {
fmt.Printf("number col dump:%#+v\n", c)
}
func (c *NumberCol) String(wb *WorkBook) []string {
if v, ok := wb.Format(c.Index, c.Float); ok {
return []string{v}
}
return []string{strconv.FormatFloat(c.Float, 'f', -1, 64)}
}
type FormulaCol struct {
Header struct {
Col
IndexXf uint16
Result [8]byte
Flags uint16
_ uint32
type FormulaColHeader struct {
Col
IndexXf uint16
Result [8]byte
Flags uint16
_ uint32
}
// Value formula header value
func (f *FormulaColHeader) Value() float64 {
var rknumhigh = ByteToUint32(f.Result[4:8])
var rknumlow = ByteToUint32(f.Result[0:4])
var sign = (rknumhigh & 0x80000000) >> 31
var exp = ((rknumhigh & 0x7ff00000) >> 20) - 1023
var mantissa = (0x100000 | (rknumhigh & 0x000fffff))
var mantissalow1 = (rknumlow & 0x80000000) >> 31
var mantissalow2 = (rknumlow & 0x7fffffff)
var value = float64(mantissa) / math.Pow(2, float64(20-exp))
if mantissalow1 != 0 {
value += 1 / math.Pow(2, float64(21-exp))
}
Bts []byte
value += float64(mantissalow2) / math.Pow(2, float64(52-exp))
if 0 != sign {
value *= -1
}
return value
}
// IsPart part of shared formula check
// WARNING:
// We can apparently not rely on $isPartOfSharedFormula. Even when $isPartOfSharedFormula = true
// the formula data may be ordinary formula data, therefore we need to check
// explicitly for the tExp token (0x01)
func (f *FormulaColHeader) IsPart() bool {
return 0 != (0x0008 & ByteToUint16(f.Result[6:8]))
}
type FormulaCol struct {
parsed bool
Code uint16
Btl uint16
Btc uint16
Bts []byte
Header *FormulaColHeader
ws int
vType int
value string
}
func (c *FormulaCol) Debug(wb *WorkBook) {
fmt.Printf("formula col dump:%#+v\n", c)
}
func (c *FormulaCol) Row() uint16 {
return c.Header.Col.RowB
}
func (c *FormulaCol) FirstCol() uint16 {
return c.Header.Col.FirstColB
}
func (c *FormulaCol) LastCol() uint16 {
return c.Header.Col.FirstColB
}
func (c *FormulaCol) String(wb *WorkBook) []string {
return []string{"FormulaCol"}
if !c.parsed {
c.parse(wb, true)
}
return []string{c.value}
}
func (c *FormulaCol) parse(wb *WorkBook, ref bool) {
c.parsed = true
if 0 == c.Header.Result[0] && 255 == c.Header.Result[6] && 255 == c.Header.Result[7] {
// String formula. Result follows in appended STRING record
c.vType = TYPE_STRING
} else if 1 == c.Header.Result[0] && 255 == c.Header.Result[6] && 255 == c.Header.Result[7] {
// Boolean formula. Result is in +2; 0=false, 1=true
c.vType = TYPE_BOOL
if 0 == c.Header.Result[3] {
c.value = "false"
} else {
c.value = "true"
}
} else if 2 == c.Header.Result[0] && 255 == c.Header.Result[6] && 255 == c.Header.Result[7] {
// Error formula. Error code is in +2
c.vType = TYPE_ERROR
switch c.Header.Result[3] {
case 0x00:
c.value = "#NULL!"
case 0x07:
c.value = "#DIV/0"
case 0x0F:
c.value = "#VALUE!"
case 0x17:
c.value = "#REF!"
case 0x1D:
c.value = "#NAME?"
case 0x24:
c.value = "#NUM!"
case 0x2A:
c.value = "#N/A"
}
} else if 3 == c.Header.Result[0] && 255 == c.Header.Result[6] && 255 == c.Header.Result[7] {
// Formula result is a null string
c.vType = TYPE_NULL
c.value = ""
} else {
// formula result is a number, first 14 bytes like _NUMBER record
c.vType = TYPE_NUMERIC
var flag bool
if c.isGetCurTime() {
// if date time format is not support, use time.RFC3339
if c.value, flag = wb.Format(c.Header.IndexXf, 0); !flag {
c.value = parseTime(0, time.RFC3339)
}
} else if c.isRef() {
if ref {
var ws = -1
var find bool
var rIdx uint16
var cIdx uint16
if 0x07 == c.Bts[0] {
var exi = ByteToUint16(c.Bts[3:5])
rIdx = ByteToUint16(c.Bts[5:7])
cIdx = 0x00FF & ByteToUint16(c.Bts[7:9])
if exi <= wb.ref.Num {
ws = int(wb.ref.Info[int(exi)].FirstSheetIndex)
}
} else {
ws = c.ws
rIdx = ByteToUint16(c.Bts[3:5])
cIdx = 0x00FF & ByteToUint16(c.Bts[5:7])
}
if ws < len(wb.sheets) {
if row := wb.GetSheet(ws).Row(int(rIdx)); nil != row {
find = true
c.value = row.Col(int(cIdx))
}
}
if !find {
c.value = "#REF!"
}
} else {
c.parsed = false
}
} else {
c.value, flag = wb.Format(c.Header.IndexXf, c.Header.Value())
if !flag {
c.value = strconv.FormatFloat(c.Header.Value(), 'f', -1, 64)
}
}
}
}
// isRef return cell is reference to other cell
func (c *FormulaCol) isRef() bool {
if 0x05 == c.Bts[0] && (0x24 == c.Bts[2] || 0x44 == c.Bts[2] || 0x64 == c.Bts[2]) {
return true
} else if 0x07 == c.Bts[0] && (0x3A == c.Bts[2] || 0x5A == c.Bts[2] || 0x7A == c.Bts[2]) {
return true
}
return false
}
// isGetCurTime return cell value is get current date or datetime flag
func (c *FormulaCol) isGetCurTime() bool {
var ret bool
var next byte
if 0x19 == c.Bts[2] && (0x21 == c.Bts[6] || 0x41 == c.Bts[6] || 0x61 == c.Bts[6]) {
next = c.Bts[7]
} else if 0x21 == c.Bts[2] || 0x41 == c.Bts[2] || 0x61 == c.Bts[2] {
next = c.Bts[3]
}
if 0x4A == next || 0xDD == next {
ret = true
}
return ret
}
type RkCol struct {
@ -178,6 +385,10 @@ type RkCol struct {
Xfrk XfRk
}
func (c *RkCol) Debug(wb *WorkBook) {
fmt.Printf("rk col dump:%#+v\n", c)
}
func (c *RkCol) String(wb *WorkBook) []string {
return []string{c.Xfrk.String(wb)}
}
@ -188,6 +399,10 @@ type LabelsstCol struct {
Sst uint32
}
func (c *LabelsstCol) Debug(wb *WorkBook) {
fmt.Printf("label sst col dump:%#+v\n", c)
}
func (c *LabelsstCol) String(wb *WorkBook) []string {
return []string{wb.sst[int(c.Sst)]}
}
@ -197,6 +412,10 @@ type labelCol struct {
Str string
}
func (c *labelCol) Debug(wb *WorkBook) {
fmt.Printf("label col dump:%#+v\n", c)
}
func (c *labelCol) String(wb *WorkBook) []string {
return []string{c.Str}
}
@ -206,6 +425,10 @@ type BlankCol struct {
Xf uint16
}
func (c *BlankCol) Debug(wb *WorkBook) {
fmt.Printf("blank col dump:%#+v\n", c)
}
func (c *BlankCol) String(wb *WorkBook) []string {
return []string{""}
}

79
date.go
View File

@ -5,37 +5,57 @@ import (
"time"
)
const MJD_0 float64 = 2400000.5
const MJD_JD2000 float64 = 51544.5
// timeLocationUTC defined the UTC time location.
var timeLocationUTC, _ = time.LoadLocation("UTC")
// timeToUTCTime provides function to convert time to UTC time.
func timeToUTCTime(t time.Time) time.Time {
return time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), timeLocationUTC)
}
// timeToExcelTime provides function to convert time to Excel time.
func timeToExcelTime(t time.Time) float64 {
return float64(t.UnixNano())/8.64e13 + 25569.0
}
// shiftJulianToNoon provides function to process julian date to noon.
func shiftJulianToNoon(julianDays, julianFraction float64) (float64, float64) {
switch {
case -0.5 < julianFraction && julianFraction < 0.5:
julianFraction += 0.5
case julianFraction >= 0.5:
julianDays += 1
julianDays++
julianFraction -= 0.5
case julianFraction <= -0.5:
julianDays -= 1
julianDays--
julianFraction += 1.5
}
return julianDays, julianFraction
}
// Return the integer values for hour, minutes, seconds and
// nanoseconds that comprised a given fraction of a day.
// fractionOfADay provides function to return the integer values for hour,
// minutes, seconds and nanoseconds that comprised a given fraction of a day.
// values would round to 1 us.
func fractionOfADay(fraction float64) (hours, minutes, seconds, nanoseconds int) {
f := 5184000000000000 * fraction
nanoseconds = int(math.Mod(f, 1000000000))
f = f / 1000000000
seconds = int(math.Mod(f, 60))
f = f / 3600
minutes = int(math.Mod(f, 60))
f = f / 60
hours = int(f)
return hours, minutes, seconds, nanoseconds
const (
c1us = 1e3
c1s = 1e9
c1day = 24 * 60 * 60 * c1s
)
frac := int64(c1day*fraction + c1us/2)
nanoseconds = int((frac%c1s)/c1us) * c1us
frac /= c1s
seconds = int(frac % 60)
frac /= 60
minutes = int(frac % 60)
hours = int(frac / 60)
return
}
// julianDateToGregorianTime provides function to convert julian date to
// gregorian time.
func julianDateToGregorianTime(part1, part2 float64) time.Time {
part1I, part1F := math.Modf(part1)
part2I, part2F := math.Modf(part2)
@ -47,13 +67,12 @@ func julianDateToGregorianTime(part1, part2 float64) time.Time {
return time.Date(year, time.Month(month), day, hours, minutes, seconds, nanoseconds, time.UTC)
}
// By this point generations of programmers have repeated the
// algorithm sent to the editor of "Communications of the ACM" in 1968
// (published in CACM, volume 11, number 10, October 1968, p.657).
// None of those programmers seems to have found it necessary to
// explain the constants or variable names set out by Henry F. Fliegel
// and Thomas C. Van Flandern. Maybe one day I'll buy that jounal and
// expand an explanation here - that day is not today.
// By this point generations of programmers have repeated the algorithm sent to
// the editor of "Communications of the ACM" in 1968 (published in CACM, volume
// 11, number 10, October 1968, p.657). None of those programmers seems to have
// found it necessary to explain the constants or variable names set out by
// Henry F. Fliegel and Thomas C. Van Flandern. Maybe one day I'll buy that
// jounal and expand an explanation here - that day is not today.
func doTheFliegelAndVanFlandernAlgorithm(jd int) (day, month, year int) {
l := jd + 68569
n := (4 * l) / 146097
@ -68,24 +87,26 @@ func doTheFliegelAndVanFlandernAlgorithm(jd int) (day, month, year int) {
return d, m, y
}
// Convert an excelTime representation (stored as a floating point number) to a time.Time.
// timeFromExcelTime provides function to convert an excelTime representation
// (stored as a floating point number) to a time.Time.
func timeFromExcelTime(excelTime float64, date1904 bool) time.Time {
var date time.Time
var intPart int64 = int64(excelTime)
// Excel uses Julian dates prior to March 1st 1900, and
// Gregorian thereafter.
var intPart = int64(excelTime)
// Excel uses Julian dates prior to March 1st 1900, and Gregorian
// thereafter.
if intPart <= 61 {
const OFFSET1900 = 15018.0
const OFFSET1904 = 16480.0
const MJD0 float64 = 2400000.5
var date time.Time
if date1904 {
date = julianDateToGregorianTime(MJD_0+OFFSET1904, excelTime)
date = julianDateToGregorianTime(MJD0, excelTime+OFFSET1904)
} else {
date = julianDateToGregorianTime(MJD_0+OFFSET1900, excelTime)
date = julianDateToGregorianTime(MJD0, excelTime+OFFSET1900)
}
return date
}
var floatPart float64 = excelTime - float64(intPart)
var floatPart = excelTime - float64(intPart)
var dayNanoSeconds float64 = 24 * 60 * 60 * 1000 * 1000 * 1000
if date1904 {
date = time.Date(1904, 1, 1, 0, 0, 0, 0, time.UTC)

View File

@ -2,6 +2,7 @@ package xls
import (
"fmt"
"testing"
)
func ExampleOpen() {
@ -35,3 +36,23 @@ func ExampleWorkBook_GetSheet() {
}
}
}
func BenchmarkGetSheet(b *testing.B) {
for i := 0; i < b.N; i++ {
if xlFile, err := Open("Table.xls", "utf-8"); err == nil {
for i := 0; i < xlFile.NumSheets(); i++ {
xlFile.GetSheet(i)
}
}
}
}
func BenchmarkGetSheetWithBuffer(b *testing.B) {
for i := 0; i < b.N; i++ {
if xlFile, err := OpenWithBuffer("Table.xls", "utf-8"); err == nil {
for i := 0; i < xlFile.NumSheets(); i++ {
xlFile.GetSheet(i)
}
}
}
}

221
format.go
View File

@ -1,9 +1,228 @@
package xls
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: "# ??/??",
14: "mm-dd-yy",
15: "d-mmm-yy",
16: "d-mmm",
17: "mmm-yy",
18: "h:mm am/pm",
19: "h:mm:ss am/pm",
20: "h:mm",
21: "h:mm:ss",
22: "m/d/yy h:mm",
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
type Format struct {
Head struct {
Index uint16
Size uint16
}
str string
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 {
var t []string
if t = strings.SplitN(f.Raw[0], ".", 2); 2 == len(t) {
f.bts = strings.Count(t[1], "")
if f.bts > 0 {
f.bts = f.bts - 1
}
}
}
}
// 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 {
ret = strconv.FormatInt(int64(v), 10)
} else {
ret = strconv.FormatFloat(v, 'f', f.bts, 64)
}
case TYPE_CURRENCY:
if 0 == f.bts {
ret = strconv.FormatInt(int64(v), 10)
} else {
ret = strconv.FormatFloat(v, 'f', f.bts, 64)
}
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")
}

29
row.go
View File

@ -2,8 +2,8 @@ package xls
type rowInfo struct {
Index uint16
Fcell uint16
Lcell uint16
First uint16
Last uint16
Height uint16
Notused uint16
Notused2 uint16
@ -20,27 +20,30 @@ type Row struct {
//Col Get the Nth Col from the Row, if has not, return nil.
//Suggest use Has function to test it.
func (r *Row) Col(i int) string {
serial := uint16(i)
var val string
var serial = uint16(i)
if ch, ok := r.cols[serial]; ok {
strs := ch.String(r.wb)
return strs[0]
val = ch.String(r.wb)[0]
} else {
for _, v := range r.cols {
if v.FirstCol() <= serial && v.LastCol() >= serial {
strs := v.String(r.wb)
return strs[serial-v.FirstCol()]
val = v.String(r.wb)[serial-v.FirstCol()]
break
}
}
}
return ""
}
//LastCol Get the number of Last Col of the Row.
func (r *Row) LastCol() int {
return int(r.info.Lcell)
return val
}
//FirstCol Get the number of First Col of the Row.
func (r *Row) FirstCol() int {
return int(r.info.Fcell)
return int(r.info.First)
}
//LastCol Get the number of Last Col of the Row.
func (r *Row) LastCol() int {
return int(r.info.Last)
}

View File

@ -4,23 +4,23 @@ import (
"bytes"
"encoding/binary"
"io"
"os"
"strings"
"unicode/utf16"
)
//xls workbook type
type WorkBook struct {
Is5ver bool
Type uint16
Codepage uint16
Xfs []st_xf_data
Fonts []Font
Formats map[uint16]*Format
//All the sheets from the workbook
Is5ver bool
Type uint16
Codepage uint16
Xfs []XF
Fonts []Font
Formats map[uint16]*Format
sheets []*WorkSheet
Author string
rs io.ReadSeeker
sst []string
ref *extSheetRef
continue_utf16 uint16
continue_rich uint16
continue_apsb uint32
@ -29,62 +29,50 @@ type WorkBook struct {
//read workbook from ole2 file
func newWorkBookFromOle2(rs io.ReadSeeker) *WorkBook {
wb := new(WorkBook)
wb.Formats = make(map[uint16]*Format)
// wb.bts = bts
wb.rs = rs
wb.sheets = make([]*WorkSheet, 0)
wb.Parse(rs)
var wb = &WorkBook{
rs: rs,
ref: new(extSheetRef),
sheets: make([]*WorkSheet, 0),
Formats: make(map[uint16]*Format),
}
wb.parse(rs)
wb.prepare()
return wb
}
func (w *WorkBook) Parse(buf io.ReadSeeker) {
func (w *WorkBook) parse(buf io.ReadSeeker) {
b := new(bof)
bof_pre := new(bof)
// buf := bytes.NewReader(bts)
bp := new(bof)
offset := 0
for {
if err := binary.Read(buf, binary.LittleEndian, b); err == nil {
bof_pre, b, offset = w.parseBof(buf, b, bof_pre, offset)
bp, b, offset = w.parseBof(buf, b, bp, offset)
} else {
break
}
}
}
func (w *WorkBook) addXf(xf st_xf_data) {
w.Xfs = append(w.Xfs, xf)
}
func (w *WorkBook) addFont(font *FontInfo, buf io.ReadSeeker) {
name, _ := w.get_string(buf, uint16(font.NameB))
w.Fonts = append(w.Fonts, Font{Info: font, Name: name})
}
func (w *WorkBook) addFormat(format *Format) {
if w.Formats == nil {
os.Exit(1)
}
w.Formats[format.Head.Index] = format
}
func (wb *WorkBook) parseBof(buf io.ReadSeeker, b *bof, pre *bof, offset_pre int) (after *bof, after_using *bof, offset int) {
after = b
after_using = pre
var bts = make([]byte, b.Size)
binary.Read(buf, binary.LittleEndian, bts)
buf_item := bytes.NewReader(bts)
item := bytes.NewReader(bts)
switch b.Id {
case 0x809:
case 0x0809: // BOF
bif := new(biffHeader)
binary.Read(buf_item, binary.LittleEndian, bif)
binary.Read(item, binary.LittleEndian, bif)
if bif.Ver != 0x600 {
wb.Is5ver = true
}
wb.Type = bif.Type
case 0x042: // CODEPAGE
binary.Read(buf_item, binary.LittleEndian, &wb.Codepage)
case 0x3c: // CONTINUE
case 0x0042: // CODEPAGE
binary.Read(item, binary.LittleEndian, &wb.Codepage)
case 0x3C: // CONTINUE
if pre.Id == 0xfc {
var size uint16
var err error
@ -92,12 +80,12 @@ func (wb *WorkBook) parseBof(buf io.ReadSeeker, b *bof, pre *bof, offset_pre int
size = wb.continue_utf16
wb.continue_utf16 = 0
} else {
err = binary.Read(buf_item, binary.LittleEndian, &size)
err = binary.Read(item, binary.LittleEndian, &size)
}
for err == nil && offset_pre < len(wb.sst) {
var str string
if size > 0 {
str, err = wb.get_string(buf_item, size)
str, err = wb.parseString(item, size)
wb.sst[offset_pre] = wb.sst[offset_pre] + str
}
@ -106,23 +94,23 @@ func (wb *WorkBook) parseBof(buf io.ReadSeeker, b *bof, pre *bof, offset_pre int
}
offset_pre++
err = binary.Read(buf_item, binary.LittleEndian, &size)
err = binary.Read(item, binary.LittleEndian, &size)
}
}
offset = offset_pre
after = pre
after_using = b
case 0xfc: // SST
case 0x00FC: // SST
info := new(SstInfo)
binary.Read(buf_item, binary.LittleEndian, info)
binary.Read(item, binary.LittleEndian, info)
wb.sst = make([]string, info.Count)
var size uint16
var i = 0
for ; i < int(info.Count); i++ {
var err error
if err = binary.Read(buf_item, binary.LittleEndian, &size); err == nil {
if err = binary.Read(item, binary.LittleEndian, &size); err == nil {
var str string
str, err = wb.get_string(buf_item, size)
str, err = wb.parseString(item, size)
wb.sst[i] = wb.sst[i] + str
}
@ -131,41 +119,90 @@ func (wb *WorkBook) parseBof(buf io.ReadSeeker, b *bof, pre *bof, offset_pre int
}
}
offset = i
case 0x85: // bOUNDSHEET
case 0x0085: // SHEET
var bs = new(boundsheet)
binary.Read(buf_item, binary.LittleEndian, bs)
binary.Read(item, binary.LittleEndian, bs)
// different for BIFF5 and BIFF8
wb.addSheet(bs, buf_item)
case 0x0e0: // XF
wb.addSheet(bs, item)
case 0x0017: // EXTERNSHEET
if !wb.Is5ver {
binary.Read(item, binary.LittleEndian, &wb.ref.Num)
wb.ref.Info = make([]ExtSheetInfo, wb.ref.Num)
binary.Read(item, binary.LittleEndian, &wb.ref.Info)
}
case 0x00e0: // XF
if wb.Is5ver {
xf := new(Xf5)
binary.Read(buf_item, binary.LittleEndian, xf)
binary.Read(item, binary.LittleEndian, xf)
wb.addXf(xf)
} else {
xf := new(Xf8)
binary.Read(buf_item, binary.LittleEndian, xf)
binary.Read(item, binary.LittleEndian, xf)
wb.addXf(xf)
}
case 0x031: // FONT
case 0x0031: // FONT
f := new(FontInfo)
binary.Read(buf_item, binary.LittleEndian, f)
wb.addFont(f, buf_item)
case 0x41E: //FORMAT
font := new(Format)
binary.Read(buf_item, binary.LittleEndian, &font.Head)
font.str, _ = wb.get_string(buf_item, font.Head.Size)
wb.addFormat(font)
case 0x22: //DATEMODE
binary.Read(buf_item, binary.LittleEndian, &wb.dateMode)
binary.Read(item, binary.LittleEndian, f)
wb.addFont(f, item)
case 0x041E: //FORMAT
format := new(Format)
binary.Read(item, binary.LittleEndian, &format.Head)
if raw, err := wb.parseString(item, format.Head.Size); nil == err && "" != raw {
format.Raw = strings.Split(raw, ";")
} else {
format.Raw = []string{}
}
wb.addFormat(format)
case 0x0022: //DATEMODE
binary.Read(item, binary.LittleEndian, &wb.dateMode)
}
return
}
func (w *WorkBook) get_string(buf io.ReadSeeker, size uint16) (res string, err error) {
func (w *WorkBook) addXf(xf XF) {
w.Xfs = append(w.Xfs, xf)
}
func (w *WorkBook) addFont(font *FontInfo, buf io.ReadSeeker) {
name, _ := w.parseString(buf, uint16(font.NameB))
w.Fonts = append(w.Fonts, Font{Info: font, Name: name})
}
func (w *WorkBook) addFormat(format *Format) {
w.Formats[format.Head.Index] = format
}
func (w *WorkBook) addSheet(sheet *boundsheet, buf io.ReadSeeker) {
name, _ := w.parseString(buf, uint16(sheet.Name))
w.sheets = append(w.sheets, &WorkSheet{id: len(w.sheets), bs: sheet, Name: name, wb: w})
}
// prepare process workbook struct
func (w *WorkBook) prepare() {
for k, v := range builtInNumFmt {
if _, ok := w.Formats[k]; !ok {
w.Formats[k] = &Format{
Raw: strings.Split(v, ";"),
}
}
}
for _, v := range w.Formats {
v.Prepare()
}
}
//reading a sheet from the compress file to memory, you should call this before you try to get anything from sheet
func (w *WorkBook) prepareSheet(sheet *WorkSheet) {
w.rs.Seek(int64(sheet.bs.Filepos), 0)
sheet.parse(w.rs)
}
func (w *WorkBook) parseString(buf io.ReadSeeker, size uint16) (res string, err error) {
if w.Is5ver {
var bts = make([]byte, size)
_, err = buf.Read(bts)
res = string(bts)
res = string(bytes.Trim(bts, "\r\n\t "))
} else {
var richtext_num = uint16(0)
var phonetic_size = uint32(0)
@ -190,7 +227,7 @@ func (w *WorkBook) get_string(buf io.ReadSeeker, size uint16) (res string, err e
err = binary.Read(buf, binary.LittleEndian, &bts[i])
}
runes := utf16.Decode(bts[:i])
res = string(runes)
res = strings.Trim(string(runes), "\r\n\t ")
if i < size {
w.continue_utf16 = size - i + 1
}
@ -208,23 +245,21 @@ func (w *WorkBook) get_string(buf io.ReadSeeker, size uint16) (res string, err e
bts1[k] = uint16(v)
}
runes := utf16.Decode(bts1)
res = string(runes)
res = strings.Trim(string(runes), "\r\n\t ")
}
if richtext_num > 0 {
var bts []byte
var seek_size int64
var ss int64
if w.Is5ver {
seek_size = int64(2 * richtext_num)
ss = int64(2 * richtext_num)
} else {
seek_size = int64(4 * richtext_num)
ss = int64(4 * richtext_num)
}
bts = make([]byte, seek_size)
bts = make([]byte, ss)
err = binary.Read(buf, binary.LittleEndian, bts)
if err == io.EOF {
w.continue_rich = richtext_num
}
// err = binary.Read(buf, binary.LittleEndian, bts)
}
if phonetic_size > 0 {
var bts []byte
@ -238,18 +273,20 @@ func (w *WorkBook) get_string(buf io.ReadSeeker, size uint16) (res string, err e
return
}
func (w *WorkBook) addSheet(sheet *boundsheet, buf io.ReadSeeker) {
name, _ := w.get_string(buf, uint16(sheet.Name))
w.sheets = append(w.sheets, &WorkSheet{bs: sheet, Name: name, wb: w})
// Format format value to string
func (w *WorkBook) Format(xf uint16, v float64) (string, bool) {
var val string
var idx = int(xf)
if len(w.Xfs) > idx {
if formatter := w.Formats[w.Xfs[idx].FormatNo()]; nil != formatter {
return formatter.String(v), true
}
}
return val, false
}
//reading a sheet from the compress file to memory, you should call this before you try to get anything from sheet
func (w *WorkBook) prepareSheet(sheet *WorkSheet) {
w.rs.Seek(int64(sheet.bs.Filepos), 0)
sheet.parse(w.rs)
}
//Get one sheet by its number
//GetSheet get one sheet by its number
func (w *WorkBook) GetSheet(num int) *WorkSheet {
if num < len(w.sheets) {
s := w.sheets[num]
@ -257,9 +294,8 @@ func (w *WorkBook) GetSheet(num int) *WorkSheet {
w.prepareSheet(s)
}
return s
} else {
return nil
}
return nil
}
//Get the number of all sheets, look into example
@ -267,9 +303,9 @@ func (w *WorkBook) NumSheets() int {
return len(w.sheets)
}
//helper function to read all cells from file
//ReadAllCells helper function to read all cells from file
//Notice: the max value is the limit of the max capacity of lines.
//Warning: the helper function will need big memeory if file is large.
//Warning: the helper function will need big memory if file is large.
func (w *WorkBook) ReadAllCells(max int) (res [][]string) {
res = make([][]string, 0)
for _, sheet := range w.sheets {
@ -277,11 +313,11 @@ func (w *WorkBook) ReadAllCells(max int) (res [][]string) {
max = max - len(res)
w.prepareSheet(sheet)
if sheet.MaxRow != 0 {
leng := int(sheet.MaxRow) + 1
if max < leng {
leng = max
length := int(sheet.MaxRow) + 1
if max < length {
length = max
}
temp := make([][]string, leng)
temp := make([][]string, length)
for k, row := range sheet.rows {
data := make([]string, 0)
if len(row.cols) > 0 {
@ -295,7 +331,7 @@ func (w *WorkBook) ReadAllCells(max int) (res [][]string) {
data[col.FirstCol()+i] = str[i]
}
}
if leng > int(k) {
if length > int(k) {
temp[k] = data
}
}

View File

@ -2,7 +2,6 @@ package xls
import (
"encoding/binary"
"fmt"
"io"
"unicode/utf16"
)
@ -14,6 +13,18 @@ type boundsheet struct {
Name byte
}
type extSheetRef struct {
Num uint16
Info []ExtSheetInfo
}
// ExtSheetInfo external sheet references provided for named cells
type ExtSheetInfo struct {
ExternalBookIndex uint16
FirstSheetIndex uint16
LastSheetIndex uint16
}
//WorkSheet in one WorkBook
type WorkSheet struct {
bs *boundsheet
@ -22,6 +33,7 @@ type WorkSheet struct {
rows map[uint16]*Row
//NOTICE: this is the max row number of the sheet, so it should be count -1
MaxRow uint16
id int
parsed bool
}
@ -36,15 +48,14 @@ func (w *WorkSheet) Row(i int) *Row {
func (w *WorkSheet) parse(buf io.ReadSeeker) {
w.rows = make(map[uint16]*Row)
b := new(bof)
var bof_pre *bof
var bp *bof
for {
if err := binary.Read(buf, binary.LittleEndian, b); err == nil {
bof_pre = w.parseBof(buf, b, bof_pre)
bp = w.parseBof(buf, b, bp)
if b.Id == 0xa {
break
}
} else {
fmt.Println(err)
break
}
}
@ -63,21 +74,17 @@ func (w *WorkSheet) parseBof(buf io.ReadSeeker, b *bof, pre *bof) *bof {
case 0x0BD: //MULRK
mc := new(MulrkCol)
size := (b.Size - 6) / 6
binary.Read(buf, binary.LittleEndian, &mc.Col)
mc.Xfrks = make([]XfRk, size)
for i := uint16(0); i < size; i++ {
binary.Read(buf, binary.LittleEndian, &mc.Xfrks[i])
}
binary.Read(buf, binary.LittleEndian, &mc.Col)
binary.Read(buf, binary.LittleEndian, &mc.Xfrks)
binary.Read(buf, binary.LittleEndian, &mc.LastColB)
col = mc
case 0x0BE: //MULBLANK
mc := new(MulBlankCol)
size := (b.Size - 6) / 2
binary.Read(buf, binary.LittleEndian, &mc.Col)
mc.Xfs = make([]uint16, size)
for i := uint16(0); i < size; i++ {
binary.Read(buf, binary.LittleEndian, &mc.Xfs[i])
}
binary.Read(buf, binary.LittleEndian, &mc.Col)
binary.Read(buf, binary.LittleEndian, &mc.Xfs)
binary.Read(buf, binary.LittleEndian, &mc.LastColB)
col = mc
case 0x203: //NUMBER
@ -85,10 +92,26 @@ func (w *WorkSheet) parseBof(buf io.ReadSeeker, b *bof, pre *bof) *bof {
binary.Read(buf, binary.LittleEndian, col)
case 0x06: //FORMULA
c := new(FormulaCol)
binary.Read(buf, binary.LittleEndian, &c.Header)
c.ws = w.id
c.Header = new(FormulaColHeader)
c.Bts = make([]byte, b.Size-20)
binary.Read(buf, binary.LittleEndian, c.Header)
binary.Read(buf, binary.LittleEndian, &c.Bts)
col = c
c.parse(w.wb, false)
if TYPE_STRING == c.vType {
binary.Read(buf, binary.LittleEndian, &c.Code)
binary.Read(buf, binary.LittleEndian, &c.Btl)
binary.Read(buf, binary.LittleEndian, &c.Btc)
var fms, fme = w.wb.parseString(buf, c.Btc)
if nil == fme {
c.value = fms
}
buf.Seek(-int64(c.Btl+4), 1)
}
case 0x27e: //RK
col = new(RkCol)
binary.Read(buf, binary.LittleEndian, col)
@ -100,18 +123,18 @@ func (w *WorkSheet) parseBof(buf io.ReadSeeker, b *bof, pre *bof) *bof {
binary.Read(buf, binary.LittleEndian, &c.BlankCol)
var count uint16
binary.Read(buf, binary.LittleEndian, &count)
c.Str, _ = w.wb.get_string(buf, count)
c.Str, _ = w.wb.parseString(buf, count)
col = c
case 0x201: //BLANK
col = new(BlankCol)
binary.Read(buf, binary.LittleEndian, col)
case 0x1b8: //HYPERLINK
var flag uint32
var count uint32
var hy HyperLink
binary.Read(buf, binary.LittleEndian, &hy.CellRange)
buf.Seek(20, 1)
var flag uint32
binary.Read(buf, binary.LittleEndian, &flag)
var count uint32
if flag&0x14 != 0 {
binary.Read(buf, binary.LittleEndian, &count)
@ -172,7 +195,6 @@ func (w *WorkSheet) add(content interface{}) {
w.addCell(col, ch)
}
}
}
func (w *WorkSheet) addCell(col Coler, ch contentHandler) {
@ -180,33 +202,36 @@ func (w *WorkSheet) addCell(col Coler, ch contentHandler) {
}
func (w *WorkSheet) addRange(rang Ranger, ch contentHandler) {
for i := rang.FirstRow(); i <= rang.LastRow(); i++ {
w.addContent(i, ch)
}
}
func (w *WorkSheet) addContent(row_num uint16, ch contentHandler) {
func (w *WorkSheet) addContent(num uint16, ch contentHandler) {
var row *Row
var ok bool
if row, ok = w.rows[row_num]; !ok {
if row, ok = w.rows[num]; !ok {
info := new(rowInfo)
info.Index = row_num
info.Index = num
row = w.addRow(info)
}
row.cols[ch.FirstCol()] = ch
}
func (w *WorkSheet) addRow(info *rowInfo) (row *Row) {
func (w *WorkSheet) addRow(info *rowInfo) *Row {
var ok bool
var row *Row
if info.Index > w.MaxRow {
w.MaxRow = info.Index
}
var ok bool
if row, ok = w.rows[info.Index]; ok {
row.info = info
} else {
row = &Row{info: info, cols: make(map[uint16]contentHandler)}
row = &Row{info: info, cols: make(map[uint16]contentHandler, int(info.Last-info.First))}
w.rows[info.Index] = row
}
return
return row
}

12
xf.go
View File

@ -1,5 +1,9 @@
package xls
type XF interface {
FormatNo() uint16
}
type Xf5 struct {
Font uint16
Format uint16
@ -11,7 +15,7 @@ type Xf5 struct {
Linestyle uint16
}
func (x *Xf5) formatNo() uint16 {
func (x *Xf5) FormatNo() uint16 {
return x.Format
}
@ -28,10 +32,6 @@ type Xf8 struct {
Groundcolor uint16
}
func (x *Xf8) formatNo() uint16 {
func (x *Xf8) FormatNo() uint16 {
return x.Format
}
type st_xf_data interface {
formatNo() uint16
}

15
xls.go
View File

@ -1,7 +1,9 @@
package xls
import (
"bytes"
"io"
"io/ioutil"
"os"
"github.com/extrame/ole2"
@ -16,7 +18,16 @@ func Open(file string, charset string) (*WorkBook, error) {
}
}
//Open one xls file and return the closer
//OpenWithBuffer open one xls file with memory buffer
func OpenWithBuffer(file string, charset string) (*WorkBook, error) {
if fi, err := ioutil.ReadFile(file); err == nil {
return OpenReader(bytes.NewReader(fi), charset)
} else {
return nil, err
}
}
//OpenWithCloser open one xls file and return the closer
func OpenWithCloser(file string, charset string) (*WorkBook, io.Closer, error) {
if fi, err := os.Open(file); err == nil {
wb, err := OpenReader(fi, charset)
@ -26,7 +37,7 @@ func OpenWithCloser(file string, charset string) (*WorkBook, io.Closer, error) {
}
}
//Open xls file from reader
//OpenReader open xls file from reader
func OpenReader(reader io.ReadSeeker, charset string) (wb *WorkBook, err error) {
var ole *ole2.Ole
if ole, err = ole2.Open(reader, charset); err == nil {