[
  {
    "path": ".github/workflows/go.yml",
    "content": "name: Go\n\non:\n  push:\n    branches: [ main ]\n  pull_request:\n    branches: [ main ]\n\njobs:\n\n  build:\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@v2\n\n    - name: Set up Go\n      uses: actions/setup-go@v2\n      with:\n        go-version: 1.17\n\n    - name: Build\n      run: go build -v ./...\n\n    - name: Test XLS\n      run: go test -v ./xls\n\n    - name: Test XLSX\n      run: go test -v ./xlsx\n\n    - name: Test CommonXL\n      run: go test -v ./commonxl\n"
  },
  {
    "path": ".gitignore",
    "content": "cmd/grate2tsv/results\ntestdata\n\n*.pprof\n*.pdf\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2021 Jeremy Jay\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "README.md",
    "content": "# grate\n\nA Go native tabular data extraction package. Currently supports `.xls`, `.xlsx`, `.csv`, `.tsv` formats.\n\n# Why?\n\nGrate focuses on speed and stability first, and makes no attempt to parse charts, figures, or other content types that may be present embedded within the input files. It tries to perform as few allocations as possible and errs on the side of caution.\n\nThere are certainly still some bugs and edge cases, but we have run it successfully on a set of 400k `.xls` and `.xlsx` files to catch many bugs and error conditions. Please file an issue with any feedback and additional problem files.\n\n# Usage\n\nGrate provides a simple standard interface for all supported filetypes, allowing access to both named worksheets in spreadsheets and single tables in plaintext formats.\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n    \"os\"\n    \"strings\"\n\n    \"github.com/pbnjay/grate\"\n    _ \"github.com/pbnjay/grate/simple\" // tsv and csv support\n    _ \"github.com/pbnjay/grate/xls\"\n    _ \"github.com/pbnjay/grate/xlsx\"\n)\n\nfunc main() {\n    wb, _ := grate.Open(os.Args[1])  // open the file\n    sheets, _ := wb.List()           // list available sheets\n    for _, s := range sheets {       // enumerate each sheet name\n        sheet, _ := wb.Get(s)        // open the sheet\n        for sheet.Next() {           // enumerate each row of data\n            row := sheet.Strings()   // get the row's content as []string\n            fmt.Println(strings.Join(row, \"\\t\"))\n        }\n    }\n    wb.Close()\n}\n```\n\n# License\n\nAll source code is licensed under the [MIT License](https://raw.github.com/pbnjay/grate/master/LICENSE).\n"
  },
  {
    "path": "cmd/grate2tsv/main.go",
    "content": "// Command grate2tsv is a highly parallel tabular data extraction tool. It's\n// probably not necessary in your situation, but is included here since it\n// is a good stress test of the codebase.\n//\n// Files on the command line will be parsed and extracted to the \"results\"\n// subdirectory under a heirarchical arrangement (to make our filesystems\n// more responsive), and a \"results.txt\" file will be created logging basic\n// information and errors for each file.\npackage main\n\nimport (\n\t\"bufio\"\n\t\"crypto/md5\"\n\t\"flag\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/ioutil\"\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"runtime/pprof\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/pbnjay/grate\"\n\t_ \"github.com/pbnjay/grate/simple\"\n\t_ \"github.com/pbnjay/grate/xls\"\n\t_ \"github.com/pbnjay/grate/xlsx\"\n)\n\nvar (\n\tlogfile        = flag.String(\"l\", \"\", \"save processing logs to `filename.txt`\")\n\tpretend        = flag.Bool(\"p\", false, \"pretend to output .tsv\")\n\tinfoFile       = flag.String(\"i\", \"results.txt\", \"`filename` to record stats about the process\")\n\tremoveNewlines = flag.Bool(\"r\", true, \"remove embedded tabs, newlines, and condense spaces in cell contents\")\n\ttrimSpaces     = flag.Bool(\"w\", true, \"trim whitespace from cell contents\")\n\tskipBlanks     = flag.Bool(\"b\", true, \"discard blank rows from the output\")\n\tcpuprofile     = flag.String(\"cpuprofile\", \"\", \"write cpu profile to file\")\n\tmemprofile     = flag.String(\"memprofile\", \"\", \"write memory profile to file\")\n\n\ttimeFormat = \"2006-01-02 15:04:05\"\n\tfstats     *os.File\n\n\tprocWG  sync.WaitGroup\n\tcleanup = make(chan *output, 100)\n\toutpool = sync.Pool{New: func() interface{} {\n\t\treturn &output{}\n\t}}\n)\n\ntype output struct {\n\tf *os.File\n\tb *bufio.Writer\n}\n\nfunc main() {\n\tflag.Parse()\n\n\tif *memprofile != \"\" {\n\t\tf, err := os.Create(*memprofile)\n\t\tif err != nil {\n\t\t\tlog.Fatal(err)\n\t\t}\n\t\tdefer func() {\n\t\t\truntime.GC()\n\t\t\tpprof.WriteHeapProfile(f)\n\t\t\tf.Close()\n\t\t}()\n\t}\n\n\tif *cpuprofile != \"\" {\n\t\tf, err := os.Create(*cpuprofile)\n\t\tif err != nil {\n\t\t\tlog.Fatal(err)\n\t\t}\n\t\tpprof.StartCPUProfile(f)\n\t\tdefer pprof.StopCPUProfile()\n\t}\n\n\tif *logfile != \"\" {\n\t\tfo, err := os.Create(*logfile)\n\t\tif err != nil {\n\t\t\tlog.Fatal(err)\n\t\t}\n\t\tdefer fo.Close()\n\t\tlog.SetOutput(fo)\n\t}\n\n\tdone := make(chan int)\n\tgo func() {\n\t\tfor x := range cleanup {\n\t\t\tx.b.Flush()\n\t\t\tx.f.Close()\n\t\t\toutpool.Put(x)\n\t\t}\n\t\tdone <- 1\n\t}()\n\n\tvar err error\n\tfstats, err = os.OpenFile(*infoFile, os.O_CREATE|os.O_RDWR, 0644)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tdefer fstats.Close()\n\tpos, err := fstats.Seek(0, io.SeekEnd)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tif pos == 0 {\n\t\tfmt.Fprintf(fstats, \"time\\tfilename\\tsheet\\trows\\tcolumns\\terrors\\n\")\n\t}\n\n\tfilenameChan := make(chan string)\n\n\t// fan out to 1/2 of CPU cores\n\t// (e.g. each file-processor can use 2 cpus)\n\toutMu := &sync.Mutex{}\n\tnparallel := runtime.NumCPU() / 2\n\tprocWG.Add(nparallel)\n\tfor i := 0; i < nparallel; i++ {\n\t\tgo runProcessor(filenameChan, outMu)\n\t}\n\tfor _, fn := range flag.Args() {\n\t\tfilenameChan <- fn\n\t}\n\n\tclose(filenameChan)\n\tprocWG.Wait()\n\tclose(cleanup)\n\t<-done\n}\n\nfunc runProcessor(from chan string, mu *sync.Mutex) {\n\tfor fn := range from {\n\t\tnowFmt := time.Now().Format(timeFormat)\n\t\tresults, err := processFile(fn)\n\t\tmu.Lock()\n\t\tif err != nil {\n\t\t\t// returned errors are fatal\n\t\t\tfmt.Fprintf(fstats, \"%s\\t%s\\t-\\t-\\t-\\t%s\\n\", nowFmt, fn, err.Error())\n\t\t\tmu.Unlock()\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, res := range results {\n\t\t\te := \"-\"\n\t\t\tif res.Err != nil {\n\t\t\t\te = res.Err.Error()\n\t\t\t}\n\t\t\tfmt.Fprintf(fstats, \"%s\\t%s\\t%s\\t%d\\t%d\\t%s\\n\", nowFmt, res.Filename, res.SheetName,\n\t\t\t\tres.NumRows, res.NumCols, e)\n\t\t}\n\t\tmu.Unlock()\n\t}\n\tprocWG.Done()\n}\n\nvar (\n\tsanitize = regexp.MustCompile(\"[^a-zA-Z0-9]+\")\n\tnewlines = regexp.MustCompile(\"[ \\n\\r\\t]+\")\n)\n\ntype stats struct {\n\tFilename  string\n\tHash      string\n\tSheetName string\n\tNumRows   int\n\tNumCols   int\n\tErr       error\n}\n\nfunc processFile(fn string) ([]stats, error) {\n\t//log.Printf(\"Opening file '%s' ...\", fn)\n\twb, err := grate.Open(fn)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer wb.Close()\n\n\tresults := []stats{}\n\n\text := filepath.Ext(fn)\n\tfn2 := filepath.Base(strings.TrimSuffix(fn, ext))\n\tsubparts := fmt.Sprintf(\"%x\", md5.Sum([]byte(fn2)))\n\tsubdir := filepath.Join(\"results\", subparts[:2], subparts[2:4])\n\tos.MkdirAll(subdir, 0755)\n\tlog.Printf(subparts[:8]+\"  Processing file '%s'\", fn2)\n\n\tsheets, err := wb.List()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfor _, s := range sheets {\n\t\tps := stats{\n\t\t\tFilename:  fn,\n\t\t\tHash:      subparts[:8],\n\t\t\tSheetName: s,\n\t\t}\n\t\tlog.Printf(subparts[:8]+\"  Opening Sheet '%s'...\", s)\n\t\tsheet, err := wb.Get(s)\n\t\tif err != nil {\n\t\t\tps.Err = err\n\t\t\tresults = append(results, ps)\n\t\t\tcontinue\n\t\t}\n\t\tif sheet.IsEmpty() {\n\t\t\tlog.Println(subparts[:8] + \"    Empty sheet. Skipping.\")\n\t\t\tresults = append(results, ps)\n\t\t\tcontinue\n\t\t}\n\t\ts2 := sanitize.ReplaceAllString(s, \"_\")\n\t\tif s == fn {\n\t\t\ts2 = \"main\"\n\t\t}\n\t\tvar ox *output\n\t\tvar w io.Writer = ioutil.Discard\n\t\tif !*pretend {\n\t\t\tf, err := os.Create(subdir + \"/\" + fn2 + \".\" + s2 + \".tsv\")\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tox = outpool.Get().(*output)\n\t\t\tox.f = f\n\t\t\tox.b = bufio.NewWriter(f)\n\t\t\tw = ox.b\n\t\t}\n\n\t\tfor sheet.Next() {\n\t\t\trow := sheet.Strings()\n\t\t\tnonblank := false\n\t\t\tfor i, x := range row {\n\t\t\t\tif *removeNewlines {\n\t\t\t\t\tx = newlines.ReplaceAllString(x, \" \")\n\t\t\t\t}\n\t\t\t\tif *trimSpaces {\n\t\t\t\t\tx = strings.TrimSpace(x)\n\t\t\t\t\trow[i] = x\n\t\t\t\t}\n\t\t\t\tif x != \"\" {\n\t\t\t\t\tnonblank = true\n\t\t\t\t\tif ps.NumCols < i {\n\t\t\t\t\t\tps.NumCols = i\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif nonblank || !*skipBlanks {\n\t\t\t\tfor i, v := range row {\n\t\t\t\t\tif i != 0 {\n\t\t\t\t\t\tw.Write([]byte{'\\t'})\n\t\t\t\t\t}\n\t\t\t\t\tw.Write([]byte(v))\n\t\t\t\t}\n\t\t\t\tw.Write([]byte{'\\n'})\n\t\t\t\tps.NumRows++\n\t\t\t}\n\t\t}\n\t\tresults = append(results, ps)\n\t\tif ox != nil {\n\t\t\tcleanup <- ox\n\t\t}\n\t}\n\treturn results, nil\n}\n"
  },
  {
    "path": "cmd/grater/main.go",
    "content": "// Command grater extracts contents of the tabular files to stdout.\npackage main\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/pbnjay/grate\"\n\t_ \"github.com/pbnjay/grate/simple\" // tsv and csv support\n\t_ \"github.com/pbnjay/grate/xls\"\n\t_ \"github.com/pbnjay/grate/xlsx\"\n)\n\nfunc main() {\n\tflagDebug := flag.Bool(\"v\", false, \"debug log\")\n\tflag.Parse()\n\tif flag.NArg() < 1 {\n\t\tfmt.Fprintf(os.Stderr, \"USAGE: %s [file1.xls file2.xlsx file3.tsv ...]\\n\", os.Args[0])\n\t\tfmt.Fprintf(os.Stderr, \"       Extracts contents of the tabular files to stdout\\n\")\n\t\tos.Exit(1)\n\t}\n\tgrate.Debug = *flagDebug\n\tfor _, fn := range flag.Args() {\n\t\twb, err := grate.Open(fn)\n\t\tif err != nil {\n\t\t\tfmt.Fprintln(os.Stderr, err)\n\t\t\tcontinue\n\t\t}\n\n\t\tsheets, err := wb.List()\n\t\tif err != nil {\n\t\t\twb.Close()\n\t\t\tfmt.Fprintln(os.Stderr, err)\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, s := range sheets {\n\t\t\tsheet, err := wb.Get(s)\n\t\t\tif err != nil {\n\t\t\t\tfmt.Fprintln(os.Stderr, err)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfor sheet.Next() {\n\t\t\t\tif *flagDebug {\n\t\t\t\t\tdtypes := sheet.Types()\n\t\t\t\t\tfmt.Println(strings.Join(dtypes, \"\\t\"))\n\t\t\t\t}\n\t\t\t\trow := sheet.Strings()\n\t\t\t\tfmt.Println(strings.Join(row, \"\\t\"))\n\t\t\t}\n\t\t}\n\t\twb.Close()\n\t}\n}\n"
  },
  {
    "path": "commonxl/cell.go",
    "content": "package commonxl\n\nimport (\n\t\"fmt\"\n\t\"math\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"time\"\n\t\"unicode/utf16\"\n)\n\n// CellType annotates the type of data extracted in the cell.\ntype CellType uint16\n\n// CellType annotations for various cell value types.\nconst (\n\tBlankCell CellType = iota\n\tIntegerCell\n\tFloatCell\n\tStringCell\n\tBooleanCell\n\tDateCell\n\n\tHyperlinkStringCell // internal type to separate URLs\n\tStaticCell          // placeholder, internal use only\n)\n\n// String returns a string description of the cell data type.\nfunc (c CellType) String() string {\n\tswitch c {\n\tcase BlankCell:\n\t\treturn \"blank\"\n\tcase IntegerCell:\n\t\treturn \"integer\"\n\tcase FloatCell:\n\t\treturn \"float\"\n\tcase BooleanCell:\n\t\treturn \"boolean\"\n\tcase DateCell:\n\t\treturn \"date\"\n\tcase HyperlinkStringCell:\n\t\treturn \"hyperlink\"\n\tcase StaticCell:\n\t\treturn \"static\"\n\tdefault: // StringCell, StaticCell\n\t\treturn \"string\"\n\t}\n}\n\n// Cell represents a single cell value.\ntype Cell []interface{}\n\n// internally, it is a slice sized 2 or 3\n//   [Value, CellType] or [Value, CellType, FormatNumber]\n// where FormatNumber is a uint16 if not 0\n\n// Value returns the contents as a generic interface{}.\nfunc (c Cell) Value() interface{} {\n\tif len(c) == 0 {\n\t\treturn \"\"\n\t}\n\treturn c[0]\n}\n\n// SetURL adds a URL hyperlink to the cell.\nfunc (c *Cell) SetURL(link string) {\n\t(*c)[1] = HyperlinkStringCell\n\tif len(*c) == 2 {\n\t\t*c = append(*c, uint16(0), link)\n\t} else { // len = 3 already\n\t\t*c = append(*c, link)\n\t}\n}\n\n// URL returns the parsed URL when a cell contains a hyperlink.\nfunc (c Cell) URL() (*url.URL, bool) {\n\tif c.Type() == HyperlinkStringCell && len(c) >= 4 {\n\t\tu, err := url.Parse(c[3].(string))\n\t\treturn u, err == nil\n\t}\n\treturn nil, false\n}\n\n// Type returns the CellType of the value.\nfunc (c Cell) Type() CellType {\n\tif len(c) < 2 {\n\t\treturn BlankCell\n\t}\n\treturn c[1].(CellType)\n}\n\n// FormatNo returns the NumberFormat used for display.\nfunc (c Cell) FormatNo() uint16 {\n\tif len(c) == 3 {\n\t\treturn c[2].(uint16)\n\t}\n\treturn 0\n}\n\n// Clone returns the new copy of this Cell.\nfunc (c Cell) Clone() Cell {\n\tc2 := make([]interface{}, len(c))\n\tfor i, x := range c {\n\t\tc2[i] = x\n\t}\n\treturn c2\n}\n\n///////\n\nvar boolStrings = map[string]bool{\n\t\"yes\": true, \"true\": true, \"t\": true, \"y\": true, \"1\": true, \"on\": true,\n\t\"no\": false, \"false\": false, \"f\": false, \"n\": false, \"0\": false, \"off\": false,\n\t\"YES\": true, \"TRUE\": true, \"T\": true, \"Y\": true, \"1.0\": true, \"ON\": true,\n\t\"NO\": false, \"FALSE\": false, \"F\": false, \"N\": false, \"0.0\": false, \"OFF\": false,\n}\n\n// NewCellWithType creates a new cell value with the given type, coercing as necessary.\nfunc NewCellWithType(value interface{}, t CellType, f *Formatter) Cell {\n\tc := NewCell(value)\n\tif c[1] == t {\n\t\t// fast path if it was already typed correctly\n\t\treturn c\n\t}\n\n\tif c[1] == BooleanCell {\n\t\tif t == IntegerCell {\n\t\t\tif c[0].(bool) {\n\t\t\t\tc[0] = int64(1)\n\t\t\t} else {\n\t\t\t\tc[0] = int64(0)\n\t\t\t}\n\t\t\tc[1] = IntegerCell\n\t\t} else if t == FloatCell {\n\t\t\tif c[0].(bool) {\n\t\t\t\tc[0] = float64(1.0)\n\t\t\t} else {\n\t\t\t\tc[0] = float64(0.0)\n\t\t\t}\n\t\t\tc[1] = FloatCell\n\t\t} else if t == StringCell {\n\t\t\tif c[0].(bool) {\n\t\t\t\tc[0] = \"TRUE\"\n\t\t\t} else {\n\t\t\t\tc[0] = \"FALSE\"\n\t\t\t}\n\t\t\tc[1] = FloatCell\n\t\t}\n\t}\n\n\tif c[1] == FloatCell {\n\t\tif t == IntegerCell {\n\t\t\tc[0] = int64(c[0].(float64))\n\t\t\tc[1] = IntegerCell\n\t\t} else if t == BooleanCell {\n\t\t\tc[0] = c[0].(float64) != 0.0\n\t\t\tc[1] = BooleanCell\n\t\t}\n\t}\n\tif c[1] == IntegerCell {\n\t\tif t == FloatCell {\n\t\t\tc[0] = float64(c[0].(int64))\n\t\t\tc[1] = FloatCell\n\t\t} else if t == BooleanCell {\n\t\t\tc[0] = c[0].(int64) != 0\n\t\t\tc[1] = BooleanCell\n\t\t}\n\t}\n\tif c[1] == StringCell {\n\t\tif t == IntegerCell {\n\t\t\tx, _ := strconv.ParseInt(c[0].(string), 10, 64)\n\t\t\tc[0] = x\n\t\t\tc[1] = IntegerCell\n\t\t} else if t == FloatCell {\n\t\t\tx, _ := strconv.ParseFloat(c[0].(string), 64)\n\t\t\tc[0] = x\n\t\t\tc[1] = FloatCell\n\t\t} else if t == BooleanCell {\n\t\t\tc[0] = boolStrings[c[0].(string)]\n\t\t\tc[1] = BooleanCell\n\t\t}\n\t}\n\tif t == StringCell {\n\t\tc[0] = fmt.Sprint(c[0])\n\t\tc[1] = StringCell\n\t}\n\tif t == DateCell {\n\t\tif c[1] == FloatCell {\n\t\t\tc[0] = f.ConvertToDate(c[0].(float64))\n\t\t} else if c[1] == IntegerCell {\n\t\t\tc[0] = f.ConvertToDate(float64(c[0].(int64)))\n\t\t}\n\t\tc[1] = DateCell\n\t}\n\treturn c\n}\n\n// NewCell creates a new cell value from any builtin type.\nfunc NewCell(value interface{}) Cell {\n\tc := make([]interface{}, 2)\n\tswitch v := value.(type) {\n\tcase bool:\n\t\tc[0] = v\n\t\tc[1] = BooleanCell\n\tcase int:\n\t\tc[0] = int64(v)\n\t\tc[1] = IntegerCell\n\tcase int8:\n\t\tc[0] = int64(v)\n\t\tc[1] = IntegerCell\n\tcase int16:\n\t\tc[0] = int64(v)\n\t\tc[1] = IntegerCell\n\tcase int32:\n\t\tc[0] = int64(v)\n\t\tc[1] = IntegerCell\n\tcase int64:\n\t\tc[0] = int64(v)\n\t\tc[1] = IntegerCell\n\tcase uint8:\n\t\tc[0] = int64(v)\n\t\tc[1] = IntegerCell\n\tcase uint16:\n\t\tc[0] = int64(v)\n\t\tc[1] = IntegerCell\n\tcase uint32:\n\t\tc[0] = int64(v)\n\t\tc[1] = IntegerCell\n\n\tcase uint:\n\t\tif int64(v) > int64(math.MaxInt64) {\n\t\t\tc[0] = float64(v)\n\t\t\tc[1] = FloatCell\n\t\t} else {\n\t\t\tc[0] = int64(v)\n\t\t\tc[1] = IntegerCell\n\t\t}\n\tcase uint64:\n\t\tif v > math.MaxInt64 {\n\t\t\tc[0] = float64(v)\n\t\t\tc[1] = FloatCell\n\t\t} else {\n\t\t\tc[0] = int64(v)\n\t\t\tc[1] = IntegerCell\n\t\t}\n\n\tcase float32:\n\t\tc[0] = float64(v)\n\t\tc[1] = FloatCell\n\tcase float64:\n\t\tc[0] = float64(v)\n\t\tc[1] = FloatCell\n\n\tcase string:\n\t\tif len(v) == 0 {\n\t\t\tc[0] = nil\n\t\t\tc[1] = BlankCell\n\t\t} else {\n\t\t\tc[0] = v\n\t\t\tc[1] = StringCell\n\t\t}\n\tcase []byte:\n\t\tif len(v) == 0 {\n\t\t\tc[0] = nil\n\t\t\tc[1] = BlankCell\n\t\t} else {\n\t\t\tc[0] = string(v)\n\t\t\tc[1] = StringCell\n\t\t}\n\tcase []uint16:\n\t\tif len(v) == 0 {\n\t\t\tc[0] = nil\n\t\t\tc[1] = BlankCell\n\t\t} else {\n\t\t\tc[0] = string(utf16.Decode(v))\n\t\t\tc[1] = StringCell\n\t\t}\n\tcase []rune:\n\t\tif len(v) == 0 {\n\t\t\tc[0] = nil\n\t\t\tc[1] = BlankCell\n\t\t} else {\n\t\t\tc[0] = string(v)\n\t\t\tc[1] = StringCell\n\t\t}\n\tcase time.Time:\n\t\tc[0] = v\n\t\tc[1] = DateCell\n\n\tcase fmt.Stringer:\n\t\ts := v.String()\n\t\tif len(s) == 0 {\n\t\t\tc[0] = nil\n\t\t\tc[1] = BlankCell\n\t\t} else {\n\t\t\tc[0] = s\n\t\t\tc[1] = StringCell\n\t\t}\n\tdefault:\n\t\tpanic(\"grate: data type not handled\")\n\t}\n\treturn Cell(c)\n}\n\n// SetFormatNumber changes the number format stored with the cell.\nfunc (c *Cell) SetFormatNumber(f uint16) {\n\tif f == 0 {\n\t\t*c = (*c)[:2]\n\t\treturn\n\t}\n\n\tif len(*c) == 2 {\n\t\t*c = append(*c, f)\n\t} else {\n\t\t(*c)[2] = f\n\t}\n}\n\nfunc (c Cell) Equal(other Cell) bool {\n\tif c.Type() == FloatCell || other.Type() == FloatCell ||\n\t\tc.Type() == IntegerCell || other.Type() == IntegerCell {\n\t\tv1, ok := c[0].(float64)\n\t\tv1x, okx := c[0].(int64)\n\t\tif okx {\n\t\t\tv1 = float64(v1x)\n\t\t\tok = true\n\t\t}\n\t\tif !ok {\n\t\t\tfmt.Sscanf(fmt.Sprint(c[0]), \"%g\", &v1)\n\t\t}\n\t\tv2, ok := other[0].(float64)\n\t\tv2x, okx := other[0].(int64)\n\t\tif okx {\n\t\t\tv2 = float64(v2x)\n\t\t\tok = true\n\t\t}\n\t\tif !ok {\n\t\t\tfmt.Sscanf(fmt.Sprint(c[0]), \"%g\", &v2)\n\t\t}\n\t\treturn v1 == v2\n\t}\n\n\treturn c.Less(other) == other.Less(c)\n}\n\nfunc (c Cell) Less(other Cell) bool {\n\tif len(c) == 0 {\n\t\treturn false\n\t}\n\tswitch v1 := c[0].(type) {\n\tcase nil:\n\t\treturn false\n\tcase bool:\n\t\t// F < T = T\n\t\t// F < F = F\n\t\t// T < T = F\n\t\t// T < F = F\n\t\tif v1 {\n\t\t\treturn false\n\t\t}\n\n\t\t// if v2 is truthy, return true\n\t\tswitch v2 := other[0].(type) {\n\t\tcase nil:\n\t\t\treturn false\n\t\tcase bool:\n\t\t\treturn v2\n\t\tcase int64:\n\t\t\treturn v2 != 0\n\t\tcase float64:\n\t\t\treturn v2 != 0.0\n\t\tcase string:\n\t\t\treturn boolStrings[v2]\n\t\t}\n\n\tcase int64:\n\t\t// v1 < v2\n\n\t\tswitch v2 := other[0].(type) {\n\t\tcase nil:\n\t\t\treturn false\n\t\tcase bool:\n\t\t\tx := int64(0)\n\t\t\tif v2 {\n\t\t\t\tx = 1\n\t\t\t}\n\t\t\treturn v1 < x\n\t\tcase int64:\n\t\t\treturn v1 < v2\n\t\tcase float64:\n\t\t\tif v2 < math.MinInt64 {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif v2 > math.MaxInt64 {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\treturn float64(v1) < v2\n\t\tcase string:\n\t\t\tvar x int64\n\t\t\t_, err := fmt.Sscanf(v2, \"%d\", &x)\n\t\t\tif err == nil {\n\t\t\t\treturn v1 < x\n\t\t\t}\n\t\t\treturn fmt.Sprint(v1) < v2\n\t\t}\n\tcase float64:\n\t\tswitch v2 := other[0].(type) {\n\t\tcase nil:\n\t\t\treturn false\n\t\tcase bool:\n\t\t\tx := float64(0.0)\n\t\t\tif v2 {\n\t\t\t\tx = 1.0\n\t\t\t}\n\t\t\treturn v1 < x\n\t\tcase int64:\n\t\t\tif v1 < math.MinInt64 {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tif v1 > math.MaxInt64 {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn v1 < float64(v2)\n\t\tcase float64:\n\t\t\treturn v1 < v2\n\t\tcase string:\n\t\t\tvar x float64\n\t\t\t_, err := fmt.Sscanf(v2, \"%g\", &x)\n\t\t\tif err == nil {\n\t\t\t\treturn v1 < x\n\t\t\t}\n\t\t\treturn fmt.Sprint(v1) < v2\n\t\t}\n\tcase string:\n\t\t//return v1 < fmt.Sprint(other[0])\n\n\t\tswitch v2 := other[0].(type) {\n\t\tcase nil:\n\t\t\treturn false\n\t\tcase bool:\n\t\t\treturn v2 && !boolStrings[v1]\n\t\tcase int64:\n\t\t\tvar x int64\n\t\t\t_, err := fmt.Sscanf(v1, \"%d\", &x)\n\t\t\tif err == nil {\n\t\t\t\treturn x < v2\n\t\t\t}\n\t\t\treturn v1 < fmt.Sprint(v2)\n\t\tcase float64:\n\t\t\tvar x float64\n\t\t\t_, err := fmt.Sscanf(v1, \"%g\", &x)\n\t\t\tif err == nil {\n\t\t\t\treturn x < v2\n\t\t\t}\n\t\t\treturn v1 < fmt.Sprint(v2)\n\t\tcase string:\n\t\t\treturn v1 < v2\n\t\t}\n\n\t}\n\n\tpanic(\"unable to compare cells (invalid internal type)\")\n}\n"
  },
  {
    "path": "commonxl/dates.go",
    "content": "package commonxl\n\nimport (\n\t\"strings\"\n\t\"time\"\n)\n\n// ConvertToDate converts a floating-point value using the\n// Excel date serialization conventions.\nfunc (x *Formatter) ConvertToDate(val float64) time.Time {\n\t// http://web.archive.org/web/20190808062235/http://aa.usno.navy.mil/faq/docs/JD_Formula.php\n\tv := int(val)\n\tif v < 61 {\n\t\tjdate := val + 0.5\n\t\tif (x.flags & fMode1904) != 0 {\n\t\t\tjdate += 2416480.5\n\t\t} else {\n\t\t\tjdate += 2415018.5\n\t\t}\n\t\tJD := int(jdate)\n\t\tfrac := jdate - float64(JD)\n\n\t\tL := JD + 68569\n\t\tN := 4 * L / 146097\n\t\tL = L - (146097*N+3)/4\n\t\tI := 4000 * (L + 1) / 1461001\n\t\tL = L - 1461*I/4 + 31\n\t\tJ := 80 * L / 2447\n\t\tday := L - 2447*J/80\n\t\tL = J / 11\n\t\tmonth := time.Month(J + 2 - 12*L)\n\t\tyear := 100*(N-49) + I + L\n\n\t\tt := time.Duration(float64(time.Hour*24) * frac)\n\t\treturn time.Date(year, month, day, 0, 0, 0, 0, time.UTC).Add(t)\n\t}\n\tfrac := val - float64(v)\n\tdate := time.Date(1904, 1, 1, 0, 0, 0, 0, time.UTC)\n\tif (x.flags & fMode1904) == 0 {\n\t\tdate = time.Date(1899, 12, 30, 0, 0, 0, 0, time.UTC)\n\t}\n\n\tt := time.Duration(float64(time.Hour*24) * frac)\n\treturn date.AddDate(0, 0, v).Add(t)\n}\n\nfunc timeFmtFunc(f string) FmtFunc {\n\treturn func(x *Formatter, v interface{}) string {\n\t\tt, ok := v.(time.Time)\n\t\tif !ok {\n\t\t\tfval, ok := convertToFloat64(v)\n\t\t\tif !ok {\n\t\t\t\treturn \"MUST BE time.Time OR numeric TO FORMAT CORRECTLY\"\n\t\t\t}\n\t\t\tt = x.ConvertToDate(fval)\n\t\t}\n\t\t//log.Println(\"formatting date\", t, \"with\", f, \"=\", t.Format(f))\n\t\treturn t.Format(f)\n\t}\n}\n\n// same as above but replaces \"AM\" and \"PM\" with chinese translations.\n// TODO: implement others\nfunc cnTimeFmtFunc(f string) FmtFunc {\n\treturn func(x *Formatter, v interface{}) string {\n\t\tt, ok := v.(time.Time)\n\t\tif !ok {\n\t\t\tfval, ok := convertToFloat64(v)\n\t\t\tif !ok {\n\t\t\t\treturn \"MUST BE time.Time OR numeric TO FORMAT CORRECTLY\"\n\t\t\t}\n\t\t\tt = x.ConvertToDate(fval)\n\t\t}\n\t\ts := t.Format(f)\n\t\ts = strings.Replace(s, `AM`, `上午`, 1)\n\t\treturn strings.Replace(s, `PM`, `下午`, 1)\n\t}\n}\n"
  },
  {
    "path": "commonxl/fmt.go",
    "content": "package commonxl\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n)\n\n// FmtFunc will format a value according to the designated style.\ntype FmtFunc func(*Formatter, interface{}) string\n\nfunc staticFmtFunc(s string) FmtFunc {\n\treturn func(x *Formatter, v interface{}) string {\n\t\treturn s\n\t}\n}\n\nfunc surround(pre string, ff FmtFunc, post string) FmtFunc {\n\treturn func(x *Formatter, v interface{}) string {\n\t\treturn pre + ff(x, v) + post\n\t}\n}\n\nfunc addNegParens(ff FmtFunc) FmtFunc {\n\treturn func(x *Formatter, v interface{}) string {\n\t\ts1 := ff(x, v)\n\t\tif s1[0] == '-' {\n\t\t\treturn \"(\" + s1[1:] + \")\"\n\t\t}\n\t\treturn s1\n\t}\n}\n\nfunc addCommas(ff FmtFunc) FmtFunc {\n\treturn func(x *Formatter, v interface{}) string {\n\t\ts1 := ff(x, v)\n\t\tisNeg := false\n\t\tif s1[0] == '-' {\n\t\t\tisNeg = true\n\t\t\ts1 = s1[1:]\n\t\t}\n\t\tendIndex := strings.IndexAny(s1, \".eE\")\n\t\tif endIndex < 0 {\n\t\t\tendIndex = len(s1)\n\t\t}\n\t\tfor endIndex > 3 {\n\t\t\tendIndex -= 3\n\t\t\ts1 = s1[:endIndex] + \",\" + s1[endIndex:]\n\t\t}\n\t\tif isNeg {\n\t\t\treturn \"-\" + s1\n\t\t}\n\t\treturn s1\n\t}\n}\n\nfunc identFunc(x *Formatter, v interface{}) string {\n\tswitch x := v.(type) {\n\tcase bool:\n\t\tif x {\n\t\t\treturn \"TRUE\"\n\t\t}\n\t\treturn \"FALSE\"\n\tcase int64:\n\t\ts := strconv.FormatInt(x, 10)\n\t\tif len(s) <= 11 {\n\t\t\treturn s\n\t\t}\n\tcase float64:\n\t\ts := strconv.FormatFloat(x, 'f', -1, 64)\n\t\tif len(s) <= 11 || (len(s) == 12 && x < 0) {\n\t\t\treturn s\n\t\t}\n\t\ts = strconv.FormatFloat(x, 'g', 6, 64)\n\t\tif len(s) <= 11 {\n\t\t\treturn s\n\t\t}\n\tcase string:\n\t\treturn x\n\tcase fmt.Stringer:\n\t\treturn x.String()\n\t}\n\treturn fmt.Sprint(v)\n}\n\nfunc sprintfFunc(fs string, mul int) FmtFunc {\n\twantInt64 := strings.Contains(fs, \"%d\")\n\treturn func(x *Formatter, v interface{}) string {\n\t\tswitch val := v.(type) {\n\t\tcase int, uint, int64, uint64, int32, uint32, uint16, int16:\n\t\t\treturn fmt.Sprintf(fs, v)\n\n\t\tcase float64:\n\t\t\tval *= float64(mul)\n\t\t\tif wantInt64 {\n\t\t\t\tv2 := int64(val)\n\t\t\t\treturn fmt.Sprintf(fs, v2)\n\t\t\t}\n\t\t\treturn fmt.Sprintf(fs, val)\n\t\t}\n\t\treturn fmt.Sprint(v)\n\t}\n}\n\nfunc convertToInt64(v interface{}) (int64, bool) {\n\tx, ok := convertToFloat64(v)\n\treturn int64(x), ok\n}\n\nfunc convertToFloat64(v interface{}) (float64, bool) {\n\tswitch val := v.(type) {\n\tcase float64:\n\t\treturn val, true\n\tcase bool:\n\t\tif val {\n\t\t\treturn 1.0, true\n\t\t}\n\t\treturn 0.0, true\n\tcase int:\n\t\treturn float64(val), true\n\tcase int8:\n\t\treturn float64(val), true\n\tcase int16:\n\t\treturn float64(val), true\n\tcase int32:\n\t\treturn float64(val), true\n\tcase int64:\n\t\treturn float64(val), true\n\tcase uint:\n\t\treturn float64(val), true\n\tcase uint8:\n\t\treturn float64(val), true\n\tcase uint16:\n\t\treturn float64(val), true\n\tcase uint32:\n\t\treturn float64(val), true\n\tcase uint64:\n\t\treturn float64(val), true\n\tcase float32:\n\t\treturn float64(val), true\n\tcase string:\n\t\tnf, err := strconv.ParseFloat(val, 64)\n\t\treturn nf, err == nil\n\tdefault:\n\t\treturn 0.0, false\n\t}\n}\n\n// replaces a zero with a dash\nfunc zeroDashFunc(ff FmtFunc) FmtFunc {\n\treturn func(x *Formatter, v interface{}) string {\n\t\tfval, ok := convertToFloat64(v)\n\t\tif !ok {\n\t\t\t// strings etc returned as-is\n\t\t\treturn fmt.Sprint(v)\n\t\t}\n\t\tif fval == 0.0 {\n\t\t\treturn \"-\"\n\t\t}\n\t\treturn ff(x, v)\n\t}\n}\n\nfunc fracFmtFunc(n int) FmtFunc {\n\treturn func(x *Formatter, v interface{}) string {\n\t\tf, ok := convertToFloat64(v)\n\t\tif !ok {\n\t\t\treturn \"MUST BE numeric TO FORMAT CORRECTLY\"\n\t\t}\n\t\tw, n, d := DecimalToWholeFraction(f, n, n)\n\t\tif n == 0 {\n\t\t\treturn fmt.Sprintf(\"%d\", w)\n\t\t}\n\t\tif w == 0 {\n\t\t\tif f < 0 && n > 0 {\n\t\t\t\tn = -n\n\t\t\t}\n\t\t\treturn fmt.Sprintf(\"%d/%d\", n, d)\n\t\t}\n\t\treturn fmt.Sprintf(\"%d %d/%d\", w, n, d)\n\t}\n}\n\n// handle (up to) all four format cases:\n// positive;negative;zero;other\nfunc switchFmtFunc(pos FmtFunc, others ...FmtFunc) FmtFunc {\n\tstringFF := identFunc\n\tzeroFF := pos\n\tnegFF := pos\n\tif len(others) > 0 {\n\t\tnegFF = others[0]\n\t\tif len(others) > 1 {\n\t\t\tzeroFF = others[1]\n\t\t\tif len(others) > 2 {\n\t\t\t\tstringFF = others[2]\n\t\t\t}\n\t\t}\n\t}\n\treturn func(x *Formatter, v interface{}) string {\n\t\tval, ok := convertToFloat64(v)\n\t\tif !ok {\n\t\t\treturn stringFF(x, v)\n\t\t}\n\t\tif val == 0.0 {\n\t\t\treturn zeroFF(x, v)\n\t\t}\n\t\tif val < 0.0 {\n\t\t\treturn negFF(x, v)\n\t\t}\n\t\treturn pos(x, v)\n\t}\n}\n\n// mapping of standard built-ins to Go date format funcs.\nvar goFormatters = map[uint16]FmtFunc{\n\t0:  identFunc, // FIXME: better \"general\" formatter\n\t49: identFunc,\n\n\t14: timeFmtFunc(`01-02-06`),\n\t15: timeFmtFunc(`2-Jan-06`),\n\t16: timeFmtFunc(`2-Jan`),\n\t17: timeFmtFunc(`Jan-06`),\n\t20: timeFmtFunc(`15:04`),\n\t21: timeFmtFunc(`15:04:05`),\n\t22: timeFmtFunc(`1/2/06 15:04`),\n\t45: timeFmtFunc(`04:05`),\n\t46: timeFmtFunc(`3:04:05`),\n\t47: timeFmtFunc(`0405.9`),\n\t27: timeFmtFunc(`2006\"年\"1\"月\"`),\n\t28: timeFmtFunc(`1\"月\"2\"日\"`),\n\t29: timeFmtFunc(`1\"月\"2\"日\"`),\n\t30: timeFmtFunc(`1-2-06`),\n\t31: timeFmtFunc(`2006\"年\"1\"月\"2\"日\"`),\n\t32: timeFmtFunc(`15\"时\"04\"分\"`),\n\t33: timeFmtFunc(`15\"时\"04\"分\"05\"秒\"`),\n\t36: timeFmtFunc(`2006\"年\"2\"月\"`),\n\t50: timeFmtFunc(`2006\"年\"2\"月\"`),\n\t51: timeFmtFunc(`1\"月\"2\"日\"`),\n\t52: timeFmtFunc(`2006\"年\"1\"月\"`),\n\t53: timeFmtFunc(`1\"月\"2\"日\"`),\n\t54: timeFmtFunc(`1\"月\"2\"日\"`),\n\t57: timeFmtFunc(`2006\"年\"1\"月\"`),\n\t58: timeFmtFunc(`1\"月\"2\"日\"`),\n\t71: timeFmtFunc(`2/1/2006`),\n\t72: timeFmtFunc(`2-Jan-06`),\n\t73: timeFmtFunc(`2-Jan`),\n\t74: timeFmtFunc(`Jan-06`),\n\t75: timeFmtFunc(`15:04`),\n\t76: timeFmtFunc(`15:04:05`),\n\t77: timeFmtFunc(`2/1/2006 15:04`),\n\t78: timeFmtFunc(`04:05`),\n\t79: timeFmtFunc(`15:04:05`),\n\t80: timeFmtFunc(`04:05.9`),\n\t81: timeFmtFunc(`2/1/06`),\n\t18: timeFmtFunc(`3:04 PM`),\n\t19: timeFmtFunc(`3:04:05 PM`),\n\n\t34: cnTimeFmtFunc(`PM 3\"时\"04\"分\"`),\n\t35: cnTimeFmtFunc(`PM 3\"时\"04\"分\"05\"秒\"`),\n\t55: cnTimeFmtFunc(`PM 3\"时\"04\"分\"`),\n\t56: cnTimeFmtFunc(`PM 3\"时\"04\"分\"05\"秒`),\n\n\t12: fracFmtFunc(1),\n\t13: fracFmtFunc(2),\n\n\t69: fracFmtFunc(1),\n\t70: fracFmtFunc(2),\n\n\t1:  sprintfFunc(`%d`, 1),\n\t2:  sprintfFunc(`%4.2f`, 1),\n\t59: sprintfFunc(`%d`, 1),\n\t60: sprintfFunc(`%4.2f`, 1),\n\n\t9:  sprintfFunc(`%d%%`, 100),\n\t10: sprintfFunc(`%4.2f%%`, 100),\n\t67: sprintfFunc(`%d%%`, 100),\n\t68: sprintfFunc(`%4.2f%%`, 100),\n\n\t3:  addCommas(sprintfFunc(\"%d\", 1)),\n\t61: addCommas(sprintfFunc(\"%d\", 1)),\n\t37: addNegParens(addCommas(sprintfFunc(\"%d\", 1))),\n\t38: addNegParens(addCommas(sprintfFunc(\"%d\", 1))),\n\n\t4:  addCommas(sprintfFunc(\"%4.2f\", 1)),\n\t62: addCommas(sprintfFunc(\"%4.2f\", 1)),\n\t39: addNegParens(addCommas(sprintfFunc(\"%4.2f\", 1))),\n\t40: addNegParens(addCommas(sprintfFunc(\"%4.2f\", 1))),\n\n\t11: sprintfFunc(`%4.2E`, 1),\n\t48: sprintfFunc(`%3.1E`, 1),\n\n\t41: zeroDashFunc(addCommas(sprintfFunc(\"%d\", 1))),\n\t43: zeroDashFunc(addCommas(sprintfFunc(\"%4.2f\", 1))),\n\n\t42: switchFmtFunc(\n\t\tsurround(\"$\", addCommas(sprintfFunc(\"%d\", 1)), \"\"),\n\t\tsurround(\"$(\", addCommas(sprintfFunc(\"%d\", 1)), \")\"),\n\t\tstaticFmtFunc(\"$-\")),\n\t44: switchFmtFunc(\n\t\tsurround(\"$\", addCommas(sprintfFunc(\"%4.2f\", 1)), \"\"),\n\t\tsurround(\"$(\", addCommas(sprintfFunc(\"%4.2f\", 1)), \")\"),\n\t\tstaticFmtFunc(\"$-\")),\n}\n"
  },
  {
    "path": "commonxl/fmt_test.go",
    "content": "package commonxl\n\nimport (\n\t\"log\"\n\t\"testing\"\n\t\"time\"\n)\n\ntype testcaseNums struct {\n\tv interface{}\n\ts string\n}\n\nvar commas = []testcaseNums{\n\t{10, \"10\"},\n\t{float64(10), \"10\"},\n\t{float64(10) + 0.12345, \"10.12345\"},\n\t{-10, \"-10\"},\n\t{float64(-10), \"-10\"},\n\t{float64(-10) + 0.12345, \"-9.87655\"},\n\t{uint16(10), \"10\"},\n\t{100, \"100\"},\n\t{float64(100), \"100\"},\n\t{float64(100) + 0.12345, \"100.12345\"},\n\t{-100, \"-100\"},\n\t{float64(-100), \"-100\"},\n\t{float64(-100) + 0.12345, \"-99.87655\"},\n\t{uint16(100), \"100\"},\n\t{1000, \"1,000\"},\n\t{float64(1000), \"1,000\"},\n\t{float64(1000) + 0.12345, \"1,000.12345\"},\n\t{-1000, \"-1,000\"},\n\t{float64(-1000), \"-1,000\"},\n\t{float64(-1000) + 0.12345, \"-999.87655\"},\n\t{uint16(1000), \"1,000\"},\n\t{10000, \"10,000\"},\n\t{float64(10000), \"10,000\"},\n\t{float64(10000) + 0.12345, \"10,000.12345\"},\n\t{-10000, \"-10,000\"},\n\t{float64(-10000), \"-10,000\"},\n\t{float64(-10000) + 0.12345, \"-9,999.87655\"},\n\t{uint16(10000), \"10,000\"},\n\t{100000, \"100,000\"},\n\t{float64(100000), \"100,000\"},\n\t{float64(100000) + 0.12345, \"100,000.12345\"},\n\t{-100000, \"-100,000\"},\n\t{float64(-100000), \"-100,000\"},\n\t{float64(-100000) + 0.12345, \"-99,999.87655\"},\n\t{uint64(100000), \"100,000\"},\n\t{1000000, \"1,000,000\"},\n\t{float64(1000000), \"1e+06\"},\n\t{float64(1000000) + 0.12345, \"1.00000012345e+06\"},\n\t{-1000000, \"-1,000,000\"},\n\t{float64(-1000000), \"-1e+06\"},\n\t{float64(-1000000) + 0.12345, \"-999,999.87655\"},\n\t{uint64(1000000), \"1,000,000\"},\n\t{10000000, \"10,000,000\"},\n\t{float64(10000000), \"1e+07\"},\n\t{float64(10000000) + 0.12345, \"1.000000012345e+07\"},\n\t{-10000000, \"-10,000,000\"},\n\t{float64(-10000000), \"-1e+07\"},\n\t{float64(-10000000) + 0.12345, \"-9.99999987655e+06\"},\n\t{uint64(10000000), \"10,000,000\"},\n\t{100000000, \"100,000,000\"},\n\t{float64(100000000), \"1e+08\"},\n\t{float64(100000000) + 0.12345, \"1.0000000012345e+08\"},\n\t{-100000000, \"-100,000,000\"},\n\t{float64(-100000000), \"-1e+08\"},\n\t{float64(-100000000) + 0.12345, \"-9.999999987655e+07\"},\n\t{uint64(100000000), \"100,000,000\"},\n}\n\nfunc TestCommas(t *testing.T) {\n\tcf := addCommas(identFunc)\n\tfor _, c := range commas {\n\t\tfs := cf(nil, c.v)\n\t\tif c.s != fs {\n\t\t\tt.Fatalf(\"commas failed: get '%s' but expected '%s' for %T(%v)\",\n\t\t\t\tfs, c.s, c.v, c.v)\n\t\t}\n\t}\n}\n\nfunc TestDateFormats(t *testing.T) {\n\tvar testDates = []time.Time{\n\t\ttime.Date(1901, 7, 11, 1, 5, 0, 0, time.UTC),\n\t\ttime.Date(1905, 7, 11, 4, 10, 0, 0, time.UTC),\n\t\ttime.Date(1904, 7, 11, 8, 15, 0, 0, time.UTC),\n\t\ttime.Date(1993, 7, 11, 12, 20, 0, 0, time.UTC),\n\t\ttime.Date(1983, 7, 11, 16, 30, 0, 0, time.UTC),\n\t\ttime.Date(1983, 7, 11, 20, 45, 0, 0, time.UTC),\n\t\ttime.Date(2000, 12, 31, 23, 59, 0, 0, time.UTC),\n\t\ttime.Date(2002, 12, 31, 23, 59, 0, 0, time.UTC),\n\t\ttime.Date(2012, 3, 10, 9, 30, 0, 0, time.UTC),\n\t\ttime.Date(2014, 3, 27, 9, 37, 0, 0, time.UTC),\n\t}\n\n\tfx := &Formatter{}\n\tfor _, t := range testDates {\n\t\tfor fid, ctype := range builtInFormatTypes {\n\t\t\tif ctype != DateCell {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tff, _ := goFormatters[fid]\n\t\t\t// mainly testing these don't crash...\n\t\t\tlog.Println(ff(fx, t))\n\t\t}\n\t}\n}\nfunc TestBoolFormats(t *testing.T) {\n\tff, _ := makeFormatter(`\"yes\";\"yes\";\"no\"`)\n\n\tif \"no\" != ff(nil, false) {\n\t\tt.Fatal(`false should be \"no\"`)\n\t}\n\tif \"no\" != ff(nil, 0) {\n\t\tt.Fatal(`0 should be \"no\"`)\n\t}\n\tif \"no\" != ff(nil, 0.0) {\n\t\tt.Fatal(`0.0 should be \"no\"`)\n\t}\n\n\t/////\n\n\tif \"yes\" != ff(nil, true) {\n\t\tt.Fatal(`true should be \"yes\"`)\n\t}\n\tif \"yes\" != ff(nil, 99) {\n\t\tt.Fatal(`99 should be \"yes\"`)\n\t}\n\tif \"yes\" != ff(nil, -4) {\n\t\tt.Fatal(`-4 should be \"yes\"`)\n\t}\n\n\tif \"yes\" != ff(nil, 4.0) {\n\t\tt.Fatal(`4.0 should be \"yes\"`)\n\t}\n\tif \"yes\" != ff(nil, -99.0) {\n\t\tt.Fatal(`-99.0 should be \"yes\"`)\n\t}\n}\n"
  },
  {
    "path": "commonxl/formats.go",
    "content": "package commonxl\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"regexp\"\n\t\"strings\"\n)\n\n// Formatter contains formatting methods common to Excel spreadsheets.\ntype Formatter struct {\n\tflags           uint64\n\tcustomCodes     map[uint16]FmtFunc\n\tcustomCodeTypes map[uint16]CellType\n}\n\nconst (\n\tfMode1904 uint64 = 1\n)\n\n// Mode1904 indicates that dates start on Jan 1, 1904\n// this setting was used in early MacOS Excel applications.\nfunc (x *Formatter) Mode1904(enabled bool) {\n\tif enabled {\n\t\tx.flags |= fMode1904\n\t} else {\n\t\tx.flags = x.flags &^ fMode1904\n\t}\n}\n\n// Add a custom number format to the formatter.\nfunc (x *Formatter) Add(fmtID uint16, formatCode string) error {\n\tif x.customCodes == nil {\n\t\tx.customCodes = make(map[uint16]FmtFunc)\n\t\tx.customCodeTypes = make(map[uint16]CellType)\n\t}\n\tif strings.ToLower(formatCode) == \"general\" {\n\t\tx.customCodes[fmtID] = goFormatters[0]\n\t\treturn nil\n\t}\n\t_, ok := goFormatters[fmtID]\n\tif ok {\n\t\treturn errors.New(\"grate/commonxl: cannot replace default number formats\")\n\t}\n\n\t_, ok2 := x.customCodes[fmtID]\n\tif ok2 {\n\t\treturn errors.New(\"grate/commonxl: cannot replace existing number formats\")\n\t}\n\n\tx.customCodes[fmtID], x.customCodeTypes[fmtID] = makeFormatter(formatCode)\n\treturn nil\n}\n\nfunc (x *Formatter) getCellType(fmtID uint16) (CellType, bool) {\n\tif ct, ok := builtInFormatTypes[fmtID]; ok {\n\t\treturn ct, true\n\t}\n\tif x.customCodeTypes != nil {\n\t\tct, ok := x.customCodeTypes[fmtID]\n\t\treturn ct, ok\n\t}\n\treturn 0, false\n}\n\nvar (\n\tminsMatch = regexp.MustCompile(\"h.*m.*s\")\n\tnonEsc    = regexp.MustCompile(`([^\"]|^)\"`)\n\tsquash    = regexp.MustCompile(`[*_].`)\n\tfixEsc    = regexp.MustCompile(`\\\\(.)`)\n\n\tformatMatchBrackets    = regexp.MustCompile(`\\[[^\\]]*\\]`)\n\tformatMatchTextLiteral = regexp.MustCompile(`\"[^\"]*\"`)\n)\n\nfunc makeFormatter(s string) (FmtFunc, CellType) {\n\t//log.Printf(\"makeFormatter('%s')\", s)\n\t// remove any coloring marks\n\ts = formatMatchBrackets.ReplaceAllString(s, \"\")\n\tif strings.Contains(s, \";\") {\n\t\tparts := strings.Split(s, \";\")\n\t\tposFF, ctypePos := makeFormatter(parts[0])\n\t\trem := make([]FmtFunc, len(parts)-1)\n\t\tfor i, ps := range parts[1:] {\n\t\t\trem[i], _ = makeFormatter(ps)\n\t\t}\n\t\treturn switchFmtFunc(posFF, rem...), ctypePos\n\t}\n\n\t// escaped characters, and quoted text\n\ts2 := fixEsc.ReplaceAllString(s, \"\")\n\ts2 = formatMatchTextLiteral.ReplaceAllString(s, \"\")\n\n\tif strings.ContainsAny(s2, \"ymdhs\") {\n\t\t// it's a date/time format\n\n\t\tif loc := minsMatch.FindStringIndex(s); loc != nil {\n\t\t\t// m or mm in loc[0]:loc[1] is a minute format\n\t\t\tinner := s[loc[0]:loc[1]]\n\t\t\tinner = strings.Replace(inner, \"mm\", \"04\", 1)\n\t\t\tinner = strings.Replace(inner, \"m\", \"4\", 1)\n\t\t\ts = s[:loc[0]] + inner + s[loc[1]:]\n\t\t}\n\t\tdfreps := [][]string{\n\t\t\t{\"hh\", \"15\"}, {\"h\", \"15\"},\n\t\t\t{\"ss\", \"05\"}, {\"s\", \"5\"},\n\t\t\t{\"mmmmm\", \"Jan\"}, // super ambiguous, replace with 3-letter month\n\t\t\t{\"mmmm\", \"January\"}, {\"mmm\", \"Jan\"},\n\t\t\t{\"mm\", \"01\"}, {\"m\", \"1\"},\n\t\t\t{\"dddd\", \"Monday\"}, {\"ddd\", \"Mon\"},\n\t\t\t{\"dd\", \"02\"}, {\"d\", \"2\"},\n\t\t\t{\"yyyy\", \"2006\"}, {\"yy\", \"06\"},\n\t\t}\n\t\tif strings.Contains(s, \"AM\") || strings.Contains(s, \"PM\") {\n\t\t\tdfreps[0][1] = \"03\"\n\t\t\tdfreps[1][1] = \"3\"\n\t\t}\n\t\tfor _, dfr := range dfreps {\n\t\t\ts = strings.Replace(s, dfr[0], dfr[1], 1)\n\t\t}\n\n\t\ts = nonEsc.ReplaceAllString(s, `$1`)\n\t\ts = squash.ReplaceAllString(s, ``)\n\t\ts = fixEsc.ReplaceAllString(s, `$1`)\n\n\t\t//log.Printf(\"   made time formatter '%s'\", s)\n\t\treturn timeFmtFunc(s), DateCell\n\t}\n\n\tvar ff FmtFunc\n\tvar ctype CellType\n\tif strings.ContainsAny(s, \".Ee\") {\n\t\tverb := \"f\"\n\t\tif strings.ContainsAny(s, \"Ee\") {\n\t\t\tverb = \"E\"\n\t\t}\n\t\ts = regexp.MustCompile(\"[eE]+[+-]0+\").ReplaceAllString(s, \"\")\n\t\ts2 := strings.ReplaceAll(s, \",\", \"\")\n\t\ti1 := strings.IndexAny(s2, \"0\")\n\t\ti2 := strings.IndexByte(s2, '.')\n\t\ti3 := strings.LastIndexAny(s2, \"0.\")\n\t\tmul := 1\n\t\tif strings.Contains(s2, \"%\") {\n\t\t\tmul = 100\n\t\t}\n\t\tsf := fmt.Sprintf(\"%%%d.%d%s\", i3-i1, i3-i2, verb)\n\t\t//log.Printf(\"   made float formatter '%s'\", sf)\n\t\tff = sprintfFunc(sf, mul)\n\t\tctype = FloatCell\n\t} else {\n\t\ts2 := strings.ReplaceAll(s, \",\", \"\")\n\t\ti1 := strings.IndexAny(s2, \"0\")\n\t\ti2 := strings.LastIndexAny(s2, \"0.\")\n\t\tmul := 1\n\t\tif strings.Contains(s2, \"%\") {\n\t\t\tmul = 100\n\t\t}\n\t\tsf := fmt.Sprintf(\"%%%dd\", i2-i1)\n\t\tif (i2 - i1) == 0 {\n\t\t\tsf = \"%d\"\n\t\t}\n\t\t//log.Printf(\"   made int formatter '%s'\", sf)\n\t\tff = sprintfFunc(sf, mul)\n\t\tctype = IntegerCell\n\t}\n\n\tif strings.Contains(s, \",\") {\n\t\tff = addCommas(ff)\n\t\t//log.Printf(\"   added commas\")\n\t}\n\n\tsurReg := regexp.MustCompile(`[0#?,.]+`)\n\tprepost := surReg.Split(s, 2)\n\tif len(prepost) > 0 && len(prepost[0]) > 0 {\n\t\tprepost[0] = nonEsc.ReplaceAllString(prepost[0], `$1`)\n\t\tprepost[0] = squash.ReplaceAllString(prepost[0], ``)\n\t\tprepost[0] = fixEsc.ReplaceAllString(prepost[0], `$1`)\n\t}\n\tif len(prepost) == 1 {\n\t\tif prepost[0] == \"@\" {\n\t\t\treturn identFunc, StringCell\n\t\t}\n\t\t//log.Printf(\"   added static ('%s')\", prepost[0])\n\t\treturn staticFmtFunc(prepost[0]), StringCell\n\t}\n\tif len(prepost[0]) > 0 || len(prepost[1]) > 0 {\n\t\tprepost[1] = nonEsc.ReplaceAllString(prepost[1], `$1`)\n\t\tprepost[1] = squash.ReplaceAllString(prepost[1], ``)\n\t\tprepost[1] = fixEsc.ReplaceAllString(prepost[1], `$1`)\n\n\t\tff = surround(prepost[0], ff, prepost[1])\n\t\t//log.Printf(\"   added surround ('%s' ... '%s')\", prepost[0], prepost[1])\n\t}\n\n\treturn ff, ctype\n}\n\n// Get the number format func to use for formatting values,\n// it returns false when fmtID is unknown.\nfunc (x *Formatter) Get(fmtID uint16) (FmtFunc, bool) {\n\tff, ok := goFormatters[fmtID]\n\tif !ok {\n\t\tfs, ok2 := x.customCodes[fmtID]\n\t\tif ok2 {\n\t\t\treturn fs, true\n\t\t}\n\t\tff = identFunc\n\t}\n\n\treturn ff, ok\n}\n\n// Apply the specified number format to the value.\n// Returns false when fmtID is unknown.\nfunc (x *Formatter) Apply(fmtID uint16, val interface{}) (string, bool) {\n\tff, ok := goFormatters[fmtID]\n\tif !ok {\n\t\tfs, ok2 := x.customCodes[fmtID]\n\t\tif ok2 {\n\t\t\treturn fs(x, val), true\n\t\t}\n\t}\n\treturn ff(x, val), ok\n}\n\n// builtInFormats are all the built-in number formats for XLS/XLSX.\nvar builtInFormats = map[uint16]string{\n\t0:  `General`,\n\t1:  `0`,\n\t2:  `0.00`,\n\t3:  `#,##0`,\n\t4:  `#,##0.00`,\n\t9:  `0%`,\n\t10: `0.00%`,\n\n\t11: `0.00E+00`,\n\t12: `# ?/?`,\n\t13: `# ??/??`,\n\t14: `mm-dd-yy`,\n\t15: `d-mmm-yy`,\n\t16: `d-mmm`,\n\t17: `mmm-yy`,\n\t18: `h:mm AM/PM`,\n\t19: `h:mm:ss AM/PM`,\n\t20: `h:mm`,\n\t21: `h:mm:ss`,\n\t22: `m/d/yy h:mm`,\n\t37: `#,##0 ;(#,##0)`,\n\t38: `#,##0 ;[Red](#,##0)`,\n\t39: `#,##0.00;(#,##0.00)`,\n\t40: `#,##0.00;[Red](#,##0.00)`,\n\n\t41: `_(* #,##0_);_(* \\(#,##0\\);_(* \"-\"_);_(@_)`,\n\t42: `_(\"$\"* #,##0_);_(\"$\"* \\(#,##0\\);_(\"$\"* \"-\"_);_(@_)`,\n\t43: `_(* #,##0.00_);_(* \\(#,##0.00\\);_(* \"-\"??_);_(@_)`,\n\t44: `_(\"$\"* #,##0.00_);_(\"$\"* \\(#,##0.00\\);_(\"$\"* \"-\"??_);_(@_)`,\n\n\t45: `mm:ss`,\n\t46: `[h]:mm:ss`,\n\t47: `mmss.0`,\n\t48: `##0.0E+0`,\n\t49: `@`,\n\n\t// zh-cn format codes\n\t27: `yyyy\"年\"m\"月\"`,\n\t28: `m\"月\"d\"日\"`,\n\t29: `m\"月\"d\"日\"`,\n\t30: `m-d-yy`,\n\t31: `yyyy\"年\"m\"月\"d\"日\"`,\n\t32: `h\"时\"mm\"分\"`,\n\t33: `h\"时\"mm\"分\"ss\"秒\"`,\n\t34: `上午/下午 h\"时\"mm\"分\"`,\n\t35: `上午/下午 h\"时\"mm\"分\"ss\"秒\"`,\n\t36: `yyyy\"年\"m\"月\"`,\n\t50: `yyyy\"年\"m\"月\"`,\n\t51: `m\"月\"d\"日\"`,\n\t52: `yyyy\"年\"m\"月\"`,\n\t53: `m\"月\"d\"日\"`,\n\t54: `m\"月\"d\"日\"`,\n\t55: `上午/下午 h\"时\"mm\"分\"`,\n\t56: `上午/下午 h\"时\"mm\"分\"ss\"秒`,\n\t57: `yyyy\"年\"m\"月\"`,\n\t58: `m\"月\"d\"日\"`,\n\n\t// th-th format codes (in the spec these have a \"t\" prefix?)\n\t59: `0`,\n\t60: `0.00`,\n\t61: `#,##0`,\n\t62: `#,##0.00`,\n\t67: `0%`,\n\t68: `0.00%`,\n\t69: `# ?/?`,\n\t70: `# ??/??`,\n\n\t// th format code, but translated to aid the parser\n\t71: `d/m/yyyy`,      // `ว/ด/ปปปป`,\n\t72: `d-mmm-yy`,      // `ว-ดดด-ปป`,\n\t73: `d-mmm`,         // `ว-ดดด`,\n\t74: `mmm-yy`,        // `ดดด-ปป`,\n\t75: `h:mm`,          // `ช:นน`,\n\t76: `h:mm:ss`,       // `ช:นน:ทท`,\n\t77: `d/m/yyyy h:mm`, // `ว/ด/ปปปป ช:นน`,\n\t78: `mm:ss`,         // `นน:ทท`,\n\t79: `[h]:mm:ss`,     // `[ช]:นน:ทท`,\n\t80: `mm:ss.0`,       // `นน:ทท.0`,\n\t81: `d/m/bb`,        // `d/m/bb`,\n}\n\n// builtInFormatTypes are the underlying datatypes for built-in number formats in XLS/XLSX.\nvar builtInFormatTypes = map[uint16]CellType{\n\t// 0 has no defined type\n\t1:  IntegerCell,\n\t2:  FloatCell,\n\t3:  IntegerCell,\n\t4:  FloatCell,\n\t9:  FloatCell,\n\t10: FloatCell,\n\n\t11: FloatCell,\n\t12: FloatCell,\n\t13: FloatCell,\n\t14: DateCell,\n\t15: DateCell,\n\t16: DateCell,\n\t17: DateCell,\n\t18: DateCell,\n\t19: DateCell,\n\t20: DateCell,\n\t21: DateCell,\n\t22: DateCell,\n\t37: IntegerCell,\n\t38: IntegerCell,\n\t39: FloatCell,\n\t40: FloatCell,\n\t41: IntegerCell,\n\t42: IntegerCell,\n\t43: FloatCell,\n\t44: FloatCell,\n\t45: DateCell, // Durations?\n\t46: DateCell,\n\t47: DateCell,\n\t48: FloatCell,\n\t49: StringCell,\n\t27: DateCell,\n\t28: DateCell,\n\t29: DateCell,\n\t30: DateCell,\n\t31: DateCell,\n\t32: DateCell,\n\t33: DateCell,\n\t34: DateCell,\n\t35: DateCell,\n\t36: DateCell,\n\t50: DateCell,\n\t51: DateCell,\n\t52: DateCell,\n\t53: DateCell,\n\t54: DateCell,\n\t55: DateCell,\n\t56: DateCell,\n\t57: DateCell,\n\t58: DateCell,\n\t59: IntegerCell,\n\t60: FloatCell,\n\t61: IntegerCell,\n\t62: FloatCell,\n\t67: FloatCell,\n\t68: FloatCell,\n\t69: FloatCell,\n\t70: FloatCell,\n\t71: DateCell,\n\t72: DateCell,\n\t73: DateCell,\n\t74: DateCell,\n\t75: DateCell,\n\t76: DateCell,\n\t77: DateCell,\n\t78: DateCell,\n\t79: DateCell,\n\t80: DateCell,\n\t81: DateCell,\n}\n"
  },
  {
    "path": "commonxl/frac_test.go",
    "content": "package commonxl\n\nimport (\n\t\"math\"\n\t\"testing\"\n)\n\ntype testcaseFrac struct {\n\tv float64\n\ts string\n\tn int\n}\n\nvar fracs = []testcaseFrac{\n\t{0, \"0\", 1},\n\t{0.5, \"1/2\", 1},\n\t{-0.5, \"-1/2\", 1},\n\t{0.125, \"1/8\", 1},\n\n\t{10, \"10\", 1},\n\t{-10, \"-10\", 1},\n\t{10.5, \"10 1/2\", 1},\n\t{-10.5, \"-10 1/2\", 1},\n\n\t{10.25, \"10 1/4\", 1},\n\t{10.75, \"10 3/4\", 1},\n\t{10.667, \"10 2/3\", 1},\n\n\t{-10.25, \"-10 1/4\", 1},\n\t{-10.75, \"-10 3/4\", 1},\n\t{-10.667, \"-10 2/3\", 1},\n\n\t{3.14159, \"3 1/7\", 1},\n\t{3.14159, \"3 1/7\", 2},\n\t{3.14159, \"3 16/113\", 3},\n\t{3.14159, \"3 431/3044\", 4},\n\t{3.14159, \"3 3432/24239\", 5},\n\t{3.14159, \"3 14159/100000\", 6},\n\n\t{math.Pi, \"3 1/7\", 1},\n\t{math.Pi, \"3 1/7\", 2},\n\t{math.Pi, \"3 16/113\", 3}, // err = 2.6e-7\n\t{math.Pi, \"3 16/113\", 4}, // better because 431/3044 err = 2.6e-6\n\t{math.Pi, \"3 14093/99532\", 5},\n\t{math.Pi, \"3 14093/99532\", 6},\n\n\t{-math.Pi, \"-3 1/7\", 1},\n\t{-math.Pi, \"-3 1/7\", 2},\n\t{-math.Pi, \"-3 16/113\", 3}, // err = 2.6e-7\n\t{-math.Pi, \"-3 16/113\", 4}, // better because 431/3044 err = 2.6e-6\n\t{-math.Pi, \"-3 14093/99532\", 5},\n\t{-math.Pi, \"-3 14093/99532\", 6},\n\n\t// TODO: fixed denominator fractions (e.g. \"??/8\" )\n\t// TODO: string interpolations (e.g. '0 \"pounds and \" ??/100 \"pence\"')\n\t// examples: https://bettersolutions.com/excel/formatting/number-tab-fractions.htm\n}\n\nfunc TestFractions(t *testing.T) {\n\tfor _, c := range fracs {\n\t\tff := fracFmtFunc(c.n)\n\t\tfs := ff(nil, c.v)\n\t\tif c.s != fs {\n\t\t\tt.Fatalf(\"fractions failed: got: '%s' expected: '%s' for %T(%v)\",\n\t\t\t\tfs, c.s, c.v, c.v)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "commonxl/numbers.go",
    "content": "package commonxl\n\nimport (\n\t\"math\"\n)\n\n// DecimalToWholeFraction converts a floating point value into a whole\n// number and fraction approximation with at most nn digits in the numerator\n// and nd digits in the denominator.\nfunc DecimalToWholeFraction(val float64, nn, nd int) (whole, num, den int) {\n\twholeF, part := math.Modf(val)\n\tif part == 0.0 {\n\t\treturn int(wholeF), 0, 1\n\t}\n\tif part < 0.0 {\n\t\tpart = -part\n\t}\n\twhole = int(wholeF)\n\tnum, den = DecimalToFraction(part, nn, nd)\n\treturn\n}\n\n// DecimalToFraction converts a floating point value into a fraction\n// approximation with at most nn digits in the numerator and nd\n// digits in the denominator.\nfunc DecimalToFraction(val float64, nn, nd int) (num, den int) {\n\t// http://web.archive.org/web/20111027100847/http://homepage.smc.edu/kennedy_john/DEC2FRAC.PDF\n\tsign := 1\n\tz := val\n\tif val < 0 {\n\t\tsign = -1\n\t\tz = -val\n\t}\n\tif nn == 0 {\n\t\tnn = 2\n\t}\n\tif nd == 0 {\n\t\tnd = 2\n\t}\n\tmaxn := math.Pow(10.0, float64(nn)) // numerator with nn digits\n\tmaxd := math.Pow(10.0, float64(nd)) // denominator with nd digits\n\n\t_, fracPart := math.Modf(val)\n\tif fracPart == 0.0 {\n\t\treturn int(z) * sign, 1\n\t}\n\tif fracPart < 1e-9 {\n\t\treturn sign, int(1e9)\n\t}\n\tif fracPart > 1e9 {\n\t\treturn int(1e9) * sign, 1\n\t}\n\n\tdiff := 1.0\n\tdenom := 1.0\n\tnumer := 0.0\n\tvar lastDenom, lastNumer float64\n\tfor diff > 1e-10 && z != math.Floor(z) {\n\t\tz = 1 / (z - math.Floor(z))\n\t\ttmp := denom\n\t\tdenom = (denom * math.Floor(z)) + lastDenom\n\t\tlastDenom = tmp\n\t\tlastNumer = numer\n\t\tnumer = math.Round(val * denom)\n\t\tif numer >= maxn || denom >= maxd {\n\t\t\treturn sign * int(lastNumer), int(lastDenom)\n\t\t}\n\t\tdiff = val - (numer / denom)\n\t\tif diff < 0.0 {\n\t\t\tdiff = -diff\n\t\t}\n\t}\n\treturn sign * int(numer), int(denom)\n}\n"
  },
  {
    "path": "commonxl/sheet.go",
    "content": "package commonxl\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"time\"\n\n\t\"github.com/pbnjay/grate\"\n)\n\n// Sheet holds raw and rendered values for a spreadsheet.\ntype Sheet struct {\n\tFormatter *Formatter\n\tNumRows   int\n\tNumCols   int\n\tRows      [][]Cell\n\n\tCurRow int\n}\n\n// Resize the sheet for the number of rows and cols given.\n// Newly added cells default to blank.\nfunc (s *Sheet) Resize(rows, cols int) {\n\tfor i := range s.Rows {\n\t\tif i > rows {\n\t\t\tbreak\n\t\t}\n\t\tn := cols - len(s.Rows[i])\n\t\tif n <= 0 {\n\t\t\tcontinue\n\t\t}\n\t\ts.Rows[i] = append(s.Rows[i], make([]Cell, n)...)\n\t}\n\n\tif rows <= 0 {\n\t\trows = 1\n\t}\n\tif cols <= 0 {\n\t\tcols = 1\n\t}\n\ts.CurRow = 0\n\ts.NumRows = rows\n\ts.NumCols = cols\n\n\tfor rows >= len(s.Rows) {\n\t\ts.Rows = append(s.Rows, make([]Cell, cols))\n\t}\n}\n\n// Put the value at the cell location given.\nfunc (s *Sheet) Put(row, col int, value interface{}, fmtNum uint16) {\n\t//log.Println(row, col, value, fmtNum)\n\tif row >= s.NumRows || col >= s.NumCols {\n\t\tif grate.Debug {\n\t\t\tlog.Printf(\"grate: cell out of bounds row %d>=%d, col %d>=%d\",\n\t\t\t\trow, s.NumRows, col, s.NumCols)\n\t\t}\n\n\t\t// per the spec, this is an invalid Excel file\n\t\t// but we'll resize in place instead of crashing out\n\t\tif row >= s.NumRows {\n\t\t\ts.NumRows = row + 1\n\t\t}\n\t\tif col >= s.NumCols {\n\t\t\ts.NumCols = col + 1\n\t\t}\n\t\ts.Resize(s.NumRows, s.NumCols)\n\t}\n\n\tif spec, ok := value.(string); ok {\n\t\tif spec == grate.EndRowMerged || spec == grate.EndColumnMerged || spec == grate.ContinueRowMerged || spec == grate.ContinueColumnMerged {\n\t\t\ts.Rows[row][col] = NewCell(value)\n\t\t\ts.Rows[row][col][1] = StaticCell\n\t\t\treturn\n\t\t}\n\t}\n\n\tct, ok := s.Formatter.getCellType(fmtNum)\n\tif !ok || fmtNum == 0 {\n\t\ts.Rows[row][col] = NewCell(value)\n\t} else {\n\t\ts.Rows[row][col] = NewCellWithType(value, ct, s.Formatter)\n\t}\n\ts.Rows[row][col].SetFormatNumber(fmtNum)\n}\n\n// Set changes the value in an existing cell location.\n// NB Currently only used for populating string results for formulas.\nfunc (s *Sheet) Set(row, col int, value interface{}) {\n\tif row > s.NumRows || col > s.NumCols {\n\t\tlog.Println(\"grate: cell out of bounds\")\n\t\treturn\n\t}\n\n\ts.Rows[row][col][0] = value\n\ts.Rows[row][col][1] = StringCell\n}\n\n// SetURL adds a hyperlink to an existing cell location.\nfunc (s *Sheet) SetURL(row, col int, link string) {\n\tif row > s.NumRows || col > s.NumCols {\n\t\tlog.Println(\"grate: cell out of bounds\")\n\t\treturn\n\t}\n\n\ts.Rows[row][col].SetURL(link)\n}\n\n// Next advances to the next record of content.\n// It MUST be called prior to any Scan().\nfunc (s *Sheet) Next() bool {\n\tif (s.CurRow + 1) > len(s.Rows) {\n\t\treturn false\n\t}\n\ts.CurRow++\n\treturn true\n}\n\n// Raw extracts the raw Cell interfaces underlying the current row.\nfunc (s *Sheet) Raw() []Cell {\n\trr := make([]Cell, s.NumCols)\n\tfor i, cell := range s.Rows[s.CurRow-1] {\n\t\trr[i] = cell.Clone()\n\t}\n\treturn rr\n}\n\n// Strings extracts values from the current record into a list of strings.\nfunc (s *Sheet) Strings() []string {\n\tres := make([]string, s.NumCols)\n\tfor i, cell := range s.Rows[s.CurRow-1] {\n\t\tif cell.Type() == BlankCell {\n\t\t\tres[i] = \"\"\n\t\t\tcontinue\n\t\t}\n\t\tif cell.Type() == StaticCell {\n\t\t\tres[i] = cell.Value().(string)\n\t\t\tcontinue\n\t\t}\n\t\tval := cell.Value()\n\t\tfs, ok := s.Formatter.Apply(cell.FormatNo(), val)\n\t\tif !ok {\n\t\t\tfs = fmt.Sprint(val)\n\t\t}\n\t\tres[i] = fs\n\t}\n\treturn res\n}\n\n// Types extracts the data types from the current record into a list.\n// options: \"boolean\", \"integer\", \"float\", \"string\", \"date\",\n// and special cases: \"blank\", \"hyperlink\" which are string types\nfunc (s *Sheet) Types() []string {\n\tres := make([]string, s.NumCols)\n\tfor i, cell := range s.Rows[s.CurRow-1] {\n\t\tres[i] = cell.Type().String()\n\t}\n\treturn res\n}\n\n// Formats extracts the format code for the current record into a list.\nfunc (s *Sheet) Formats() []string {\n\tok := true\n\tres := make([]string, s.NumCols)\n\tfor i, cell := range s.Rows[s.CurRow-1] {\n\t\tres[i], ok = builtInFormats[cell.FormatNo()]\n\t\tif !ok {\n\t\t\tres[i] = fmt.Sprint(cell.FormatNo())\n\t\t}\n\t}\n\treturn res\n}\n\n// Scan extracts values from the current record into the provided arguments\n// Arguments must be pointers to one of 5 supported types:\n//     bool, int64, float64, string, or time.Time\n// If invalid, returns ErrInvalidScanType\nfunc (s *Sheet) Scan(args ...interface{}) error {\n\trow := s.Rows[s.CurRow-1]\n\n\tfor i, a := range args {\n\t\tval := row[i].Value()\n\n\t\tswitch v := a.(type) {\n\t\tcase bool, int64, float64, string, time.Time:\n\t\t\treturn fmt.Errorf(\"scan destinations must be pointer (arg %d is not)\", i)\n\t\tcase *bool:\n\t\t\tif x, ok := val.(bool); ok {\n\t\t\t\t*v = x\n\t\t\t} else {\n\t\t\t\treturn fmt.Errorf(\"scan destination %d expected *%T, not *bool\", i, val)\n\t\t\t}\n\t\tcase *int64:\n\t\t\tif x, ok := val.(int64); ok {\n\t\t\t\t*v = x\n\t\t\t} else {\n\t\t\t\treturn fmt.Errorf(\"scan destination %d expected *%T, not *int64\", i, val)\n\t\t\t}\n\t\tcase *float64:\n\t\t\tif x, ok := val.(float64); ok {\n\t\t\t\t*v = x\n\t\t\t} else {\n\t\t\t\treturn fmt.Errorf(\"scan destination %d expected *%T, not *float64\", i, val)\n\t\t\t}\n\t\tcase *string:\n\t\t\tif x, ok := val.(string); ok {\n\t\t\t\t*v = x\n\t\t\t} else {\n\t\t\t\treturn fmt.Errorf(\"scan destination %d expected *%T, not *string\", i, val)\n\t\t\t}\n\t\tcase *time.Time:\n\t\t\tif x, ok := val.(time.Time); ok {\n\t\t\t\t*v = x\n\t\t\t} else {\n\t\t\t\treturn fmt.Errorf(\"scan destination %d expected *%T, not *time.Time\", i, val)\n\t\t\t}\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"scan destination for arg %d is not supported (%T)\", i, a)\n\t\t}\n\t}\n\treturn nil\n}\n\n// IsEmpty returns true if there are no data values.\nfunc (s *Sheet) IsEmpty() bool {\n\treturn (s.NumCols <= 1 && s.NumRows <= 1)\n}\n\n// Err returns the last error that occured.\nfunc (s *Sheet) Err() error {\n\treturn nil\n}\n"
  },
  {
    "path": "errs.go",
    "content": "package grate\n\nimport \"errors\"\n\nvar (\n\t// configure at build time by adding go build arguments:\n\t//   -ldflags=\"-X github.com/pbnjay/grate.loglevel=debug\"\n\tloglevel string = \"warn\"\n\n\t// Debug should be set to true to expose detailed logging.\n\tDebug bool = (loglevel == \"debug\")\n)\n\n// ErrInvalidScanType is returned by Scan for invalid arguments.\nvar ErrInvalidScanType = errors.New(\"grate: Scan only supports *bool, *int, *float64, *string, *time.Time arguments\")\n\n// ErrNotInFormat is used to auto-detect file types using the defined OpenFunc\n// It is returned by OpenFunc when the code does not detect correct file formats.\nvar ErrNotInFormat = errors.New(\"grate: file is not in this format\")\n\n// ErrUnknownFormat is used when grate does not know how to open a file format.\nvar ErrUnknownFormat = errors.New(\"grate: file format is not known/supported\")\n\ntype errx struct {\n\terrs []error\n}\n\nfunc (e errx) Error() string {\n\treturn e.errs[0].Error()\n}\nfunc (e errx) Unwrap() error {\n\tif len(e.errs) > 1 {\n\t\treturn e.errs[1]\n\t}\n\treturn nil\n}\n\n// WrapErr wraps a set of errors.\nfunc WrapErr(e ...error) error {\n\tif len(e) == 1 {\n\t\treturn e[0]\n\t}\n\treturn errx{errs: e}\n}\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/pbnjay/grate\n\ngo 1.16\n"
  },
  {
    "path": "grate.go",
    "content": "// Package grate opens tabular data files (such as spreadsheets and delimited plaintext files)\n// and allows programmatic access to the data contents in a consistent interface.\npackage grate\n\nimport (\n\t\"errors\"\n\t\"log\"\n\t\"sort\"\n)\n\n// Source represents a set of data collections.\ntype Source interface {\n\t// List the individual data tables within this source.\n\tList() ([]string, error)\n\n\t// Get a Collection from the source by name.\n\tGet(name string) (Collection, error)\n\n\t// Close the source and discard memory.\n\tClose() error\n}\n\n// Collection represents an iterable collection of records.\ntype Collection interface {\n\t// Next advances to the next record of content.\n\t// It MUST be called prior to any Scan().\n\tNext() bool\n\n\t// Strings extracts values from the current record into a list of strings.\n\tStrings() []string\n\n\t// Types extracts the data types from the current record into a list.\n\t// options: \"boolean\", \"integer\", \"float\", \"string\", \"date\",\n\t// and special cases: \"blank\", \"hyperlink\" which are string types\n\tTypes() []string\n\n\t// Formats extracts the format codes for the current record into a list.\n\tFormats() []string\n\n\t// Scan extracts values from the current record into the provided arguments\n\t// Arguments must be pointers to one of 5 supported types:\n\t//     bool, int64, float64, string, or time.Time\n\t// If invalid, returns ErrInvalidScanType\n\tScan(args ...interface{}) error\n\n\t// IsEmpty returns true if there are no data values.\n\tIsEmpty() bool\n\n\t// Err returns the last error that occured.\n\tErr() error\n}\n\n// OpenFunc defines a Source's instantiation function.\n// It should return ErrNotInFormat immediately if filename is not of the correct file type.\ntype OpenFunc func(filename string) (Source, error)\n\n// Open a tabular data file and return a Source for accessing it's contents.\nfunc Open(filename string) (Source, error) {\n\tfor _, o := range srcTable {\n\t\tsrc, err := o.op(filename)\n\t\tif err == nil {\n\t\t\treturn src, nil\n\t\t}\n\t\tif !errors.Is(err, ErrNotInFormat) {\n\t\t\treturn nil, err\n\t\t}\n\t\tif Debug {\n\t\t\tlog.Println(\" \", filename, \"is not in\", o.name, \"format\")\n\t\t}\n\t}\n\treturn nil, ErrUnknownFormat\n}\n\ntype srcOpenTab struct {\n\tname string\n\tpri  int\n\top   OpenFunc\n}\n\nvar srcTable = make([]*srcOpenTab, 0, 20)\n\n// Register the named source as a grate datasource implementation.\nfunc Register(name string, priority int, opener OpenFunc) error {\n\tif Debug {\n\t\tlog.Println(\"Registering the\", name, \"format at priority\", priority)\n\t}\n\tsrcTable = append(srcTable, &srcOpenTab{name: name, pri: priority, op: opener})\n\tsort.Slice(srcTable, func(i, j int) bool {\n\t\treturn srcTable[i].pri < srcTable[j].pri\n\t})\n\treturn nil\n}\n\nconst (\n\t// ContinueColumnMerged marks a continuation column within a merged cell.\n\tContinueColumnMerged = \"→\"\n\t// EndColumnMerged marks the last column of a merged cell.\n\tEndColumnMerged = \"⇥\"\n\n\t// ContinueRowMerged marks a continuation row within a merged cell.\n\tContinueRowMerged = \"↓\"\n\t// EndRowMerged marks the last row of a merged cell.\n\tEndRowMerged = \"⤓\"\n)\n"
  },
  {
    "path": "simple/csv.go",
    "content": "package simple\n\nimport (\n\t\"encoding/csv\"\n\t\"os\"\n\n\t\"github.com/pbnjay/grate\"\n)\n\nvar _ = grate.Register(\"csv\", 15, OpenCSV)\n\n// OpenCSV defines a Source's instantiation function.\n// It should return ErrNotInFormat immediately if filename is not of the correct file type.\nfunc OpenCSV(filename string) (grate.Source, error) {\n\tf, err := os.Open(filename)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer f.Close()\n\tt := &simpleFile{\n\t\tfilename: filename,\n\t\titerRow:  -1,\n\t}\n\n\ts := csv.NewReader(f)\n\ts.FieldsPerRecord = -1\n\n\ttotal := 0\n\tncols := make(map[int]int)\n\trec, err := s.Read()\n\tfor ; err == nil; rec, err = s.Read() {\n\t\tncols[len(rec)]++\n\t\ttotal++\n\t\tt.rows = append(t.rows, rec)\n\t}\n\tif err != nil {\n\t\tswitch perr := err.(type) {\n\t\tcase *csv.ParseError:\n\t\t\treturn nil, grate.WrapErr(perr, grate.ErrNotInFormat)\n\t\t}\n\t\tif total < 10 {\n\t\t\t// probably? not in this format\n\t\t\treturn nil, grate.WrapErr(err, grate.ErrNotInFormat)\n\t\t}\n\t\treturn nil, err\n\t}\n\n\t// kinda arbitrary metrics for detecting CSV\n\tlooksGood := 0\n\tfor c, n := range ncols {\n\t\tif c <= 1 {\n\t\t\tcontinue\n\t\t}\n\t\tif n > 10 && float64(n)/float64(total) > 0.8 {\n\t\t\t// more than 80% of rows have the same number of columns, we're good\n\t\t\tlooksGood = 2\n\t\t} else if n > 25 && looksGood == 0 {\n\t\t\tlooksGood = 1\n\t\t}\n\t}\n\tif looksGood == 1 {\n\t\treturn t, grate.ErrNotInFormat\n\t}\n\n\treturn t, nil\n}\n"
  },
  {
    "path": "simple/simple.go",
    "content": "package simple\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/pbnjay/grate\"\n)\n\n// represents a set of data collections.\ntype simpleFile struct {\n\tfilename string\n\trows     [][]string\n\titerRow  int\n}\n\n// List the individual data tables within this source.\nfunc (t *simpleFile) List() ([]string, error) {\n\treturn []string{filepath.Base(t.filename)}, nil\n}\n\nfunc (t *simpleFile) Close() error {\n\treturn nil\n}\n\n// Get a Collection from the source by name.\nfunc (t *simpleFile) Get(name string) (grate.Collection, error) {\n\treturn t, nil\n}\n\n// Next advances to the next record of content.\n// It MUST be called prior to any Scan().\nfunc (t *simpleFile) Next() bool {\n\tt.iterRow++\n\treturn t.iterRow < len(t.rows)\n}\n\n// Strings extracts values from the current record into a list of strings.\nfunc (t *simpleFile) Strings() []string {\n\treturn t.rows[t.iterRow]\n}\n\n// Formats extracts the format code for the current record into a list.\nfunc (t *simpleFile) Formats() []string {\n\tres := make([]string, len(t.rows[t.iterRow]))\n\tfor i := range res {\n\t\tres[i] = \"General\"\n\t}\n\treturn res\n}\n\n// Types extracts the data types from the current record into a list.\n// options: \"boolean\", \"integer\", \"float\", \"string\", \"date\",\n// and special cases: \"blank\", \"hyperlink\" which are string types\nfunc (t *simpleFile) Types() []string {\n\tres := make([]string, len(t.rows[t.iterRow]))\n\tfor i, v := range t.rows[t.iterRow] {\n\t\tif v == \"\" {\n\t\t\tres[i] = \"blank\"\n\t\t} else {\n\t\t\tres[i] = \"string\"\n\t\t}\n\t}\n\treturn res\n}\n\n// Scan extracts values from the current record into the provided arguments\n// Arguments must be pointers to one of 5 supported types:\n//     bool, int, float64, string, or time.Time\nfunc (t *simpleFile) Scan(args ...interface{}) error {\n\tvar err error\n\trow := t.rows[t.iterRow]\n\tif len(row) != len(args) {\n\t\treturn fmt.Errorf(\"grate/simple: expected %d Scan destinations, got %d\", len(row), len(args))\n\t}\n\n\tfor i, a := range args {\n\t\tswitch v := a.(type) {\n\t\tcase *bool:\n\t\t\tswitch strings.ToLower(row[i]) {\n\t\t\tcase \"1\", \"t\", \"true\", \"y\", \"yes\":\n\t\t\t\t*v = true\n\t\t\tdefault:\n\t\t\t\t*v = false\n\t\t\t}\n\t\tcase *int:\n\t\t\tvar n int64\n\t\t\tn, err = strconv.ParseInt(row[i], 10, 64)\n\t\t\t*v = int(n)\n\t\tcase *float64:\n\t\t\t*v, err = strconv.ParseFloat(row[i], 64)\n\t\tcase *string:\n\t\t\t*v = row[i]\n\t\tcase *time.Time:\n\t\t\treturn errors.New(\"grate/simple: time.Time not supported, you must parse date strings manually\")\n\t\tdefault:\n\t\t\treturn grate.ErrInvalidScanType\n\t\t}\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\n// IsEmpty returns true if there are no data values.\nfunc (t *simpleFile) IsEmpty() bool {\n\treturn len(t.rows) == 0\n}\n\n// Err returns the last error that occured.\nfunc (t *simpleFile) Err() error {\n\treturn nil\n}\n"
  },
  {
    "path": "simple/tsv.go",
    "content": "package simple\n\nimport (\n\t\"bufio\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/pbnjay/grate\"\n)\n\nvar _ = grate.Register(\"tsv\", 10, OpenTSV)\n\n// OpenTSV defines a Source's instantiation function.\n// It should return ErrNotInFormat immediately if filename is not of the correct file type.\nfunc OpenTSV(filename string) (grate.Source, error) {\n\tf, err := os.Open(filename)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer f.Close()\n\tt := &simpleFile{\n\t\tfilename: filename,\n\t\titerRow:  -1,\n\t}\n\n\ts := bufio.NewScanner(f)\n\ttotal := 0\n\tncols := make(map[int]int)\n\tfor s.Scan() {\n\t\tr := strings.Split(s.Text(), \"\\t\")\n\t\tncols[len(r)]++\n\t\ttotal++\n\t\tt.rows = append(t.rows, r)\n\t}\n\tif s.Err() != nil {\n\t\t// this can only be read errors, not format\n\t\treturn nil, s.Err()\n\t}\n\n\t// kinda arbitrary metrics for detecting TSV\n\tlooksGood := 0\n\tfor c, n := range ncols {\n\t\tif c <= 1 {\n\t\t\tcontinue\n\t\t}\n\t\tif n > 10 && float64(n)/float64(total) > 0.8 {\n\t\t\t// more than 80% of rows have the same number of columns, we're good\n\t\t\tlooksGood = 2\n\t\t} else if n > 25 && looksGood == 0 {\n\t\t\tlooksGood = 1\n\t\t}\n\t}\n\tif looksGood == 1 {\n\t\treturn t, grate.ErrNotInFormat\n\t}\n\n\treturn t, nil\n}\n"
  },
  {
    "path": "xls/cfb/cfb.go",
    "content": "// Package cfb implements the Microsoft Compound File Binary File Format.\npackage cfb\n\n// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-cfb/53989ce4-7b05-4f8d-829b-d08d6148375b\n// Note for myself:\n//   Storage = Directory\n//   Stream = File\n\nimport (\n\t\"bytes\"\n\t\"encoding/binary\"\n\t\"errors\"\n\t\"io\"\n\t\"io/ioutil\"\n\t\"log\"\n\t\"unicode/utf16\"\n\n\t\"github.com/pbnjay/grate\"\n)\n\nconst fullAssertions = true\n\nconst (\n\tsecFree       uint32 = 0xFFFFFFFF // FREESECT\n\tsecEndOfChain uint32 = 0xFFFFFFFE // ENDOFCHAIN\n\tsecFAT        uint32 = 0xFFFFFFFD // FATSECT\n\tsecDIFAT      uint32 = 0xFFFFFFFC // DIFSECT\n\tsecReserved   uint32 = 0xFFFFFFFB\n\tsecMaxRegular uint32 = 0xFFFFFFFA // MAXREGSECT\n)\n\n// Header of the Compound File MUST be at the beginning of the file (offset 0).\ntype header struct {\n\tSignature                    uint64      // Identification signature for the compound file structure, and MUST be set to the value 0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1.\n\tClassID                      [2]uint64   // Reserved and unused class ID that MUST be set to all zeroes (CLSID_NULL).\n\tMinorVersion                 uint16      // Version number for nonbreaking changes. This field SHOULD be set to 0x003E if the major version field is either 0x0003 or 0x0004.\n\tMajorVersion                 uint16      // Version number for breaking changes. This field MUST be set to either 0x0003 (version 3) or 0x0004 (version 4).\n\tByteOrder                    uint16      // This field MUST be set to 0xFFFE. This field is a byte order mark for all integer fields, specifying little-endian byte order.\n\tSectorShift                  uint16      // This field MUST be set to 0x0009, or 0x000c, depending on the Major Version field. This field specifies the sector size of the compound file as a power of 2.\n\tMiniSectorShift              uint16      // This field MUST be set to 0x0006. This field specifies the sector size of the Mini Stream as a power of 2. The sector size of the Mini Stream MUST be 64 bytes.\n\tReserved1                    [6]byte     // This field MUST be set to all zeroes.\n\tNumDirectorySectors          int32       // This integer field contains the count of the number of directory sectors in the compound file.\n\tNumFATSectors                int32       // This integer field contains the count of the number of FAT sectors in the compound file.\n\tFirstDirectorySectorLocation uint32      // This integer field contains the starting sector number for the directory stream.\n\tTransactionSignature         int32       // This integer field MAY contain a sequence number that is incremented every time the compound file is saved by an implementation that supports file transactions. This is the field that MUST be set to all zeroes if file transactions are not implemented.<1>\n\tMiniStreamCutoffSize         int32       // This integer field MUST be set to 0x00001000. This field specifies the maximum size of a user-defined data stream that is allocated from the mini FAT and mini stream, and that cutoff is 4,096 bytes. Any user-defined data stream that is greater than or equal to this cutoff size must be allocated as normal sectors from the FAT.\n\tFirstMiniFATSectorLocation   uint32      // This integer field contains the starting sector number for the mini FAT.\n\tNumMiniFATSectors            int32       // This integer field contains the count of the number of mini FAT sectors in the compound file.\n\tFirstDIFATSectorLocation     uint32      // This integer field contains the starting sector number for the DIFAT.\n\tNumDIFATSectors              int32       // This integer field contains the count of the number of DIFAT sectors in the compound file.\n\tDIFAT                        [109]uint32 // This array of 32-bit integer fields contains the first 109 FAT sector locations of the compound file.\n}\n\ntype objectType byte\n\nconst (\n\ttypeUnknown     objectType = 0x00\n\ttypeStorage     objectType = 0x01\n\ttypeStream      objectType = 0x02\n\ttypeRootStorage objectType = 0x05\n)\n\ntype directory struct {\n\tName                   [32]uint16 // 32 utf16 characters\n\tNameByteLen            int16      // length of Name in bytes\n\tObjectType             objectType\n\tColorFlag              byte   // 0=red, 1=black\n\tLeftSiblingID          uint32 // stream ids\n\tRightSiblingID         uint32\n\tChildID                uint32\n\tClassID                [2]uint64 // GUID\n\tStateBits              uint32\n\tCreationTime           int64\n\tModifiedTime           int64\n\tStartingSectorLocation int32\n\tStreamSize             uint64\n}\n\nfunc (d *directory) String() string {\n\tif (d.NameByteLen&1) == 1 || d.NameByteLen > 64 {\n\t\treturn \"<invalid utf16 string>\"\n\t}\n\tr16 := utf16.Decode(d.Name[:int(d.NameByteLen)/2])\n\t// trim off null terminator\n\treturn string(r16[:len(r16)-1])\n}\n\n// Document represents a Compound File Binary Format document.\ntype Document struct {\n\t// the entire file, loaded into memory\n\tdata []byte\n\n\t// pre-parsed info\n\theader *header\n\tdir    []*directory\n\n\t// lookup tables for all the sectors\n\tfat     []uint32\n\tminifat []uint32\n\n\tministreamstart uint32\n\tministreamsize  uint32\n}\n\nfunc (d *Document) load(rx io.ReadSeeker) error {\n\tvar err error\n\td.data, err = ioutil.ReadAll(rx)\n\tif err != nil {\n\t\treturn err\n\t}\n\tbr := bytes.NewReader(d.data)\n\n\th := &header{}\n\terr = binary.Read(br, binary.LittleEndian, h)\n\tif h.Signature != 0xe11ab1a1e011cfd0 {\n\t\treturn grate.ErrNotInFormat // errors.New(\"ole2: invalid format\")\n\t}\n\tif h.ByteOrder != 0xFFFE {\n\t\treturn grate.ErrNotInFormat //errors.New(\"ole2: invalid format\")\n\t}\n\tif fullAssertions {\n\t\tif h.ClassID[0] != 0 || h.ClassID[1] != 0 {\n\t\t\treturn grate.ErrNotInFormat //errors.New(\"ole2: invalid CLSID\")\n\t\t}\n\t\tif h.MajorVersion != 3 && h.MajorVersion != 4 {\n\t\t\treturn errors.New(\"ole2: unknown major version\")\n\t\t}\n\t\tif h.MinorVersion != 0x3B && h.MinorVersion != 0x3E {\n\t\t\tlog.Printf(\"WARNING MinorVersion = 0x%02x NOT 0x3E\", h.MinorVersion)\n\t\t\t//return errors.New(\"ole2: unknown minor version\")\n\t\t}\n\n\t\tfor _, v := range h.Reserved1 {\n\t\t\tif v != 0 {\n\t\t\t\treturn errors.New(\"ole2: reserved section is non-zero\")\n\t\t\t}\n\t\t}\n\t\tif h.MajorVersion == 3 {\n\t\t\tif h.SectorShift != 9 {\n\t\t\t\treturn errors.New(\"ole2: invalid sector size\")\n\t\t\t}\n\t\t\tif h.NumDirectorySectors != 0 {\n\t\t\t\treturn errors.New(\"ole2: version 3 does not support directory sectors\")\n\t\t\t}\n\t\t}\n\t\tif h.MajorVersion == 4 {\n\t\t\tif h.SectorShift != 12 {\n\t\t\t\treturn errors.New(\"ole2: invalid sector size\")\n\t\t\t}\n\t\t}\n\t\tif h.MiniSectorShift != 6 {\n\t\t\treturn errors.New(\"ole2: invalid mini sector size\")\n\t\t}\n\t\tif h.MiniStreamCutoffSize != 0x00001000 {\n\t\t\treturn errors.New(\"ole2: invalid mini sector cutoff\")\n\t\t}\n\t}\n\td.header = h\n\n\tnumFATentries := (1 << (h.SectorShift - 2))\n\tle := binary.LittleEndian\n\td.fat = make([]uint32, 0, numFATentries*int(1+d.header.NumFATSectors))\n\td.minifat = make([]uint32, 0, numFATentries*int(1+h.NumMiniFATSectors))\n\n\t// step 1: read the DIFAT sector list\n\tfor i := 0; i < 109; i++ {\n\t\tsid := h.DIFAT[i]\n\t\tif sid == secFree {\n\t\t\tbreak\n\t\t}\n\t\toffs := int64(1+sid) << int32(h.SectorShift)\n\t\tif offs >= int64(len(d.data)) {\n\t\t\treturn errors.New(\"xls/cfb: unable to load file\")\n\t\t}\n\t\tsector := d.data[offs:]\n\t\tfor j := 0; j < numFATentries; j++ {\n\t\t\tsid2 := le.Uint32(sector)\n\t\t\td.fat = append(d.fat, sid2)\n\t\t\tsector = sector[4:]\n\t\t}\n\t}\n\tif h.NumDIFATSectors > 0 {\n\t\tsid1 := h.FirstDIFATSectorLocation\n\n\t\tfor sid1 != secEndOfChain {\n\t\t\toffs := int64(1+sid1) << int32(h.SectorShift)\n\t\t\tdifatSector := d.data[offs:]\n\n\t\t\tfor i := 0; i < numFATentries-1; i++ {\n\t\t\t\tsid2 := le.Uint32(difatSector)\n\t\t\t\tif sid2 == secFree || sid2 == secEndOfChain {\n\t\t\t\t\tdifatSector = difatSector[4:]\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\toffs := int64(1+sid2) << int32(h.SectorShift)\n\t\t\t\tif offs >= int64(len(d.data)) {\n\t\t\t\t\treturn errors.New(\"xls/cfb: unable to load file\")\n\t\t\t\t}\n\t\t\t\tsector := d.data[offs:]\n\t\t\t\tfor j := 0; j < numFATentries; j++ {\n\t\t\t\t\tsid3 := le.Uint32(sector)\n\t\t\t\t\td.fat = append(d.fat, sid3)\n\t\t\t\t\tsector = sector[4:]\n\t\t\t\t}\n\n\t\t\t\tdifatSector = difatSector[4:]\n\t\t\t}\n\t\t\t// chain the next DIFAT sector\n\t\t\tsid1 = le.Uint32(difatSector)\n\t\t}\n\t}\n\n\t// step 2: read the mini FAT\n\tsid := h.FirstMiniFATSectorLocation\n\tfor sid != secEndOfChain {\n\t\toffs := int64(1+sid) << int32(h.SectorShift)\n\t\tif offs >= int64(len(d.data)) {\n\t\t\treturn errors.New(\"xls/cfb: unable to load file\")\n\t\t}\n\t\tsector := d.data[offs:]\n\t\tfor j := 0; j < numFATentries; j++ {\n\t\t\tsid = le.Uint32(sector)\n\t\t\td.minifat = append(d.minifat, sid)\n\t\t\tsector = sector[4:]\n\t\t}\n\n\t\tif len(d.minifat) >= int(h.NumMiniFATSectors) {\n\t\t\tbreak\n\t\t}\n\n\t\t// chain the next mini FAT sector\n\t\tsid = le.Uint32(sector)\n\t}\n\n\t// step 3: read the Directory Entries\n\terr = d.buildDirs(br)\n\n\treturn err\n}\n\nfunc (d *Document) buildDirs(br *bytes.Reader) error {\n\th := d.header\n\tle := binary.LittleEndian\n\n\t// step 2: read the Directory\n\tsid := h.FirstDirectorySectorLocation\n\toffs := int64(1+sid) << int64(h.SectorShift)\n\tbr.Seek(offs, io.SeekStart)\n\n\tfor j := 0; j < 4; j++ {\n\t\tdirent := &directory{}\n\t\tbinary.Read(br, le, dirent)\n\t\tif d.header.MajorVersion == 3 {\n\t\t\t// mask out upper 32bits\n\t\t\tdirent.StreamSize = dirent.StreamSize & 0xFFFFFFFF\n\t\t}\n\n\t\tswitch dirent.ObjectType {\n\t\tcase typeRootStorage:\n\t\t\td.ministreamstart = uint32(dirent.StartingSectorLocation)\n\t\t\td.ministreamsize = uint32(dirent.StreamSize)\n\t\tcase typeStorage:\n\t\t\t//log.Println(\"got a storage? what to do now?\")\n\t\tcase typeStream:\n\t\t\t/*\n\t\t\t\tvar freader io.Reader\n\t\t\t\tif dirent.StreamSize < uint64(d.header.MiniStreamCutoffSize) {\n\t\t\t\t\tfreader = d.getMiniStreamReader(uint32(dirent.StartingSectorLocation), dirent.StreamSize)\n\t\t\t\t} else if dirent.StreamSize != 0 {\n\t\t\t\t\tfreader = d.getStreamReader(uint32(dirent.StartingSectorLocation), dirent.StreamSize)\n\t\t\t\t}\n\t\t\t*/\n\t\tcase typeUnknown:\n\t\t\treturn nil\n\t\t}\n\t\td.dir = append(d.dir, dirent)\n\t}\n\n\treturn nil\n}\n\nfunc (d *Document) getStreamReader(sid uint32, size uint64) (io.ReadSeeker, error) {\n\t// NB streamData is a slice of slices of the raw data, so this is the\n\t// only allocation - for the (much smaller) list of sector slices\n\tstreamData := make([][]byte, 1+(size>>d.header.SectorShift))\n\n\tx := 0\n\tsecSize := int64(1) << int32(d.header.SectorShift)\n\tfor sid != secEndOfChain && sid != secFree {\n\t\toffs := int64(1+sid) << int64(d.header.SectorShift)\n\t\tif offs > int64(len(d.data)) {\n\t\t\treturn nil, errors.New(\"ole2: corrupt data format\")\n\t\t}\n\t\tslice := d.data[offs : offs+secSize]\n\t\tif size < uint64(len(slice)) {\n\t\t\tslice = slice[:size]\n\t\t\tsize = 0\n\t\t} else {\n\t\t\tsize -= uint64(len(slice))\n\t\t}\n\t\tstreamData[x] = slice\n\t\tif size == 0 {\n\t\t\tbreak\n\t\t}\n\t\tsid = d.fat[sid]\n\t\tx++\n\t}\n\tif size != 0 {\n\t\treturn nil, errors.New(\"ole2: incomplete read\")\n\t}\n\n\treturn &SliceReader{Data: streamData}, nil\n}\n\nfunc (d *Document) getMiniStreamReader(sid uint32, size uint64) (io.ReadSeeker, error) {\n\t// TODO: move into a separate cache so we don't recalculate it each time\n\tfatStreamData := make([][]byte, 1+(d.ministreamsize>>d.header.SectorShift))\n\n\t// NB streamData is a slice of slices of the raw data, so this is the\n\t// only allocation - for the (much smaller) list of sector slices\n\tstreamData := make([][]byte, 1+(size>>d.header.MiniSectorShift))\n\n\tx := 0\n\tfsid := d.ministreamstart\n\tfsize := uint64(d.ministreamsize)\n\tsecSize := int64(1) << int64(d.header.SectorShift)\n\tfor fsid != secEndOfChain && fsid != secFree {\n\t\toffs := int64(1+fsid) << int64(d.header.SectorShift)\n\t\tslice := d.data[offs : offs+secSize]\n\t\tif fsize < uint64(len(slice)) {\n\t\t\tslice = slice[:fsize]\n\t\t\tfsize = 0\n\t\t} else {\n\t\t\tfsize -= uint64(len(slice))\n\t\t}\n\t\tfatStreamData[x] = slice\n\t\tx++\n\t\tfsid = d.fat[fsid]\n\t}\n\n\tx = 0\n\tminiSecSize := int64(1) << int64(d.header.MiniSectorShift)\n\tfor sid != secEndOfChain && sid != secFree {\n\t\toffs := int64(sid) << int64(d.header.MiniSectorShift)\n\n\t\tso, si := offs/secSize, offs%secSize\n\t\tdata := fatStreamData[so]\n\n\t\tslice := data[si : si+miniSecSize]\n\t\tif size < uint64(len(slice)) {\n\t\t\tslice = slice[:size]\n\t\t\tsize = 0\n\t\t} else {\n\t\t\tsize -= uint64(len(slice))\n\t\t}\n\t\tstreamData[x] = slice\n\t\tx++\n\t\tsid = d.minifat[sid]\n\t}\n\n\treturn &SliceReader{Data: streamData}, nil\n}\n"
  },
  {
    "path": "xls/cfb/interface.go",
    "content": "package cfb\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n)\n\n// Open a Compound File Binary Format document.\nfunc Open(filename string) (*Document, error) {\n\td := &Document{}\n\tf, err := os.Open(filename)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\terr = d.load(f)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn d, nil\n}\n\n// List the streams contained in the document.\nfunc (d *Document) List() ([]string, error) {\n\tvar res []string\n\tfor _, e := range d.dir {\n\t\tif e.ObjectType == typeStream {\n\t\t\tres = append(res, e.String())\n\t\t}\n\t}\n\treturn res, nil\n}\n\n// Open the named stream contained in the document.\nfunc (d *Document) Open(name string) (io.ReadSeeker, error) {\n\tfor _, e := range d.dir {\n\t\tif e.String() == name && e.ObjectType == typeStream {\n\t\t\tif e.StreamSize < uint64(d.header.MiniStreamCutoffSize) {\n\t\t\t\treturn d.getMiniStreamReader(uint32(e.StartingSectorLocation), e.StreamSize)\n\t\t\t} else if e.StreamSize != 0 {\n\t\t\t\treturn d.getStreamReader(uint32(e.StartingSectorLocation), e.StreamSize)\n\t\t\t}\n\t\t}\n\t}\n\treturn nil, fmt.Errorf(\"cfb: stream '%s' not found\", name)\n}\n"
  },
  {
    "path": "xls/cfb/simple_test.go",
    "content": "package cfb\n\nimport (\n\t\"io\"\n\t\"io/ioutil\"\n\t\"log\"\n\t\"os\"\n\t\"testing\"\n)\n\nfunc TestHeader(t *testing.T) {\n\td := &Document{}\n\tf, _ := os.Open(\"../../testdata/test.xls\")\n\terr := d.load(f)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\nfunc TestHeader2(t *testing.T) {\n\td := &Document{}\n\tf, _ := os.Open(\"../../testdata/test2.xls\")\n\terr := d.load(f)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\nfunc TestHeader3(t *testing.T) {\n\td := &Document{}\n\tf, _ := os.Open(\"../../testdata/test3.xls\")\n\terr := d.load(f)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\nfunc TestHeader4(t *testing.T) {\n\td := &Document{}\n\tf, _ := os.Open(\"../../testdata/test4.xls\")\n\terr := d.load(f)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tlog.Println(d.List())\n\n\tr, err := d.Open(\"Workbook\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tbook, err := ioutil.ReadAll(r)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tlog.Println(len(book))\n\n\tr, err = d.Open(\"\\x05DocumentSummaryInformation\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdata, err := ioutil.ReadAll(r)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tlog.Println(len(data))\n}\n\nvar testSlices = [][]byte{\n\t{0, 1, 2, 3, 4, 5, 6, 7, 8, 9},\n\t{10, 11, 12, 13, 14, 15, 16, 17, 18, 19},\n\t{20, 21, 22, 23, 24, 25, 26, 27, 28, 29},\n\t{30, 31, 32, 33, 34, 35, 36, 37, 38, 39},\n\t{40, 41, 42, 43, 44, 45, 46, 47, 48, 49},\n}\n\nfunc TestSliceReader(t *testing.T) {\n\tsr := &SliceReader{\n\t\tData: testSlices,\n\t}\n\tvar uno, old [1]byte\n\t_, err := sr.Read(uno[:])\n\tfor err == nil {\n\t\told[0] = uno[0]\n\t\t_, err = sr.Read(uno[:])\n\t\tif err == nil && uno[0] != (old[0]+1) {\n\t\t\tlog.Printf(\"read data out of order new=%d, old=%d\", old[0], uno[0])\n\t\t\tt.Fail()\n\t\t}\n\t}\n\tsr.Seek(0, io.SeekStart)\n\t_, err = sr.Read(uno[:])\n\tfor err == nil {\n\t\told[0] = uno[0]\n\t\t_, err = sr.Read(uno[:])\n\t\tif err == nil && uno[0] != (old[0]+1) {\n\t\t\tlog.Printf(\"read data out of order new=%d, old=%d\", old[0], uno[0])\n\t\t\tt.Fail()\n\t\t}\n\t}\n\tsr.Seek(10, io.SeekStart)\n\t_, err = sr.Read(uno[:])\n\tif uno[0] != 10 {\n\t\tlog.Printf(\"unexpected element %d (expected %d)\", uno[0], 10)\n\t\tt.Fail()\n\t}\n\tsr.Seek(35, io.SeekStart)\n\t_, err = sr.Read(uno[:])\n\tif uno[0] != 35 {\n\t\tlog.Printf(\"unexpected element %d (expected %d)\", uno[0], 35)\n\t\tt.Fail()\n\t}\n\tsr.Seek(7, io.SeekCurrent)\n\t_, err = sr.Read(uno[:])\n\tif uno[0] != 43 {\n\t\tlog.Printf(\"unexpected element %d (expected %d)\", uno[0], 43)\n\t\tt.Fail()\n\t}\n\tsr.Seek(-9, io.SeekCurrent)\n\t_, err = sr.Read(uno[:])\n\tif uno[0] != 35 {\n\t\tlog.Printf(\"unexpected element %d (expected %d)\", uno[0], 35)\n\t\tt.Fail()\n\t}\n}\n"
  },
  {
    "path": "xls/cfb/slicereader.go",
    "content": "package cfb\n\nimport (\n\t\"errors\"\n\t\"io\"\n)\n\n// SliceReader wraps a list of slices as a io.ReadSeeker that\n// can transparently merge them into a single coherent stream.\ntype SliceReader struct {\n\tCSize  []int64\n\tData   [][]byte\n\tIndex  uint\n\tOffset uint\n}\n\n// Read implements the io.Reader interface.\nfunc (s *SliceReader) Read(b []byte) (int, error) {\n\tif s.Index >= uint(len(s.Data)) {\n\t\treturn 0, io.EOF\n\t}\n\tn := copy(b, s.Data[s.Index][s.Offset:])\n\tif n > 0 {\n\t\ts.Offset += uint(n)\n\t\tif s.Offset == uint(len(s.Data[s.Index])) {\n\t\t\ts.Offset = 0\n\t\t\ts.Index++\n\t\t}\n\t\treturn n, nil\n\t}\n\n\treturn 0, io.EOF\n}\n\nvar x io.Seeker\n\n// Seek implements the io.Seeker interface.\nfunc (s *SliceReader) Seek(offset int64, whence int) (int64, error) {\n\tif len(s.CSize) != len(s.Data) {\n\t\t// calculate the cumulative block size cache\n\t\ts.CSize = make([]int64, len(s.Data))\n\t\tsz := int64(0)\n\t\tfor i, d := range s.Data {\n\t\t\ts.CSize[i] = sz\n\t\t\tsz += int64(len(d))\n\t\t}\n\t}\n\tif s.Index >= uint(len(s.CSize)) {\n\t\ts.Index = uint(len(s.CSize) - 1)\n\t\ts.Offset = uint(len(s.Data[s.Index]))\n\t}\n\t// current offset in stream\n\ttrueOffset := int64(s.Offset) + s.CSize[int(s.Index)]\n\tif offset == 0 && whence == io.SeekCurrent {\n\t\t// just asking for current position\n\t\treturn trueOffset, nil\n\t}\n\n\tswitch whence {\n\tcase io.SeekStart:\n\t\tif offset < 0 {\n\t\t\treturn -1, errors.New(\"xls: invalid seek offset\")\n\t\t}\n\t\ts.Index = 0\n\t\ts.Offset = 0\n\t\ttrueOffset = 0\n\n\tcase io.SeekEnd:\n\t\tif offset > 0 {\n\t\t\treturn -1, errors.New(\"xls: invalid seek offset\")\n\t\t}\n\n\t\ts.Index = uint(len(s.Data) - 1)\n\t\ts.Offset = uint(len(s.Data[s.Index]))\n\t\ttrueOffset = int64(s.Offset) + s.CSize[s.Index]\n\n\tdefault:\n\t\t// current position already defined\n\t}\n\n\twantOffset := offset + trueOffset\n\tfor trueOffset != wantOffset {\n\t\tloOffset := s.CSize[int(s.Index)]\n\t\thiOffset := s.CSize[int(s.Index)] + int64(len(s.Data[s.Index]))\n\t\tif wantOffset > loOffset && wantOffset < hiOffset {\n\t\t\ts.Offset = uint(wantOffset - loOffset)\n\t\t\treturn wantOffset, nil\n\t\t}\n\n\t\tif trueOffset > wantOffset {\n\t\t\ts.Index--\n\t\t\ts.Offset = 0\n\t\t\ttrueOffset = s.CSize[int(s.Index)]\n\t\t} else if trueOffset < wantOffset {\n\t\t\ts.Index++\n\t\t\ts.Offset = 0\n\t\t\ttrueOffset = s.CSize[int(s.Index)]\n\t\t}\n\t}\n\treturn wantOffset, nil\n}\n"
  },
  {
    "path": "xls/comp_test.go",
    "content": "package xls\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestAllFiles(t *testing.T) {\n\terr := filepath.Walk(\"../testdata\", func(p string, info os.FileInfo, err error) error {\n\t\tif info.IsDir() {\n\t\t\treturn nil\n\t\t}\n\t\tif !strings.HasSuffix(info.Name(), \".xls\") {\n\t\t\treturn nil\n\t\t}\n\t\twb, err := Open(p)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tsheets, err := wb.List()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfor _, s := range sheets {\n\t\t\tsheet, err := wb.Get(s)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tfor sheet.Next() {\n\t\t\t\tsheet.Strings()\n\t\t\t}\n\t\t}\n\n\t\treturn wb.Close()\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n"
  },
  {
    "path": "xls/crypto/crypto.go",
    "content": "// Package crypto implements excel encryption algorithms from the\n// MS-OFFCRYPTO design specs. Currently only standard/basic RC4\n// \"obfuscation\" is supported.\npackage crypto\n\nimport (\n\t\"bytes\"\n\t\"encoding/binary\"\n\t\"fmt\"\n)\n\n// Decryptor describes methods to decrypt an excel sheet.\ntype Decryptor interface {\n\t// SetPassword for the decryption.\n\tSetPassword(password []byte)\n\n\t// Read implements the io.Reader interface.\n\tRead(p []byte) (n int, err error)\n\n\t// Write implements the io.Writer interface.\n\tWrite(p []byte) (n int, err error)\n\n\t// Bytes returns the decrypted data.\n\tBytes() []byte\n\n\t// Flush tells the decryptor to decrypt the latest block.\n\tFlush()\n\n\t// Reset the decryptor, and clear all written and readable data.\n\tReset()\n}\n\n// Algorithms designed based on specs in MS-OFFCRYPTO:\n// https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-offcrypto/3c34d72a-1a61-4b52-a893-196f9157f083\n\n// Important notes from MS-XLS section 2.2.10:\n// https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-xls/cd03cb5f-ca02-4934-a391-bb674cb8aa06\n\n// When obfuscating or encrypting BIFF records in these streams the record type and\n// record size components MUST NOT be obfuscated or encrypted.\n// In addition the following records MUST NOT be obfuscated or encrypted:\n// BOF (section 2.4.21), FilePass (section 2.4.117), UsrExcl (section 2.4.339),\n// FileLock (section 2.4.116), InterfaceHdr (section 2.4.146), RRDInfo (section 2.4.227),\n// and RRDHead (section 2.4.226). Additionally, the lbPlyPos field of the BoundSheet8\n// record (section 2.4.28) MUST NOT be encrypted.\n\n// For RC4 encryption and RC4 CryptoAPI encryption, the Unicode password string is used\n// to generate the encryption key as specified in [MS-OFFCRYPTO] section 2.3.6.2 or\n// [MS-OFFCRYPTO] section 2.3.5.2 depending on the RC4 algorithm used. The record data\n// is then encrypted by the specific RC4 algorithm in 1024-byte blocks. The block number\n// is set to zero at the beginning of every BIFF record stream, and incremented by one\n// at each 1024-byte boundary. Bytes to be encrypted are passed into the RC4 encryption\n// function and then written to the stream. For unencrypted records and the record\n// headers consisting of the record type and record size, a byte buffer of all zeros,\n// of the same size as the section of unencrypted bytes, is passed into the RC4\n// encryption function. The results are then ignored and the unencrypted bytes are\n// written to the stream.\n\n// DefaultXLSPassword is the default encryption password defined by note\n// <100> Section 2.4.191: If the value of the wPassword field of the Password record in\n// the Globals Substream is not 0x0000, Excel 97, Excel 2000, Excel 2002, Office Excel\n// 2003, Office Excel 2007, and Excel 2010 encrypt the document as specified in [MS-OFFCRYPTO],\n// section 2.3. If an encryption password is not specified or the workbook or sheet is only\n// protected, the document is encrypted with the default password of:\n\n// DefaultXLSPassword is the default Excel encryption password.\nvar DefaultXLSPassword = \"VelvetSweatshop\"\n\n/////////////\n\n// 2.3.6.1\ntype basicRC4Encryption struct {\n\tMajorVersion uint16\n\tMinorVersion uint16\n\tSalt         [16]byte\n\tVerifier     [16]byte\n\tVerifierHash [16]byte\n}\n\n// NewBasicRC4 implements the standard RC4 decryption.\nfunc NewBasicRC4(data []byte) (Decryptor, error) {\n\th := basicRC4Encryption{}\n\tb := bytes.NewReader(data)\n\terr := binary.Read(b, binary.LittleEndian, &h)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif h.MinorVersion != 1 {\n\t\treturn nil, fmt.Errorf(\"xls: unknown basic-RC4 minor version %d (%d byte record)\",\n\t\t\th.MinorVersion, len(data))\n\t}\n\tif len(data) != 52 {\n\t\treturn nil, fmt.Errorf(\"xls: data length is invalid (expected 52 bytes, got %d)\",\n\t\t\tlen(data))\n\t}\n\n\td := &rc4Writer{\n\t\tSalt: make([]byte, len(h.Salt)),\n\t}\n\tcopy(d.Salt, h.Salt[:])\n\n\treturn d, d.Verify(h.Verifier[:], h.VerifierHash[:])\n}\n"
  },
  {
    "path": "xls/crypto/rc4.go",
    "content": "package crypto\n\nimport (\n\t\"bytes\"\n\t\"crypto/md5\"\n\t\"crypto/rc4\"\n\t\"encoding/binary\"\n\t\"fmt\"\n)\n\nvar _ Decryptor = &rc4Writer{}\n\nfunc (d *rc4Writer) Write(data []byte) (n int, err error) {\n\tx := len(data)\n\tfor len(data) > 0 {\n\t\tn := copy(d.bytes[d.offset:], data)\n\t\td.offset += n\n\t\tif d.offset >= 1024 {\n\t\t\tif d.offset != 1024 {\n\t\t\t\tpanic(\"invalid offset from write\")\n\t\t\t}\n\t\t\td.Flush()\n\t\t}\n\t\tdata = data[n:]\n\t}\n\treturn x, nil\n}\n\nfunc (d *rc4Writer) Read(data []byte) (n int, err error) {\n\treturn d.buf.Read(data)\n}\n\n// Reset to block 0, and clear all written and readable data.\nfunc (d *rc4Writer) Reset() {\n\td.block = 0\n\td.offset = 0\n\td.buf.Reset()\n}\n\n// Flush tells the decryptor to decrypt the latest block.\nfunc (d *rc4Writer) Flush() {\n\tvar zeros [1024]byte\n\n\tendpad := 0\n\tif d.offset < 1024 {\n\t\tendpad = copy(d.bytes[d.offset:], zeros[:])\n\t\td.offset += endpad\n\t}\n\tif d.offset != 1024 {\n\t\tpanic(\"invalid offset fill\")\n\t}\n\n\t// decrypt and write results to output buffer\n\td.startBlock()\n\td.dec.XORKeyStream(d.bytes[:], d.bytes[:])\n\td.buf.Write(d.bytes[:1024-endpad])\n\n\td.offset = 0\n\td.block++\n}\n\n// SetPassword for the decryption.\nfunc (d *rc4Writer) SetPassword(password []byte) {\n\td.Password = make([]rune, len(password))\n\tfor i, p := range password {\n\t\td.Password[i] = rune(p)\n\t}\n\n\t/// compute the first part of the encryption key\n\tresult := generateStd97Key(d.Password, d.Salt)\n\td.encKey = make([]byte, len(result))\n\tcopy(d.encKey, result)\n}\n\ntype rc4Writer struct {\n\tblock  uint32\n\toffset int\n\tbytes  [1024]byte\n\n\t// records the decrypted data\n\tbuf bytes.Buffer\n\n\t///////\n\n\t// decrypter for RC4 content streams\n\tdec *rc4.Cipher\n\n\tcipherKey []byte // H1 per 2.3.6.2\n\tencKey    []byte // Hfinal per 2.3.6.2\n\n\tSalt     []byte\n\tPassword []rune\n}\n\nfunc (d *rc4Writer) Bytes() []byte {\n\treturn d.buf.Bytes()\n}\n\nfunc (d *rc4Writer) Verify(everifier, everifierHash []byte) error {\n\td.Reset()\n\td.startBlock()\n\n\tvar temp1 [16]byte\n\tvar temp2 [16]byte\n\td.dec.XORKeyStream(temp1[:], everifier)\n\td.dec.XORKeyStream(temp2[:], everifierHash)\n\n\tnewhash := md5.Sum(temp1[:])\n\tfor i, c := range newhash {\n\t\tif temp2[i] != c {\n\t\t\treturn fmt.Errorf(\"verification failed\")\n\t\t}\n\t}\n\treturn nil\n}\n\n/////////////////////\n\nfunc (d *rc4Writer) startBlock() {\n\tif d.encKey == nil {\n\t\td.SetPassword([]byte(DefaultXLSPassword))\n\t}\n\n\td.cipherKey = make([]byte, 16)\n\tcopy(d.cipherKey, d.encKey[:5])\n\tbinary.LittleEndian.PutUint32(d.cipherKey[5:], d.block)\n\tmhash := md5.Sum(d.cipherKey[:9])\n\td.dec, _ = rc4.NewCipher(mhash[:])\n}\n\nfunc generateStd97Key(passData []rune, salt []byte) []byte {\n\tif len(passData) == 0 || len(salt) != 16 {\n\t\tpanic(\"invalid keygen material\")\n\t}\n\n\tpassBytes := make([]byte, len(passData)*2)\n\n\tfor i, c := range passData {\n\t\tbinary.LittleEndian.PutUint16(passBytes[2*i:], uint16(c))\n\t}\n\n\t// digest the IV then copy back into pKeyData\n\th0 := md5.Sum(passBytes)\n\n\t// now do the final set of keygen ops\n\tmsum := md5.New()\n\tfor i := 0; i < 16; i++ {\n\t\tmsum.Write(h0[:5])\n\t\tmsum.Write(salt)\n\t}\n\t// return H1\n\ttemp := make([]byte, 0, 16)\n\ttemp = msum.Sum(temp)\n\treturn temp\n}\n"
  },
  {
    "path": "xls/hyperlinks.go",
    "content": "package xls\n\nimport (\n\t\"encoding/binary\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\t\"unicode/utf16\"\n)\n\nfunc decodeHyperlinks(raw []byte) (displayText, linkText string, err error) {\n\traw = raw[16:] // skip classid\n\tslen := binary.LittleEndian.Uint32(raw[:4])\n\tif slen != 2 {\n\t\treturn \"\", \"\", errors.New(\"xls: unknown hyperlink version\")\n\t}\n\n\tflags := binary.LittleEndian.Uint32(raw[4:8])\n\traw = raw[8:]\n\tif (flags & hlstmfHasDisplayName) != 0 {\n\t\tslen = binary.LittleEndian.Uint32(raw[:4])\n\t\traw = raw[4:]\n\t\tus := make([]uint16, slen)\n\t\tfor i := 0; i < int(slen); i++ {\n\t\t\tus[i] = binary.LittleEndian.Uint16(raw)\n\t\t\traw = raw[2:]\n\t\t}\n\t\tdisplayText = string(utf16.Decode(us))\n\t}\n\n\tif (flags & hlstmfHasFrameName) != 0 {\n\t\t// skip a HyperlinkString containing target Frame\n\t\tslen = binary.LittleEndian.Uint32(raw[:4])\n\t\traw = raw[4+(slen*2):]\n\t}\n\n\tif (flags & hlstmfHasMoniker) != 0 {\n\t\tif (flags & hlstmfMonikerSavedAsStr) != 0 {\n\t\t\t// read HyperlinkString containing the URL\n\t\t\tslen = binary.LittleEndian.Uint32(raw[:4])\n\t\t\traw = raw[4:]\n\t\t\tus := make([]uint16, slen)\n\t\t\tfor i := 0; i < int(slen); i++ {\n\t\t\t\tus[i] = binary.LittleEndian.Uint16(raw)\n\t\t\t\traw = raw[2:]\n\t\t\t}\n\t\t\tlinkText = string(utf16.Decode(us))\n\n\t\t} else {\n\t\t\tn := 0\n\t\t\tvar err error\n\t\t\tlinkText, n, err = parseHyperlinkMoniker(raw)\n\t\t\traw = raw[n:]\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", \"\", err\n\t\t\t}\n\t\t}\n\t}\n\n\tif (flags & hlstmfHasLocationStr) != 0 {\n\t\tslen = binary.LittleEndian.Uint32(raw[:4])\n\t\traw = raw[4:]\n\t\tus := make([]uint16, slen)\n\t\tfor i := 0; i < int(slen); i++ {\n\t\t\tus[i] = binary.LittleEndian.Uint16(raw)\n\t\t\traw = raw[2:]\n\t\t}\n\t\tlinkText = string(utf16.Decode(us))\n\t}\n\n\tlinkText = strings.Trim(linkText, \" \\v\\f\\t\\r\\n\\x00\")\n\tdisplayText = strings.Trim(displayText, \" \\v\\f\\t\\r\\n\\x00\")\n\treturn\n}\n\nfunc parseHyperlinkMoniker(raw []byte) (string, int, error) {\n\tclassid := raw[:16]\n\tno := 16\n\n\tisURLMoniker := true\n\tisFileMoniker := true\n\turlMonikerClassID := [16]byte{0xE0, 0xC9, 0xEA, 0x79, 0xF9, 0xBA, 0xCE, 0x11, 0x8C, 0x82, 0x00, 0xAA, 0x00, 0x4B, 0xA9, 0x0B}\n\tfileMonikerClassID := [16]byte{0x03, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46}\n\tfor i, b := range classid {\n\t\tif urlMonikerClassID[i] != b {\n\t\t\tisURLMoniker = false\n\t\t}\n\t\tif fileMonikerClassID[i] != b {\n\t\t\tisFileMoniker = false\n\t\t}\n\t}\n\tif isURLMoniker {\n\t\tlength := binary.LittleEndian.Uint32(raw[no:])\n\t\tno += 4\n\t\tlength /= 2\n\t\tbuf := make([]uint16, length)\n\t\tfor i := 0; i < int(length); i++ {\n\t\t\tbuf[i] = binary.LittleEndian.Uint16(raw[no:])\n\t\t\tno += 2\n\t\t}\n\t\tif length > 12 && buf[length-13] == 0 {\n\t\t\tbuf = buf[:length-12]\n\t\t}\n\t\treturn string(utf16.Decode(buf)), no, nil\n\t}\n\tif isFileMoniker {\n\t\t//x := binary.LittleEndian.Uint16(raw[no:])        //cAnti\n\t\tlength := binary.LittleEndian.Uint32(raw[no+2:]) //ansiLength\n\t\tno += 6\n\t\tbuf := raw[no : no+int(length)]\n\n\t\t// skip 24 more bytes for misc fixed properties\n\t\tno += int(length) + 24\n\n\t\tlength = binary.LittleEndian.Uint32(raw[no:]) // cbUnicodePathSize\n\t\tno += 4\n\t\tif length > 0 {\n\t\t\tno += 6\n\t\t\tlength -= 6\n\t\t\tbuf2 := make([]uint16, length/2)\n\t\t\tfor i := 0; i < int(length/2); i++ {\n\t\t\t\tbuf2[i] = binary.LittleEndian.Uint16(raw[no:])\n\t\t\t\tno += 2\n\t\t\t}\n\t\t\treturn string(utf16.Decode(buf2)), no, nil\n\t\t}\n\n\t\treturn string(buf), no, nil\n\t}\n\n\treturn \"\", 0, fmt.Errorf(\"xls: unknown moniker classid\")\n}\n\n// HLink flags\nconst (\n\thlstmfHasMoniker          = uint32(0x001)\n\thlstmfIsAbsolute          = uint32(0x002)\n\thlstmfSiteGaveDisplayName = uint32(0x004)\n\thlstmfHasLocationStr      = uint32(0x008)\n\thlstmfHasDisplayName      = uint32(0x010)\n\thlstmfHasGUID             = uint32(0x020)\n\thlstmfHasCreationTime     = uint32(0x040)\n\thlstmfHasFrameName        = uint32(0x080)\n\thlstmfMonikerSavedAsStr   = uint32(0x100)\n\thlstmfAbsFromGetdataRel   = uint32(0x200)\n)\n"
  },
  {
    "path": "xls/records.go",
    "content": "package xls\n\nimport \"fmt\"\n\ntype recordType uint16\n\n// Record types defined by the XLS specification document, section 2.3/2.4.\n// https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-xls/43684742-8fcd-4fcd-92df-157d8d7241f9\nconst (\n\tRecTypeFormula              recordType = 6    // per section 2.4.127\n\tRecTypeEOF                  recordType = 10   // section 2.4.103\n\tRecTypeCalcCount            recordType = 12   // section 2.4.31\n\tRecTypeCalcMode             recordType = 13   // section 2.4.34\n\tRecTypeCalcPrecision        recordType = 14   // section 2.4.35\n\tRecTypeCalcRefMode          recordType = 15   // section 2.4.36\n\tRecTypeCalcDelta            recordType = 16   // section 2.4.32\n\tRecTypeCalcIter             recordType = 17   // section 2.4.33\n\tRecTypeProtect              recordType = 18   // section 2.4.207\n\tRecTypePassword             recordType = 19   // section 2.4.191\n\tRecTypeHeader               recordType = 20   // section 2.4.136\n\tRecTypeFooter               recordType = 21   // section 2.4.124\n\tRecTypeExternSheet          recordType = 23   // section 2.4.106\n\tRecTypeLbl                  recordType = 24   // section 2.4.150\n\tRecTypeWinProtect           recordType = 25   // section 2.4.347\n\tRecTypeVerticalPageBreaks   recordType = 26   // section 2.4.343\n\tRecTypeHorizontalPageBreaks recordType = 27   // section 2.4.142\n\tRecTypeNote                 recordType = 28   // section 2.4.179\n\tRecTypeSelection            recordType = 29   // section 2.4.248\n\tRecTypeDate1904             recordType = 34   // section 2.4.77\n\tRecTypeExternName           recordType = 35   // section 2.4.105\n\tRecTypeLeftMargin           recordType = 38   // section 2.4.151\n\tRecTypeRightMargin          recordType = 39   // section 2.4.219\n\tRecTypeTopMargin            recordType = 40   // section 2.4.328\n\tRecTypeBottomMargin         recordType = 41   // section 2.4.27\n\tRecTypePrintRowCol          recordType = 42   // section 2.4.203\n\tRecTypePrintGrid            recordType = 43   // section 2.4.202\n\tRecTypeFilePass             recordType = 47   // section 2.4.117\n\tRecTypeFont                 recordType = 49   // section 2.4.122\n\tRecTypePrintSize            recordType = 51   // section 2.4.204\n\tRecTypeContinue             recordType = 60   // section 2.4.58\n\tRecTypeWindow1              recordType = 61   // section 2.4.345\n\tRecTypeBackup               recordType = 64   // section 2.4.14\n\tRecTypePane                 recordType = 65   // section 2.4.189\n\tRecTypeCodePage             recordType = 66   // section 2.4.52\n\tRecTypePls                  recordType = 77   // section 2.4.199\n\tRecTypeDCon                 recordType = 80   // section 2.4.82\n\tRecTypeDConRef              recordType = 81   // section 2.4.86\n\tRecTypeDConName             recordType = 82   // section 2.4.85\n\tRecTypeDefColWidth          recordType = 85   // section 2.4.89\n\tRecTypeXCT                  recordType = 89   // section 2.4.352\n\tRecTypeCRN                  recordType = 90   // section 2.4.65\n\tRecTypeFileSharing          recordType = 91   // section 2.4.118\n\tRecTypeWriteAccess          recordType = 92   // section 2.4.349\n\tRecTypeObj                  recordType = 93   // section 2.4.181\n\tRecTypeUncalced             recordType = 94   // section 2.4.331\n\tRecTypeCalcSaveRecalc       recordType = 95   // section 2.4.37\n\tRecTypeTemplate             recordType = 96   // section 2.4.323\n\tRecTypeIntl                 recordType = 97   // section 2.4.147\n\tRecTypeObjProtect           recordType = 99   // section 2.4.183\n\tRecTypeColInfo              recordType = 125  // section 2.4.53\n\tRecTypeGuts                 recordType = 128  // section 2.4.134\n\tRecTypeWsBool               recordType = 129  // section 2.4.351\n\tRecTypeGridSet              recordType = 130  // section 2.4.132\n\tRecTypeHCenter              recordType = 131  // section 2.4.135\n\tRecTypeVCenter              recordType = 132  // section 2.4.342\n\tRecTypeBoundSheet8          recordType = 133  // section 2.4.28\n\tRecTypeWriteProtect         recordType = 134  // section 2.4.350\n\tRecTypeCountry              recordType = 140  // section 2.4.63\n\tRecTypeHideObj              recordType = 141  // section 2.4.139\n\tRecTypeSort                 recordType = 144  // section 2.4.263\n\tRecTypePalette              recordType = 146  // section 2.4.188\n\tRecTypeSync                 recordType = 151  // section 2.4.318\n\tRecTypeLPr                  recordType = 152  // section 2.4.158\n\tRecTypeDxGCol               recordType = 153  // section 2.4.98\n\tRecTypeFnGroupName          recordType = 154  // section 2.4.120\n\tRecTypeFilterMode           recordType = 155  // section 2.4.119\n\tRecTypeBuiltInFnGroupCount  recordType = 156  // section 2.4.30\n\tRecTypeAutoFilterInfo       recordType = 157  // section 2.4.8\n\tRecTypeAutoFilter           recordType = 158  // section 2.4.6\n\tRecTypeScl                  recordType = 160  // section 2.4.247\n\tRecTypeSetup                recordType = 161  // section 2.4.257\n\tRecTypeScenMan              recordType = 174  // section 2.4.246\n\tRecTypeSCENARIO             recordType = 175  // section 2.4.244\n\tRecTypeSxView               recordType = 176  // section 2.4.313\n\tRecTypeSxvd                 recordType = 177  // section 2.4.309\n\tRecTypeSXVI                 recordType = 178  // section 2.4.312\n\tRecTypeSxIvd                recordType = 180  // section 2.4.292\n\tRecTypeSXLI                 recordType = 181  // section 2.4.293\n\tRecTypeSXPI                 recordType = 182  // section 2.4.298\n\tRecTypeDocRoute             recordType = 184  // section 2.4.91\n\tRecTypeRecipName            recordType = 185  // section 2.4.216\n\tRecTypeMulRk                recordType = 189  // section 2.4.175\n\tRecTypeMulBlank             recordType = 190  // section 2.4.174\n\tRecTypeMms                  recordType = 193  // section 2.4.169\n\tRecTypeSXDI                 recordType = 197  // section 2.4.278\n\tRecTypeSXDB                 recordType = 198  // section 2.4.275\n\tRecTypeSXFDB                recordType = 199  // section 2.4.283\n\tRecTypeSXDBB                recordType = 200  // section 2.4.276\n\tRecTypeSXNum                recordType = 201  // section 2.4.296\n\tRecTypeSxBool               recordType = 202  // section 2.4.274\n\tRecTypeSxErr                recordType = 203  // section 2.4.281\n\tRecTypeSXInt                recordType = 204  // section 2.4.289\n\tRecTypeSXString             recordType = 205  // section 2.4.304\n\tRecTypeSXDtr                recordType = 206  // section 2.4.279\n\tRecTypeSxNil                recordType = 207  // section 2.4.295\n\tRecTypeSXTbl                recordType = 208  // section 2.4.305\n\tRecTypeSXTBRGIITM           recordType = 209  // section 2.4.307\n\tRecTypeSxTbpg               recordType = 210  // section 2.4.306\n\tRecTypeObProj               recordType = 211  // section 2.4.185\n\tRecTypeSXStreamID           recordType = 213  // section 2.4.303\n\tRecTypeDBCell               recordType = 215  // section 2.4.78\n\tRecTypeSXRng                recordType = 216  // section 2.4.300\n\tRecTypeSxIsxoper            recordType = 217  // section 2.4.290\n\tRecTypeBookBool             recordType = 218  // section 2.4.22\n\tRecTypeDbOrParamQry         recordType = 220  // section 2.4.79\n\tRecTypeScenarioProtect      recordType = 221  // section 2.4.245\n\tRecTypeOleObjectSize        recordType = 222  // section 2.4.187\n\tRecTypeXF                   recordType = 224  // section 2.4.353\n\tRecTypeInterfaceHdr         recordType = 225  // section 2.4.146\n\tRecTypeInterfaceEnd         recordType = 226  // section 2.4.145\n\tRecTypeSXVS                 recordType = 227  // section 2.4.317\n\tRecTypeMergeCells           recordType = 229  // section 2.4.168\n\tRecTypeBkHim                recordType = 233  // section 2.4.19\n\tRecTypeMsoDrawingGroup      recordType = 235  // section 2.4.171\n\tRecTypeMsoDrawing           recordType = 236  // section 2.4.170\n\tRecTypeMsoDrawingSelection  recordType = 237  // section 2.4.172\n\tRecTypePhoneticInfo         recordType = 239  // section 2.4.192\n\tRecTypeSxRule               recordType = 240  // section 2.4.301\n\tRecTypeSXEx                 recordType = 241  // section 2.4.282\n\tRecTypeSxFilt               recordType = 242  // section 2.4.285\n\tRecTypeSxDXF                recordType = 244  // section 2.4.280\n\tRecTypeSxItm                recordType = 245  // section 2.4.291\n\tRecTypeSxName               recordType = 246  // section 2.4.294\n\tRecTypeSxSelect             recordType = 247  // section 2.4.302\n\tRecTypeSXPair               recordType = 248  // section 2.4.297\n\tRecTypeSxFmla               recordType = 249  // section 2.4.286\n\tRecTypeSxFormat             recordType = 251  // section 2.4.287\n\tRecTypeSST                  recordType = 252  // section 2.4.265\n\tRecTypeLabelSst             recordType = 253  // section 2.4.149\n\tRecTypeExtSST               recordType = 255  // section 2.4.107\n\tRecTypeSXVDEx               recordType = 256  // section 2.4.310\n\tRecTypeSXFormula            recordType = 259  // section 2.4.288\n\tRecTypeSXDBEx               recordType = 290  // section 2.4.277\n\tRecTypeRRDInsDel            recordType = 311  // section 2.4.228\n\tRecTypeRRDHead              recordType = 312  // section 2.4.226\n\tRecTypeRRDChgCell           recordType = 315  // section 2.4.223\n\tRecTypeRRTabID              recordType = 317  // section 2.4.241\n\tRecTypeRRDRenSheet          recordType = 318  // section 2.4.234\n\tRecTypeRRSort               recordType = 319  // section 2.4.240\n\tRecTypeRRDMove              recordType = 320  // section 2.4.231\n\tRecTypeRRFormat             recordType = 330  // section 2.4.238\n\tRecTypeRRAutoFmt            recordType = 331  // section 2.4.222\n\tRecTypeRRInsertSh           recordType = 333  // section 2.4.239\n\tRecTypeRRDMoveBegin         recordType = 334  // section 2.4.232\n\tRecTypeRRDMoveEnd           recordType = 335  // section 2.4.233\n\tRecTypeRRDInsDelBegin       recordType = 336  // section 2.4.229\n\tRecTypeRRDInsDelEnd         recordType = 337  // section 2.4.230\n\tRecTypeRRDConflict          recordType = 338  // section 2.4.224\n\tRecTypeRRDDefName           recordType = 339  // section 2.4.225\n\tRecTypeRRDRstEtxp           recordType = 340  // section 2.4.235\n\tRecTypeLRng                 recordType = 351  // section 2.4.159\n\tRecTypeUsesELFs             recordType = 352  // section 2.4.337\n\tRecTypeDSF                  recordType = 353  // section 2.4.94\n\tRecTypeCUsr                 recordType = 401  // section 2.4.72\n\tRecTypeCbUsr                recordType = 402  // section 2.4.40\n\tRecTypeUsrInfo              recordType = 403  // section 2.4.340\n\tRecTypeUsrExcl              recordType = 404  // section 2.4.339\n\tRecTypeFileLock             recordType = 405  // section 2.4.116\n\tRecTypeRRDInfo              recordType = 406  // section 2.4.227\n\tRecTypeBCUsrs               recordType = 407  // section 2.4.16\n\tRecTypeUsrChk               recordType = 408  // section 2.4.338\n\tRecTypeUserBView            recordType = 425  // section 2.4.333\n\tRecTypeUserSViewBegin       recordType = 426  // section 2.4.334\n\tRecTypeUserSViewBeginChart  recordType = 426  // section 2.4.335\n\tRecTypeUserSViewEnd         recordType = 427  // section 2.4.336\n\tRecTypeRRDUserView          recordType = 428  // section 2.4.237\n\tRecTypeQsi                  recordType = 429  // section 2.4.208\n\tRecTypeSupBook              recordType = 430  // section 2.4.271\n\tRecTypeProt4Rev             recordType = 431  // section 2.4.205\n\tRecTypeCondFmt              recordType = 432  // section 2.4.56\n\tRecTypeCF                   recordType = 433  // section 2.4.42\n\tRecTypeDVal                 recordType = 434  // section 2.4.96\n\tRecTypeDConBin              recordType = 437  // section 2.4.83\n\tRecTypeTxO                  recordType = 438  // section 2.4.329\n\tRecTypeRefreshAll           recordType = 439  // section 2.4.217\n\tRecTypeHLink                recordType = 440  // section 2.4.140\n\tRecTypeLel                  recordType = 441  // section 2.4.154\n\tRecTypeCodeName             recordType = 442  // section 2.4.51\n\tRecTypeSXFDBType            recordType = 443  // section 2.4.284\n\tRecTypeProt4RevPass         recordType = 444  // section 2.4.206\n\tRecTypeObNoMacros           recordType = 445  // section 2.4.184\n\tRecTypeDv                   recordType = 446  // section 2.4.95\n\tRecTypeExcel9File           recordType = 448  // section 2.4.104\n\tRecTypeRecalcID             recordType = 449  // section 2.4.215\n\tRecTypeEntExU2              recordType = 450  // section 2.4.102\n\tRecTypeDimensions           recordType = 512  // section 2.4.90\n\tRecTypeBlank                recordType = 513  // section 2.4.20\n\tRecTypeNumber               recordType = 515  // section 2.4.180\n\tRecTypeLabel                recordType = 516  // section 2.4.148\n\tRecTypeBoolErr              recordType = 517  // section 2.4.24\n\tRecTypeString               recordType = 519  // section 2.4.268\n\tRecTypeRow                  recordType = 520  // section 2.4.221\n\tRecTypeIndex                recordType = 523  // section 2.4.144\n\tRecTypeArray                recordType = 545  // section 2.4.4\n\tRecTypeDefaultRowHeight     recordType = 549  // section 2.4.87\n\tRecTypeTable                recordType = 566  // section 2.4.319\n\tRecTypeWindow2              recordType = 574  // section 2.4.346\n\tRecTypeRK                   recordType = 638  // section 2.4.220\n\tRecTypeStyle                recordType = 659  // section 2.4.269\n\tRecTypeBigName              recordType = 1048 // section 2.4.18\n\tRecTypeFormat               recordType = 1054 // section 2.4.126\n\tRecTypeContinueBigName      recordType = 1084 // section 2.4.59\n\tRecTypeShrFmla              recordType = 1212 // section 2.4.260\n\tRecTypeHLinkTooltip         recordType = 2048 // section 2.4.141\n\tRecTypeWebPub               recordType = 2049 // section 2.4.344\n\tRecTypeQsiSXTag             recordType = 2050 // section 2.4.211\n\tRecTypeDBQueryExt           recordType = 2051 // section 2.4.81\n\tRecTypeExtString            recordType = 2052 // section 2.4.108\n\tRecTypeTxtQry               recordType = 2053 // section 2.4.330\n\tRecTypeQsir                 recordType = 2054 // section 2.4.210\n\tRecTypeQsif                 recordType = 2055 // section 2.4.209\n\tRecTypeRRDTQSIF             recordType = 2056 // section 2.4.236\n\tRecTypeBOF                  recordType = 2057 // section 2.4.21\n\tRecTypeOleDbConn            recordType = 2058 // section 2.4.186\n\tRecTypeWOpt                 recordType = 2059 // section 2.4.348\n\tRecTypeSXViewEx             recordType = 2060 // section 2.4.314\n\tRecTypeSXTH                 recordType = 2061 // section 2.4.308\n\tRecTypeSXPIEx               recordType = 2062 // section 2.4.299\n\tRecTypeSXVDTEx              recordType = 2063 // section 2.4.311\n\tRecTypeSXViewEx9            recordType = 2064 // section 2.4.315\n\tRecTypeContinueFrt          recordType = 2066 // section 2.4.60\n\tRecTypeRealTimeData         recordType = 2067 // section 2.4.214\n\tRecTypeChartFrtInfo         recordType = 2128 // section 2.4.49\n\tRecTypeFrtWrapper           recordType = 2129 // section 2.4.130\n\tRecTypeStartBlock           recordType = 2130 // section 2.4.266\n\tRecTypeEndBlock             recordType = 2131 // section 2.4.100\n\tRecTypeStartObject          recordType = 2132 // section 2.4.267\n\tRecTypeEndObject            recordType = 2133 // section 2.4.101\n\tRecTypeCatLab               recordType = 2134 // section 2.4.38\n\tRecTypeYMult                recordType = 2135 // section 2.4.356\n\tRecTypeSXViewLink           recordType = 2136 // section 2.4.316\n\tRecTypePivotChartBits       recordType = 2137 // section 2.4.196\n\tRecTypeFrtFontList          recordType = 2138 // section 2.4.129\n\tRecTypeSheetExt             recordType = 2146 // section 2.4.259\n\tRecTypeBookExt              recordType = 2147 // section 2.4.23\n\tRecTypeSXAddl               recordType = 2148 // section 2.4.273.2\n\tRecTypeCrErr                recordType = 2149 // section 2.4.64\n\tRecTypeHFPicture            recordType = 2150 // section 2.4.138\n\tRecTypeFeatHdr              recordType = 2151 // section 2.4.112\n\tRecTypeFeat                 recordType = 2152 // section 2.4.111\n\tRecTypeDataLabExt           recordType = 2154 // section 2.4.75\n\tRecTypeDataLabExtContents   recordType = 2155 // section 2.4.76\n\tRecTypeCellWatch            recordType = 2156 // section 2.4.41\n\tRecTypeFeatHdr11            recordType = 2161 // section 2.4.113\n\tRecTypeFeature11            recordType = 2162 // section 2.4.114\n\tRecTypeDropDownObjIds       recordType = 2164 // section 2.4.93\n\tRecTypeContinueFrt11        recordType = 2165 // section 2.4.61\n\tRecTypeDConn                recordType = 2166 // section 2.4.84\n\tRecTypeList12               recordType = 2167 // section 2.4.157\n\tRecTypeFeature12            recordType = 2168 // section 2.4.115\n\tRecTypeCondFmt12            recordType = 2169 // section 2.4.57\n\tRecTypeCF12                 recordType = 2170 // section 2.4.43\n\tRecTypeCFEx                 recordType = 2171 // section 2.4.44\n\tRecTypeXFCRC                recordType = 2172 // section 2.4.354\n\tRecTypeXFExt                recordType = 2173 // section 2.4.355\n\tRecTypeAutoFilter12         recordType = 2174 // section 2.4.7\n\tRecTypeContinueFrt12        recordType = 2175 // section 2.4.62\n\tRecTypeMDTInfo              recordType = 2180 // section 2.4.162\n\tRecTypeMDXStr               recordType = 2181 // section 2.4.166\n\tRecTypeMDXTuple             recordType = 2182 // section 2.4.167\n\tRecTypeMDXSet               recordType = 2183 // section 2.4.165\n\tRecTypeMDXProp              recordType = 2184 // section 2.4.164\n\tRecTypeMDXKPI               recordType = 2185 // section 2.4.163\n\tRecTypeMDB                  recordType = 2186 // section 2.4.161\n\tRecTypePLV                  recordType = 2187 // section 2.4.200\n\tRecTypeCompat12             recordType = 2188 // section 2.4.54\n\tRecTypeDXF                  recordType = 2189 // section 2.4.97\n\tRecTypeTableStyles          recordType = 2190 // section 2.4.322\n\tRecTypeTableStyle           recordType = 2191 // section 2.4.320\n\tRecTypeTableStyleElement    recordType = 2192 // section 2.4.321\n\tRecTypeStyleExt             recordType = 2194 // section 2.4.270\n\tRecTypeNamePublish          recordType = 2195 // section 2.4.178\n\tRecTypeNameCmt              recordType = 2196 // section 2.4.176\n\tRecTypeSortData             recordType = 2197 // section 2.4.264\n\tRecTypeTheme                recordType = 2198 // section 2.4.326\n\tRecTypeGUIDTypeLib          recordType = 2199 // section 2.4.133\n\tRecTypeFnGrp12              recordType = 2200 // section 2.4.121\n\tRecTypeNameFnGrp12          recordType = 2201 // section 2.4.177\n\tRecTypeMTRSettings          recordType = 2202 // section 2.4.173\n\tRecTypeCompressPictures     recordType = 2203 // section 2.4.55\n\tRecTypeHeaderFooter         recordType = 2204 // section 2.4.137\n\tRecTypeCrtLayout12          recordType = 2205 // section 2.4.66\n\tRecTypeCrtMlFrt             recordType = 2206 // section 2.4.70\n\tRecTypeCrtMlFrtContinue     recordType = 2207 // section 2.4.71\n\tRecTypeForceFullCalculation recordType = 2211 // section 2.4.125\n\tRecTypeShapePropsStream     recordType = 2212 // section 2.4.258\n\tRecTypeTextPropsStream      recordType = 2213 // section 2.4.325\n\tRecTypeRichTextStream       recordType = 2214 // section 2.4.218\n\tRecTypeCrtLayout12A         recordType = 2215 // section 2.4.67\n\tRecTypeUnits                recordType = 4097 // section 2.4.332\n\tRecTypeChart                recordType = 4098 // section 2.4.45\n\tRecTypeSeries               recordType = 4099 // section 2.4.252\n\tRecTypeDataFormat           recordType = 4102 // section 2.4.74\n\tRecTypeLineFormat           recordType = 4103 // section 2.4.156\n\tRecTypeMarkerFormat         recordType = 4105 // section 2.4.160\n\tRecTypeAreaFormat           recordType = 4106 // section 2.4.3\n\tRecTypePieFormat            recordType = 4107 // section 2.4.195\n\tRecTypeAttachedLabel        recordType = 4108 // section 2.4.5\n\tRecTypeSeriesText           recordType = 4109 // section 2.4.254\n\tRecTypeChartFormat          recordType = 4116 // section 2.4.48\n\tRecTypeLegend               recordType = 4117 // section 2.4.152\n\tRecTypeSeriesList           recordType = 4118 // section 2.4.253\n\tRecTypeBar                  recordType = 4119 // section 2.4.15\n\tRecTypeLine                 recordType = 4120 // section 2.4.155\n\tRecTypePie                  recordType = 4121 // section 2.4.194\n\tRecTypeArea                 recordType = 4122 // section 2.4.2\n\tRecTypeScatter              recordType = 4123 // section 2.4.243\n\tRecTypeCrtLine              recordType = 4124 // section 2.4.68\n\tRecTypeAxis                 recordType = 4125 // section 2.4.11\n\tRecTypeTick                 recordType = 4126 // section 2.4.327\n\tRecTypeValueRange           recordType = 4127 // section 2.4.341\n\tRecTypeCatSerRange          recordType = 4128 // section 2.4.39\n\tRecTypeAxisLine             recordType = 4129 // section 2.4.12\n\tRecTypeCrtLink              recordType = 4130 // section 2.4.69\n\tRecTypeDefaultText          recordType = 4132 // section 2.4.88\n\tRecTypeText                 recordType = 4133 // section 2.4.324\n\tRecTypeFontX                recordType = 4134 // section 2.4.123\n\tRecTypeObjectLink           recordType = 4135 // section 2.4.182\n\tRecTypeFrame                recordType = 4146 // section 2.4.128\n\tRecTypeBegin                recordType = 4147 // section 2.4.17\n\tRecTypeEnd                  recordType = 4148 // section 2.4.99\n\tRecTypePlotArea             recordType = 4149 // section 2.4.197\n\tRecTypeChart3d              recordType = 4154 // section 2.4.46\n\tRecTypePicF                 recordType = 4156 // section 2.4.193\n\tRecTypeDropBar              recordType = 4157 // section 2.4.92\n\tRecTypeRadar                recordType = 4158 // section 2.4.212\n\tRecTypeSurf                 recordType = 4159 // section 2.4.272\n\tRecTypeRadarArea            recordType = 4160 // section 2.4.213\n\tRecTypeAxisParent           recordType = 4161 // section 2.4.13\n\tRecTypeLegendException      recordType = 4163 // section 2.4.153(\n\tRecTypeShtProps             recordType = 4164 // section 2.4.261\n\tRecTypeSerToCrt             recordType = 4165 // section 2.4.256\n\tRecTypeAxesUsed             recordType = 4166 // section 2.4.10\n\tRecTypeSBaseRef             recordType = 4168 // section 2.4.242\n\tRecTypeSerParent            recordType = 4170 // section 2.4.255\n\tRecTypeSerAuxTrend          recordType = 4171 // section 2.4.250\n\tRecTypeIFmtRecord           recordType = 4174 // section 2.4.143\n\tRecTypePos                  recordType = 4175 // section 2.4.201\n\tRecTypeAlRuns               recordType = 4176 // section 2.4.1\n\tRecTypeBRAI                 recordType = 4177 // section 2.4.29\n\tRecTypeSerAuxErrBar         recordType = 4187 // section 2.4.249\n\tRecTypeClrtClient           recordType = 4188 // section 2.4.50\n\tRecTypeSerFmt               recordType = 4189 // section 2.4.251\n\tRecTypeChart3DBarShape      recordType = 4191 // section 2.4.47\n\tRecTypeFbi                  recordType = 4192 // section 2.4.109\n\tRecTypeBopPop               recordType = 4193 // section 2.4.25\n\tRecTypeAxcExt               recordType = 4194 // section 2.4.9\n\tRecTypeDat                  recordType = 4195 // section 2.4.73\n\tRecTypePlotGrowth           recordType = 4196 // section 2.4.198\n\tRecTypeSIIndex              recordType = 4197 // section 2.4.262\n\tRecTypeGelFrame             recordType = 4198 // section 2.4.131\n\tRecTypeBopPopCustom         recordType = 4199 // section 2.4.26\n\tRecTypeFbi2                 recordType = 4200 // section 2.4.110\n)\n\nfunc (r recordType) String() string {\n\tswitch r {\n\tcase RecTypeFormula:\n\t\treturn \"Formula (6)\"\n\tcase RecTypeEOF:\n\t\treturn \"EOF (10)\"\n\tcase RecTypeCalcCount:\n\t\treturn \"CalcCount (12)\"\n\tcase RecTypeCalcMode:\n\t\treturn \"CalcMode (13)\"\n\tcase RecTypeCalcPrecision:\n\t\treturn \"CalcPrecision (14)\"\n\tcase RecTypeCalcRefMode:\n\t\treturn \"CalcRefMode (15)\"\n\tcase RecTypeCalcDelta:\n\t\treturn \"CalcDelta (16)\"\n\tcase RecTypeCalcIter:\n\t\treturn \"CalcIter (17)\"\n\tcase RecTypeProtect:\n\t\treturn \"Protect (18)\"\n\tcase RecTypePassword:\n\t\treturn \"Password (19)\"\n\tcase RecTypeHeader:\n\t\treturn \"Header (20)\"\n\tcase RecTypeFooter:\n\t\treturn \"Footer (21)\"\n\tcase RecTypeExternSheet:\n\t\treturn \"ExternSheet (23)\"\n\tcase RecTypeLbl:\n\t\treturn \"Lbl (24)\"\n\tcase RecTypeWinProtect:\n\t\treturn \"WinProtect (25)\"\n\tcase RecTypeVerticalPageBreaks:\n\t\treturn \"VerticalPageBreaks (26)\"\n\tcase RecTypeHorizontalPageBreaks:\n\t\treturn \"HorizontalPageBreaks (27)\"\n\tcase RecTypeNote:\n\t\treturn \"Note (28)\"\n\tcase RecTypeSelection:\n\t\treturn \"Selection (29)\"\n\tcase RecTypeDate1904:\n\t\treturn \"Date1904 (34)\"\n\tcase RecTypeExternName:\n\t\treturn \"ExternName (35)\"\n\tcase RecTypeLeftMargin:\n\t\treturn \"LeftMargin (38)\"\n\tcase RecTypeRightMargin:\n\t\treturn \"RightMargin (39)\"\n\tcase RecTypeTopMargin:\n\t\treturn \"TopMargin (40)\"\n\tcase RecTypeBottomMargin:\n\t\treturn \"BottomMargin (41)\"\n\tcase RecTypePrintRowCol:\n\t\treturn \"PrintRowCol (42)\"\n\tcase RecTypePrintGrid:\n\t\treturn \"PrintGrid (43)\"\n\tcase RecTypeFilePass:\n\t\treturn \"FilePass (47)\"\n\tcase RecTypeFont:\n\t\treturn \"Font (49)\"\n\tcase RecTypePrintSize:\n\t\treturn \"PrintSize (51)\"\n\tcase RecTypeContinue:\n\t\treturn \"Continue (60)\"\n\tcase RecTypeWindow1:\n\t\treturn \"Window1 (61)\"\n\tcase RecTypeBackup:\n\t\treturn \"Backup (64)\"\n\tcase RecTypePane:\n\t\treturn \"Pane (65)\"\n\tcase RecTypeCodePage:\n\t\treturn \"CodePage (66)\"\n\tcase RecTypePls:\n\t\treturn \"Pls (77)\"\n\tcase RecTypeDCon:\n\t\treturn \"DCon (80)\"\n\tcase RecTypeDConRef:\n\t\treturn \"DConRef (81)\"\n\tcase RecTypeDConName:\n\t\treturn \"DConName (82)\"\n\tcase RecTypeDefColWidth:\n\t\treturn \"DefColWidth (85)\"\n\tcase RecTypeXCT:\n\t\treturn \"XCT (89)\"\n\tcase RecTypeCRN:\n\t\treturn \"CRN (90)\"\n\tcase RecTypeFileSharing:\n\t\treturn \"FileSharing (91)\"\n\tcase RecTypeWriteAccess:\n\t\treturn \"WriteAccess (92)\"\n\tcase RecTypeObj:\n\t\treturn \"Obj (93)\"\n\tcase RecTypeUncalced:\n\t\treturn \"Uncalced (94)\"\n\tcase RecTypeCalcSaveRecalc:\n\t\treturn \"CalcSaveRecalc (95)\"\n\tcase RecTypeTemplate:\n\t\treturn \"Template (96)\"\n\tcase RecTypeIntl:\n\t\treturn \"Intl (97)\"\n\tcase RecTypeObjProtect:\n\t\treturn \"ObjProtect (99)\"\n\tcase RecTypeColInfo:\n\t\treturn \"ColInfo (125)\"\n\tcase RecTypeGuts:\n\t\treturn \"Guts (128)\"\n\tcase RecTypeWsBool:\n\t\treturn \"WsBool (129)\"\n\tcase RecTypeGridSet:\n\t\treturn \"GridSet (130)\"\n\tcase RecTypeHCenter:\n\t\treturn \"HCenter (131)\"\n\tcase RecTypeVCenter:\n\t\treturn \"VCenter (132)\"\n\tcase RecTypeBoundSheet8:\n\t\treturn \"BoundSheet8 (133)\"\n\tcase RecTypeWriteProtect:\n\t\treturn \"WriteProtect (134)\"\n\tcase RecTypeCountry:\n\t\treturn \"Country (140)\"\n\tcase RecTypeHideObj:\n\t\treturn \"HideObj (141)\"\n\tcase RecTypeSort:\n\t\treturn \"Sort (144)\"\n\tcase RecTypePalette:\n\t\treturn \"Palette (146)\"\n\tcase RecTypeSync:\n\t\treturn \"Sync (151)\"\n\tcase RecTypeLPr:\n\t\treturn \"LPr (152)\"\n\tcase RecTypeDxGCol:\n\t\treturn \"DxGCol (153)\"\n\tcase RecTypeFnGroupName:\n\t\treturn \"FnGroupName (154)\"\n\tcase RecTypeFilterMode:\n\t\treturn \"FilterMode (155)\"\n\tcase RecTypeBuiltInFnGroupCount:\n\t\treturn \"BuiltInFnGroupCount (156)\"\n\tcase RecTypeAutoFilterInfo:\n\t\treturn \"AutoFilterInfo (157)\"\n\tcase RecTypeAutoFilter:\n\t\treturn \"AutoFilter (158)\"\n\tcase RecTypeScl:\n\t\treturn \"Scl (160)\"\n\tcase RecTypeSetup:\n\t\treturn \"Setup (161)\"\n\tcase RecTypeScenMan:\n\t\treturn \"ScenMan (174)\"\n\tcase RecTypeSCENARIO:\n\t\treturn \"SCENARIO (175)\"\n\tcase RecTypeSxView:\n\t\treturn \"SxView (176)\"\n\tcase RecTypeSxvd:\n\t\treturn \"Sxvd (177)\"\n\tcase RecTypeSXVI:\n\t\treturn \"SXVI (178)\"\n\tcase RecTypeSxIvd:\n\t\treturn \"SxIvd (180)\"\n\tcase RecTypeSXLI:\n\t\treturn \"SXLI (181)\"\n\tcase RecTypeSXPI:\n\t\treturn \"SXPI (182)\"\n\tcase RecTypeDocRoute:\n\t\treturn \"DocRoute (184)\"\n\tcase RecTypeRecipName:\n\t\treturn \"RecipName (185)\"\n\tcase RecTypeMulRk:\n\t\treturn \"MulRk (189)\"\n\tcase RecTypeMulBlank:\n\t\treturn \"MulBlank (190)\"\n\tcase RecTypeMms:\n\t\treturn \"Mms (193)\"\n\tcase RecTypeSXDI:\n\t\treturn \"SXDI (197)\"\n\tcase RecTypeSXDB:\n\t\treturn \"SXDB (198)\"\n\tcase RecTypeSXFDB:\n\t\treturn \"SXFDB (199)\"\n\tcase RecTypeSXDBB:\n\t\treturn \"SXDBB (200)\"\n\tcase RecTypeSXNum:\n\t\treturn \"SXNum (201)\"\n\tcase RecTypeSxBool:\n\t\treturn \"SxBool (202)\"\n\tcase RecTypeSxErr:\n\t\treturn \"SxErr (203)\"\n\tcase RecTypeSXInt:\n\t\treturn \"SXInt (204)\"\n\tcase RecTypeSXString:\n\t\treturn \"SXString (205)\"\n\tcase RecTypeSXDtr:\n\t\treturn \"SXDtr (206)\"\n\tcase RecTypeSxNil:\n\t\treturn \"SxNil (207)\"\n\tcase RecTypeSXTbl:\n\t\treturn \"SXTbl (208)\"\n\tcase RecTypeSXTBRGIITM:\n\t\treturn \"SXTBRGIITM (209)\"\n\tcase RecTypeSxTbpg:\n\t\treturn \"SxTbpg (210)\"\n\tcase RecTypeObProj:\n\t\treturn \"ObProj (211)\"\n\tcase RecTypeSXStreamID:\n\t\treturn \"SXStreamID (213)\"\n\tcase RecTypeDBCell:\n\t\treturn \"DBCell (215)\"\n\tcase RecTypeSXRng:\n\t\treturn \"SXRng (216)\"\n\tcase RecTypeSxIsxoper:\n\t\treturn \"SxIsxoper (217)\"\n\tcase RecTypeBookBool:\n\t\treturn \"BookBool (218)\"\n\tcase RecTypeDbOrParamQry:\n\t\treturn \"DbOrParamQry (220)\"\n\tcase RecTypeScenarioProtect:\n\t\treturn \"ScenarioProtect (221)\"\n\tcase RecTypeOleObjectSize:\n\t\treturn \"OleObjectSize (222)\"\n\tcase RecTypeXF:\n\t\treturn \"XF (224)\"\n\tcase RecTypeInterfaceHdr:\n\t\treturn \"InterfaceHdr (225)\"\n\tcase RecTypeInterfaceEnd:\n\t\treturn \"InterfaceEnd (226)\"\n\tcase RecTypeSXVS:\n\t\treturn \"SXVS (227)\"\n\tcase RecTypeMergeCells:\n\t\treturn \"MergeCells (229)\"\n\tcase RecTypeBkHim:\n\t\treturn \"BkHim (233)\"\n\tcase RecTypeMsoDrawingGroup:\n\t\treturn \"MsoDrawingGroup (235)\"\n\tcase RecTypeMsoDrawing:\n\t\treturn \"MsoDrawing (236)\"\n\tcase RecTypeMsoDrawingSelection:\n\t\treturn \"MsoDrawingSelection (237)\"\n\tcase RecTypePhoneticInfo:\n\t\treturn \"PhoneticInfo (239)\"\n\tcase RecTypeSxRule:\n\t\treturn \"SxRule (240)\"\n\tcase RecTypeSXEx:\n\t\treturn \"SXEx (241)\"\n\tcase RecTypeSxFilt:\n\t\treturn \"SxFilt (242)\"\n\tcase RecTypeSxDXF:\n\t\treturn \"SxDXF (244)\"\n\tcase RecTypeSxItm:\n\t\treturn \"SxItm (245)\"\n\tcase RecTypeSxName:\n\t\treturn \"SxName (246)\"\n\tcase RecTypeSxSelect:\n\t\treturn \"SxSelect (247)\"\n\tcase RecTypeSXPair:\n\t\treturn \"SXPair (248)\"\n\tcase RecTypeSxFmla:\n\t\treturn \"SxFmla (249)\"\n\tcase RecTypeSxFormat:\n\t\treturn \"SxFormat (251)\"\n\tcase RecTypeSST:\n\t\treturn \"SST (252)\"\n\tcase RecTypeLabelSst:\n\t\treturn \"LabelSst (253)\"\n\tcase RecTypeExtSST:\n\t\treturn \"ExtSST (255)\"\n\tcase RecTypeSXVDEx:\n\t\treturn \"SXVDEx (256)\"\n\tcase RecTypeSXFormula:\n\t\treturn \"SXFormula (259)\"\n\tcase RecTypeSXDBEx:\n\t\treturn \"SXDBEx (290)\"\n\tcase RecTypeRRDInsDel:\n\t\treturn \"RRDInsDel (311)\"\n\tcase RecTypeRRDHead:\n\t\treturn \"RRDHead (312)\"\n\tcase RecTypeRRDChgCell:\n\t\treturn \"RRDChgCell (315)\"\n\tcase RecTypeRRTabID:\n\t\treturn \"RRTabID (317)\"\n\tcase RecTypeRRDRenSheet:\n\t\treturn \"RRDRenSheet (318)\"\n\tcase RecTypeRRSort:\n\t\treturn \"RRSort (319)\"\n\tcase RecTypeRRDMove:\n\t\treturn \"RRDMove (320)\"\n\tcase RecTypeRRFormat:\n\t\treturn \"RRFormat (330)\"\n\tcase RecTypeRRAutoFmt:\n\t\treturn \"RRAutoFmt (331)\"\n\tcase RecTypeRRInsertSh:\n\t\treturn \"RRInsertSh (333)\"\n\tcase RecTypeRRDMoveBegin:\n\t\treturn \"RRDMoveBegin (334)\"\n\tcase RecTypeRRDMoveEnd:\n\t\treturn \"RRDMoveEnd (335)\"\n\tcase RecTypeRRDInsDelBegin:\n\t\treturn \"RRDInsDelBegin (336)\"\n\tcase RecTypeRRDInsDelEnd:\n\t\treturn \"RRDInsDelEnd (337)\"\n\tcase RecTypeRRDConflict:\n\t\treturn \"RRDConflict (338)\"\n\tcase RecTypeRRDDefName:\n\t\treturn \"RRDDefName (339)\"\n\tcase RecTypeRRDRstEtxp:\n\t\treturn \"RRDRstEtxp (340)\"\n\tcase RecTypeLRng:\n\t\treturn \"LRng (351)\"\n\tcase RecTypeUsesELFs:\n\t\treturn \"UsesELFs (352)\"\n\tcase RecTypeDSF:\n\t\treturn \"DSF (353)\"\n\tcase RecTypeCUsr:\n\t\treturn \"CUsr (401)\"\n\tcase RecTypeCbUsr:\n\t\treturn \"CbUsr (402)\"\n\tcase RecTypeUsrInfo:\n\t\treturn \"UsrInfo (403)\"\n\tcase RecTypeUsrExcl:\n\t\treturn \"UsrExcl (404)\"\n\tcase RecTypeFileLock:\n\t\treturn \"FileLock (405)\"\n\tcase RecTypeRRDInfo:\n\t\treturn \"RRDInfo (406)\"\n\tcase RecTypeBCUsrs:\n\t\treturn \"BCUsrs (407)\"\n\tcase RecTypeUsrChk:\n\t\treturn \"UsrChk (408)\"\n\tcase RecTypeUserBView:\n\t\treturn \"UserBView (425)\"\n\tcase RecTypeUserSViewBegin:\n\t\treturn \"UserSViewBegin[Chart] (426)\"\n\tcase RecTypeUserSViewEnd:\n\t\treturn \"UserSViewEnd (427)\"\n\tcase RecTypeRRDUserView:\n\t\treturn \"RRDUserView (428)\"\n\tcase RecTypeQsi:\n\t\treturn \"Qsi (429)\"\n\tcase RecTypeSupBook:\n\t\treturn \"SupBook (430)\"\n\tcase RecTypeProt4Rev:\n\t\treturn \"Prot4Rev (431)\"\n\tcase RecTypeCondFmt:\n\t\treturn \"CondFmt (432)\"\n\tcase RecTypeCF:\n\t\treturn \"CF (433)\"\n\tcase RecTypeDVal:\n\t\treturn \"DVal (434)\"\n\tcase RecTypeDConBin:\n\t\treturn \"DConBin (437)\"\n\tcase RecTypeTxO:\n\t\treturn \"TxO (438)\"\n\tcase RecTypeRefreshAll:\n\t\treturn \"RefreshAll (439)\"\n\tcase RecTypeHLink:\n\t\treturn \"HLink (440)\"\n\tcase RecTypeLel:\n\t\treturn \"Lel (441)\"\n\tcase RecTypeCodeName:\n\t\treturn \"CodeName (442)\"\n\tcase RecTypeSXFDBType:\n\t\treturn \"SXFDBType (443)\"\n\tcase RecTypeProt4RevPass:\n\t\treturn \"Prot4RevPass (444)\"\n\tcase RecTypeObNoMacros:\n\t\treturn \"ObNoMacros (445)\"\n\tcase RecTypeDv:\n\t\treturn \"Dv (446)\"\n\tcase RecTypeExcel9File:\n\t\treturn \"Excel9File (448)\"\n\tcase RecTypeRecalcID:\n\t\treturn \"RecalcID (449)\"\n\tcase RecTypeEntExU2:\n\t\treturn \"EntExU2 (450)\"\n\tcase RecTypeDimensions:\n\t\treturn \"Dimensions (512)\"\n\tcase RecTypeBlank:\n\t\treturn \"Blank (513)\"\n\tcase RecTypeNumber:\n\t\treturn \"Number (515)\"\n\tcase RecTypeLabel:\n\t\treturn \"Label (516)\"\n\tcase RecTypeBoolErr:\n\t\treturn \"BoolErr (517)\"\n\tcase RecTypeString:\n\t\treturn \"String (519)\"\n\tcase RecTypeRow:\n\t\treturn \"Row (520)\"\n\tcase RecTypeIndex:\n\t\treturn \"Index (523)\"\n\tcase RecTypeArray:\n\t\treturn \"Array (545)\"\n\tcase RecTypeDefaultRowHeight:\n\t\treturn \"DefaultRowHeight (549)\"\n\tcase RecTypeTable:\n\t\treturn \"Table (566)\"\n\tcase RecTypeWindow2:\n\t\treturn \"Window2 (574)\"\n\tcase RecTypeRK:\n\t\treturn \"RK (638)\"\n\tcase RecTypeStyle:\n\t\treturn \"Style (659)\"\n\tcase RecTypeBigName:\n\t\treturn \"BigName (1048)\"\n\tcase RecTypeFormat:\n\t\treturn \"Format (1054)\"\n\tcase RecTypeContinueBigName:\n\t\treturn \"ContinueBigName (1084)\"\n\tcase RecTypeShrFmla:\n\t\treturn \"ShrFmla (1212)\"\n\tcase RecTypeHLinkTooltip:\n\t\treturn \"HLinkTooltip (2048)\"\n\tcase RecTypeWebPub:\n\t\treturn \"WebPub (2049)\"\n\tcase RecTypeQsiSXTag:\n\t\treturn \"QsiSXTag (2050)\"\n\tcase RecTypeDBQueryExt:\n\t\treturn \"DBQueryExt (2051)\"\n\tcase RecTypeExtString:\n\t\treturn \"ExtString (2052)\"\n\tcase RecTypeTxtQry:\n\t\treturn \"TxtQry (2053)\"\n\tcase RecTypeQsir:\n\t\treturn \"Qsir (2054)\"\n\tcase RecTypeQsif:\n\t\treturn \"Qsif (2055)\"\n\tcase RecTypeRRDTQSIF:\n\t\treturn \"RRDTQSIF (2056)\"\n\tcase RecTypeBOF:\n\t\treturn \"BOF (2057)\"\n\tcase RecTypeOleDbConn:\n\t\treturn \"OleDbConn (2058)\"\n\tcase RecTypeWOpt:\n\t\treturn \"WOpt (2059)\"\n\tcase RecTypeSXViewEx:\n\t\treturn \"SXViewEx (2060)\"\n\tcase RecTypeSXTH:\n\t\treturn \"SXTH (2061)\"\n\tcase RecTypeSXPIEx:\n\t\treturn \"SXPIEx (2062)\"\n\tcase RecTypeSXVDTEx:\n\t\treturn \"SXVDTEx (2063)\"\n\tcase RecTypeSXViewEx9:\n\t\treturn \"SXViewEx9 (2064)\"\n\tcase RecTypeContinueFrt:\n\t\treturn \"ContinueFrt (2066)\"\n\tcase RecTypeRealTimeData:\n\t\treturn \"RealTimeData (2067)\"\n\tcase RecTypeChartFrtInfo:\n\t\treturn \"ChartFrtInfo (2128)\"\n\tcase RecTypeFrtWrapper:\n\t\treturn \"FrtWrapper (2129)\"\n\tcase RecTypeStartBlock:\n\t\treturn \"StartBlock (2130)\"\n\tcase RecTypeEndBlock:\n\t\treturn \"EndBlock (2131)\"\n\tcase RecTypeStartObject:\n\t\treturn \"StartObject (2132)\"\n\tcase RecTypeEndObject:\n\t\treturn \"EndObject (2133)\"\n\tcase RecTypeCatLab:\n\t\treturn \"CatLab (2134)\"\n\tcase RecTypeYMult:\n\t\treturn \"YMult (2135)\"\n\tcase RecTypeSXViewLink:\n\t\treturn \"SXViewLink (2136)\"\n\tcase RecTypePivotChartBits:\n\t\treturn \"PivotChartBits (2137)\"\n\tcase RecTypeFrtFontList:\n\t\treturn \"FrtFontList (2138)\"\n\tcase RecTypeSheetExt:\n\t\treturn \"SheetExt (2146)\"\n\tcase RecTypeBookExt:\n\t\treturn \"BookExt (2147)\"\n\tcase RecTypeSXAddl:\n\t\treturn \"SXAddl (2148)\"\n\tcase RecTypeCrErr:\n\t\treturn \"CrErr (2149)\"\n\tcase RecTypeHFPicture:\n\t\treturn \"HFPicture (2150)\"\n\tcase RecTypeFeatHdr:\n\t\treturn \"FeatHdr (2151)\"\n\tcase RecTypeFeat:\n\t\treturn \"Feat (2152)\"\n\tcase RecTypeDataLabExt:\n\t\treturn \"DataLabExt (2154)\"\n\tcase RecTypeDataLabExtContents:\n\t\treturn \"DataLabExtContents (2155)\"\n\tcase RecTypeCellWatch:\n\t\treturn \"CellWatch (2156)\"\n\tcase RecTypeFeatHdr11:\n\t\treturn \"FeatHdr11 (2161)\"\n\tcase RecTypeFeature11:\n\t\treturn \"Feature11 (2162)\"\n\tcase RecTypeDropDownObjIds:\n\t\treturn \"DropDownObjIds (2164)\"\n\tcase RecTypeContinueFrt11:\n\t\treturn \"ContinueFrt11 (2165)\"\n\tcase RecTypeDConn:\n\t\treturn \"DConn (2166)\"\n\tcase RecTypeList12:\n\t\treturn \"List12 (2167)\"\n\tcase RecTypeFeature12:\n\t\treturn \"Feature12 (2168)\"\n\tcase RecTypeCondFmt12:\n\t\treturn \"CondFmt12 (2169)\"\n\tcase RecTypeCF12:\n\t\treturn \"CF12 (2170)\"\n\tcase RecTypeCFEx:\n\t\treturn \"CFEx (2171)\"\n\tcase RecTypeXFCRC:\n\t\treturn \"XFCRC (2172)\"\n\tcase RecTypeXFExt:\n\t\treturn \"XFExt (2173)\"\n\tcase RecTypeAutoFilter12:\n\t\treturn \"AutoFilter12 (2174)\"\n\tcase RecTypeContinueFrt12:\n\t\treturn \"ContinueFrt12 (2175)\"\n\tcase RecTypeMDTInfo:\n\t\treturn \"MDTInfo (2180)\"\n\tcase RecTypeMDXStr:\n\t\treturn \"MDXStr (2181)\"\n\tcase RecTypeMDXTuple:\n\t\treturn \"MDXTuple (2182)\"\n\tcase RecTypeMDXSet:\n\t\treturn \"MDXSet (2183)\"\n\tcase RecTypeMDXProp:\n\t\treturn \"MDXProp (2184)\"\n\tcase RecTypeMDXKPI:\n\t\treturn \"MDXKPI (2185)\"\n\tcase RecTypeMDB:\n\t\treturn \"MDB (2186)\"\n\tcase RecTypePLV:\n\t\treturn \"PLV (2187)\"\n\tcase RecTypeCompat12:\n\t\treturn \"Compat12 (2188)\"\n\tcase RecTypeDXF:\n\t\treturn \"DXF (2189)\"\n\tcase RecTypeTableStyles:\n\t\treturn \"TableStyles (2190)\"\n\tcase RecTypeTableStyle:\n\t\treturn \"TableStyle (2191)\"\n\tcase RecTypeTableStyleElement:\n\t\treturn \"TableStyleElement (2192)\"\n\tcase RecTypeStyleExt:\n\t\treturn \"StyleExt (2194)\"\n\tcase RecTypeNamePublish:\n\t\treturn \"NamePublish (2195)\"\n\tcase RecTypeNameCmt:\n\t\treturn \"NameCmt (2196)\"\n\tcase RecTypeSortData:\n\t\treturn \"SortData (2197)\"\n\tcase RecTypeTheme:\n\t\treturn \"Theme (2198)\"\n\tcase RecTypeGUIDTypeLib:\n\t\treturn \"GUIDTypeLib (2199)\"\n\tcase RecTypeFnGrp12:\n\t\treturn \"FnGrp12 (2200)\"\n\tcase RecTypeNameFnGrp12:\n\t\treturn \"NameFnGrp12 (2201)\"\n\tcase RecTypeMTRSettings:\n\t\treturn \"MTRSettings (2202)\"\n\tcase RecTypeCompressPictures:\n\t\treturn \"CompressPictures (2203)\"\n\tcase RecTypeHeaderFooter:\n\t\treturn \"HeaderFooter (2204)\"\n\tcase RecTypeCrtLayout12:\n\t\treturn \"CrtLayout12 (2205)\"\n\tcase RecTypeCrtMlFrt:\n\t\treturn \"CrtMlFrt (2206)\"\n\tcase RecTypeCrtMlFrtContinue:\n\t\treturn \"CrtMlFrtContinue (2207)\"\n\tcase RecTypeForceFullCalculation:\n\t\treturn \"ForceFullCalculation (2211)\"\n\tcase RecTypeShapePropsStream:\n\t\treturn \"ShapePropsStream (2212)\"\n\tcase RecTypeTextPropsStream:\n\t\treturn \"TextPropsStream (2213)\"\n\tcase RecTypeRichTextStream:\n\t\treturn \"RichTextStream (2214)\"\n\tcase RecTypeCrtLayout12A:\n\t\treturn \"CrtLayout12A (2215)\"\n\tcase RecTypeUnits:\n\t\treturn \"Units (4097)\"\n\tcase RecTypeChart:\n\t\treturn \"Chart (4098)\"\n\tcase RecTypeSeries:\n\t\treturn \"Series (4099)\"\n\tcase RecTypeDataFormat:\n\t\treturn \"DataFormat (4102)\"\n\tcase RecTypeLineFormat:\n\t\treturn \"LineFormat (4103)\"\n\tcase RecTypeMarkerFormat:\n\t\treturn \"MarkerFormat (4105)\"\n\tcase RecTypeAreaFormat:\n\t\treturn \"AreaFormat (4106)\"\n\tcase RecTypePieFormat:\n\t\treturn \"PieFormat (4107)\"\n\tcase RecTypeAttachedLabel:\n\t\treturn \"AttachedLabel (4108)\"\n\tcase RecTypeSeriesText:\n\t\treturn \"SeriesText (4109)\"\n\tcase RecTypeChartFormat:\n\t\treturn \"ChartFormat (4116)\"\n\tcase RecTypeLegend:\n\t\treturn \"Legend (4117)\"\n\tcase RecTypeSeriesList:\n\t\treturn \"SeriesList (4118)\"\n\tcase RecTypeBar:\n\t\treturn \"Bar (4119)\"\n\tcase RecTypeLine:\n\t\treturn \"Line (4120)\"\n\tcase RecTypePie:\n\t\treturn \"Pie (4121)\"\n\tcase RecTypeArea:\n\t\treturn \"Area (4122)\"\n\tcase RecTypeScatter:\n\t\treturn \"Scatter (4123)\"\n\tcase RecTypeCrtLine:\n\t\treturn \"CrtLine (4124)\"\n\tcase RecTypeAxis:\n\t\treturn \"Axis (4125)\"\n\tcase RecTypeTick:\n\t\treturn \"Tick (4126)\"\n\tcase RecTypeValueRange:\n\t\treturn \"ValueRange (4127)\"\n\tcase RecTypeCatSerRange:\n\t\treturn \"CatSerRange (4128)\"\n\tcase RecTypeAxisLine:\n\t\treturn \"AxisLine (4129)\"\n\tcase RecTypeCrtLink:\n\t\treturn \"CrtLink (4130)\"\n\tcase RecTypeDefaultText:\n\t\treturn \"DefaultText (4132)\"\n\tcase RecTypeText:\n\t\treturn \"Text (4133)\"\n\tcase RecTypeFontX:\n\t\treturn \"FontX (4134)\"\n\tcase RecTypeObjectLink:\n\t\treturn \"ObjectLink (4135)\"\n\tcase RecTypeFrame:\n\t\treturn \"Frame (4146)\"\n\tcase RecTypeBegin:\n\t\treturn \"Begin (4147)\"\n\tcase RecTypeEnd:\n\t\treturn \"End (4148)\"\n\tcase RecTypePlotArea:\n\t\treturn \"PlotArea (4149)\"\n\tcase RecTypeChart3d:\n\t\treturn \"Chart3d (4154)\"\n\tcase RecTypePicF:\n\t\treturn \"PicF (4156)\"\n\tcase RecTypeDropBar:\n\t\treturn \"DropBar (4157)\"\n\tcase RecTypeRadar:\n\t\treturn \"Radar (4158)\"\n\tcase RecTypeSurf:\n\t\treturn \"Surf (4159)\"\n\tcase RecTypeRadarArea:\n\t\treturn \"RadarArea (4160)\"\n\tcase RecTypeAxisParent:\n\t\treturn \"AxisParent (4161)\"\n\tcase RecTypeLegendException:\n\t\treturn \"LegendException (4163)\"\n\tcase RecTypeShtProps:\n\t\treturn \"ShtProps (4164)\"\n\tcase RecTypeSerToCrt:\n\t\treturn \"SerToCrt (4165)\"\n\tcase RecTypeAxesUsed:\n\t\treturn \"AxesUsed (4166)\"\n\tcase RecTypeSBaseRef:\n\t\treturn \"SBaseRef (4168)\"\n\tcase RecTypeSerParent:\n\t\treturn \"SerParent (4170)\"\n\tcase RecTypeSerAuxTrend:\n\t\treturn \"SerAuxTrend (4171)\"\n\tcase RecTypeIFmtRecord:\n\t\treturn \"IFmtRecord (4174)\"\n\tcase RecTypePos:\n\t\treturn \"Pos (4175)\"\n\tcase RecTypeAlRuns:\n\t\treturn \"AlRuns (4176)\"\n\tcase RecTypeBRAI:\n\t\treturn \"BRAI (4177)\"\n\tcase RecTypeSerAuxErrBar:\n\t\treturn \"SerAuxErrBar (4187)\"\n\tcase RecTypeClrtClient:\n\t\treturn \"ClrtClient (4188)\"\n\tcase RecTypeSerFmt:\n\t\treturn \"SerFmt (4189)\"\n\tcase RecTypeChart3DBarShape:\n\t\treturn \"Chart3DBarShape (4191)\"\n\tcase RecTypeFbi:\n\t\treturn \"Fbi (4192)\"\n\tcase RecTypeBopPop:\n\t\treturn \"BopPop (4193)\"\n\tcase RecTypeAxcExt:\n\t\treturn \"AxcExt (4194)\"\n\tcase RecTypeDat:\n\t\treturn \"Dat (4195)\"\n\tcase RecTypePlotGrowth:\n\t\treturn \"PlotGrowth (4196)\"\n\tcase RecTypeSIIndex:\n\t\treturn \"SIIndex (4197)\"\n\tcase RecTypeGelFrame:\n\t\treturn \"GelFrame (4198)\"\n\tcase RecTypeBopPopCustom:\n\t\treturn \"BopPopCustom (4199)\"\n\tcase RecTypeFbi2:\n\t\treturn \"Fbi2 (4200)\"\n\t}\n\treturn fmt.Sprintf(\"unknown (%d 0x%x)\", uint16(r), uint16(r))\n}\n"
  },
  {
    "path": "xls/sheets.go",
    "content": "package xls\n\nimport (\n\t\"encoding/binary\"\n\t\"errors\"\n\t\"log\"\n\t\"math\"\n\t\"unicode/utf16\"\n\n\t\"github.com/pbnjay/grate\"\n\t\"github.com/pbnjay/grate/commonxl\"\n)\n\n// List (visible) sheet names from the workbook.\nfunc (b *WorkBook) List() ([]string, error) {\n\tres := make([]string, 0, len(b.sheets))\n\tfor _, s := range b.sheets {\n\t\tif (s.HiddenState & 0x03) == 0 {\n\t\t\tres = append(res, s.Name)\n\t\t}\n\t}\n\treturn res, nil\n}\n\n// ListHidden sheet names in the workbook.\nfunc (b *WorkBook) ListHidden() ([]string, error) {\n\tres := make([]string, 0, len(b.sheets))\n\tfor _, s := range b.sheets {\n\t\tif (s.HiddenState & 0x03) != 0 {\n\t\t\tres = append(res, s.Name)\n\t\t}\n\t}\n\treturn res, nil\n}\n\n// Get opens the named worksheet and return an iterator for its contents.\nfunc (b *WorkBook) Get(sheetName string) (grate.Collection, error) {\n\tfor _, s := range b.sheets {\n\t\tif s.Name == sheetName {\n\t\t\tss := b.pos2substream[int64(s.Position)]\n\t\t\treturn b.parseSheet(s, ss)\n\t\t}\n\t}\n\treturn nil, errors.New(\"xls: sheet not found\")\n}\n\nfunc (b *WorkBook) parseSheet(s *boundSheet, ss int) (*commonxl.Sheet, error) {\n\tres := &commonxl.Sheet{\n\t\tFormatter: &b.nfmt,\n\t}\n\tvar minRow, maxRow uint32\n\tvar minCol, maxCol uint16\n\n\t// temporary string buffer\n\tus := make([]uint16, 8224)\n\n\tinSubstream := 0\n\tfor idx, r := range b.substreams[ss] {\n\t\tif inSubstream > 0 {\n\t\t\tif r.RecType == RecTypeEOF {\n\t\t\t\tinSubstream--\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tswitch r.RecType {\n\t\tcase RecTypeBOF:\n\t\t\t// a BOF inside a sheet usually means embedded content like a chart\n\t\t\t// (which we aren't interested in). So we we set a flag and wait\n\t\t\t// for the EOF for that content block.\n\t\t\tif idx > 0 {\n\t\t\t\tinSubstream++\n\t\t\t\tcontinue\n\t\t\t}\n\t\tcase RecTypeWsBool:\n\t\t\tif (r.Data[1] & 0x10) != 0 {\n\t\t\t\t// it's a dialog\n\t\t\t\treturn nil, nil\n\t\t\t}\n\n\t\tcase RecTypeDimensions:\n\t\t\t// max = 0-based index of the row AFTER the last valid index\n\t\t\tminRow = binary.LittleEndian.Uint32(r.Data[:4])\n\t\t\tmaxRow = binary.LittleEndian.Uint32(r.Data[4:8]) // max = 0x010000\n\t\t\tminCol = binary.LittleEndian.Uint16(r.Data[8:10])\n\t\t\tmaxCol = binary.LittleEndian.Uint16(r.Data[10:12]) // max = 0x000100\n\t\t\tif grate.Debug {\n\t\t\t\tlog.Printf(\"    Sheet dimensions (%d, %d) - (%d,%d)\",\n\t\t\t\t\tminCol, minRow, maxCol, maxRow)\n\t\t\t}\n\t\t\tif minRow > 0x0000FFFF || maxRow > 0x00010000 {\n\t\t\t\tlog.Println(\"invalid dimensions\")\n\t\t\t}\n\t\t\tif minCol > 0x00FF || maxCol > 0x0100 {\n\t\t\t\tlog.Println(\"invalid dimensions\")\n\t\t\t}\n\n\t\t\t// pre-allocate cells\n\t\t\tres.Resize(int(maxRow), int(maxCol))\n\t\t}\n\t}\n\tinSubstream = 0\n\n\tvar formulaRow, formulaCol uint16\n\tfor ridx, r := range b.substreams[ss] {\n\t\tif inSubstream > 0 {\n\t\t\tif r.RecType == RecTypeEOF {\n\t\t\t\tinSubstream--\n\t\t\t} else if grate.Debug {\n\t\t\t\tlog.Println(\"      Unhandled sheet substream record type:\", r.RecType, ridx)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\t// sec 2.1.7.20.6 Common Productions ABNF:\n\t\t/*\n\t\t\tCELLTABLE = 1*(1*Row *CELL 1*DBCell) *EntExU2\n\t\t\tCELL = FORMULA / Blank / MulBlank / RK / MulRk / BoolErr / Number / LabelSst\n\t\t\tFORMULA = [Uncalced] Formula [Array / Table / ShrFmla / SUB] [String *Continue]\n\n\t\t\tNot parsed form the list above:\n\t\t\t\tDBCell, EntExU2, Uncalced, Array, Table,ShrFmla\n\t\t\t\tNB: no idea what \"SUB\" is\n\t\t*/\n\n\t\tswitch r.RecType {\n\t\tcase RecTypeBOF:\n\t\t\tif ridx > 0 {\n\t\t\t\tinSubstream++\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\tcase RecTypeBoolErr:\n\t\t\trowIndex := int(binary.LittleEndian.Uint16(r.Data[:2]))\n\t\t\tcolIndex := int(binary.LittleEndian.Uint16(r.Data[2:4]))\n\t\t\tixfe := int(binary.LittleEndian.Uint16(r.Data[4:6]))\n\t\t\tif r.Data[7] == 0 {\n\t\t\t\t// Boolean value\n\t\t\t\tbv := false\n\t\t\t\tif r.Data[6] == 1 {\n\t\t\t\t\tbv = true\n\t\t\t\t}\n\t\t\t\tvar fno uint16\n\t\t\t\tif ixfe < len(b.xfs) {\n\t\t\t\t\tfno = b.xfs[ixfe]\n\t\t\t\t}\n\t\t\t\tres.Put(rowIndex, colIndex, bv, fno)\n\t\t\t\t//log.Printf(\"bool/error spec: %d %d %+v\", rowIndex, colIndex, bv)\n\t\t\t} else {\n\t\t\t\t// it's an error, load the label\n\t\t\t\tbe, ok := berrLookup[r.Data[6]]\n\t\t\t\tif !ok {\n\t\t\t\t\tbe = \"<unknown error>\"\n\t\t\t\t}\n\t\t\t\tres.Put(rowIndex, colIndex, be, 0)\n\t\t\t\t//log.Printf(\"bool/error spec: %d %d %s\", rowIndex, colIndex, be)\n\t\t\t}\n\n\t\tcase RecTypeMulRk:\n\t\t\t// MulRk encodes multiple RK values in a row\n\t\t\tnrk := int((r.RecSize - 6) / 6)\n\t\t\trowIndex := int(binary.LittleEndian.Uint16(r.Data[:2]))\n\t\t\tcolIndex := int(binary.LittleEndian.Uint16(r.Data[2:4]))\n\t\t\tfor i := 0; i < nrk; i++ {\n\t\t\t\toff := 4 + i*6\n\t\t\t\tixfe := int(binary.LittleEndian.Uint16(r.Data[off:]))\n\t\t\t\tvalue := RKNumber(binary.LittleEndian.Uint32(r.Data[off+2:]))\n\n\t\t\t\tvar rval interface{}\n\t\t\t\tif value.IsInteger() {\n\t\t\t\t\trval = value.Int()\n\t\t\t\t} else {\n\t\t\t\t\trval = value.Float64()\n\t\t\t\t}\n\t\t\t\tvar fno uint16\n\t\t\t\tif ixfe < len(b.xfs) {\n\t\t\t\t\tfno = b.xfs[ixfe]\n\t\t\t\t}\n\t\t\t\tres.Put(rowIndex, colIndex+i, rval, fno)\n\t\t\t}\n\t\t\t//log.Printf(\"mulrow spec: %+v\", *mr)\n\n\t\tcase RecTypeNumber:\n\t\t\trowIndex := int(binary.LittleEndian.Uint16(r.Data[:2]))\n\t\t\tcolIndex := int(binary.LittleEndian.Uint16(r.Data[2:4]))\n\t\t\tixfe := int(binary.LittleEndian.Uint16(r.Data[4:6]))\n\t\t\txnum := binary.LittleEndian.Uint64(r.Data[6:])\n\n\t\t\tvalue := math.Float64frombits(xnum)\n\t\t\tvar fno uint16\n\t\t\tif ixfe < len(b.xfs) {\n\t\t\t\tfno = b.xfs[ixfe]\n\t\t\t}\n\t\t\tres.Put(rowIndex, colIndex, value, fno)\n\t\t\t//log.Printf(\"Number spec: %d %d = %f\", rowIndex, colIndex, value)\n\n\t\tcase RecTypeRK:\n\t\t\trowIndex := int(binary.LittleEndian.Uint16(r.Data[:2]))\n\t\t\tcolIndex := int(binary.LittleEndian.Uint16(r.Data[2:4]))\n\t\t\tixfe := int(binary.LittleEndian.Uint16(r.Data[4:]))\n\t\t\tvalue := RKNumber(binary.LittleEndian.Uint32(r.Data[6:]))\n\n\t\t\tvar rval interface{}\n\t\t\tif value.IsInteger() {\n\t\t\t\trval = value.Int()\n\t\t\t} else {\n\t\t\t\trval = value.Float64()\n\t\t\t}\n\t\t\tvar fno uint16\n\t\t\tif ixfe < len(b.xfs) {\n\t\t\t\tfno = b.xfs[ixfe]\n\t\t\t}\n\t\t\tres.Put(rowIndex, colIndex, rval, fno)\n\t\t\t//log.Printf(\"RK spec: %d %d = %+v\", rowIndex, colIndex, rval)\n\n\t\tcase RecTypeFormula:\n\t\t\tformulaRow = binary.LittleEndian.Uint16(r.Data[:2])\n\t\t\tformulaCol = binary.LittleEndian.Uint16(r.Data[2:4])\n\t\t\tixfe := int(binary.LittleEndian.Uint16(r.Data[4:6]))\n\t\t\tfdata := r.Data[6:]\n\t\t\tvar fno uint16\n\t\t\tif ixfe < len(b.xfs) {\n\t\t\t\tfno = b.xfs[ixfe]\n\t\t\t}\n\t\t\tif fdata[6] == 0xFF && fdata[7] == 0xFF {\n\t\t\t\tswitch fdata[0] {\n\t\t\t\tcase 0:\n\t\t\t\t\t// string in next record\n\t\t\t\t\t// put placeholder now to record the numFmt\n\t\t\t\t\tres.Put(int(formulaRow), int(formulaCol), \"\", fno)\n\t\t\t\tcase 1:\n\t\t\t\t\t// boolean\n\t\t\t\t\tbv := false\n\t\t\t\t\tif fdata[2] != 0 {\n\t\t\t\t\t\tbv = true\n\t\t\t\t\t}\n\t\t\t\t\tres.Put(int(formulaRow), int(formulaCol), bv, fno)\n\t\t\t\tcase 2:\n\t\t\t\t\t// error value\n\t\t\t\t\tbe, ok := berrLookup[fdata[2]]\n\t\t\t\t\tif !ok {\n\t\t\t\t\t\tbe = \"<unknown error>\"\n\t\t\t\t\t}\n\t\t\t\t\tres.Put(int(formulaRow), int(formulaCol), be, 0)\n\t\t\t\tcase 3:\n\t\t\t\t\t// blank string\n\t\t\t\tdefault:\n\t\t\t\t\tlog.Printf(\"unknown formula value type %d\", fdata[0])\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\txnum := binary.LittleEndian.Uint64(fdata)\n\t\t\t\tvalue := math.Float64frombits(xnum)\n\t\t\t\tres.Put(int(formulaRow), int(formulaCol), value, fno)\n\t\t\t}\n\t\t\t//log.Printf(\"formula spec: %d %d ~~ %+v\", formulaRow, formulaCol, r.Data)\n\n\t\tcase RecTypeString:\n\t\t\t// String is the previously rendered value of a formula\n\t\t\t// NB similar to the workbook SST, this can continue over\n\t\t\t// addition records up to 32k characters. A 1-byte flag\n\t\t\t// at each gap indicates if the encoding switches\n\t\t\t// to/from 8/16-bit characters.\n\n\t\t\tcharCount := binary.LittleEndian.Uint16(r.Data[:2])\n\t\t\tflags := r.Data[2]\n\t\t\tfstr := \"\"\n\t\t\tif (flags & 1) == 0 {\n\t\t\t\tfstr = string(r.Data[3:])\n\t\t\t} else {\n\t\t\t\traw := r.Data[3:]\n\t\t\t\tif int(charCount) > cap(us) {\n\t\t\t\t\tus = make([]uint16, charCount)\n\t\t\t\t}\n\t\t\t\tus = us[:charCount]\n\t\t\t\tfor i := 0; i < int(charCount); i++ {\n\t\t\t\t\tus[i] = binary.LittleEndian.Uint16(raw)\n\t\t\t\t\traw = raw[2:]\n\t\t\t\t}\n\t\t\t\tfstr = string(utf16.Decode(us))\n\t\t\t}\n\n\t\t\tif (ridx + 1) < len(b.substreams[ss]) {\n\t\t\t\tridx2 := ridx + 1\n\t\t\t\tnrecs := len(b.substreams[ss])\n\t\t\t\tfor ridx2 < nrecs {\n\t\t\t\t\tr2 := b.substreams[ss][ridx2]\n\t\t\t\t\tif r2.RecType != RecTypeContinue {\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t\tif (r2.Data[0] & 1) == 0 {\n\t\t\t\t\t\tfstr += string(r2.Data[1:])\n\t\t\t\t\t} else {\n\t\t\t\t\t\traw := r2.Data[1:]\n\t\t\t\t\t\tslen := len(raw) / 2\n\t\t\t\t\t\tus = us[:slen]\n\t\t\t\t\t\tfor i := 0; i < slen; i++ {\n\t\t\t\t\t\t\tus[i] = binary.LittleEndian.Uint16(raw)\n\t\t\t\t\t\t\traw = raw[2:]\n\t\t\t\t\t\t}\n\t\t\t\t\t\tfstr += string(utf16.Decode(us))\n\t\t\t\t\t}\n\t\t\t\t\tridx2++\n\t\t\t\t}\n\t\t\t}\n\t\t\tres.Set(int(formulaRow), int(formulaCol), fstr)\n\t\t\t//log.Printf(\"String direct: %d %d '%s'\", int(formulaRow), int(formulaCol), fstr)\n\n\t\tcase RecTypeLabelSst:\n\t\t\trowIndex := int(binary.LittleEndian.Uint16(r.Data[:2]))\n\t\t\tcolIndex := int(binary.LittleEndian.Uint16(r.Data[2:4]))\n\t\t\tixfe := int(binary.LittleEndian.Uint16(r.Data[4:6]))\n\t\t\tsstIndex := int(binary.LittleEndian.Uint32(r.Data[6:]))\n\t\t\tif sstIndex > len(b.strings) {\n\t\t\t\treturn nil, errors.New(\"xls: invalid sst index\")\n\t\t\t}\n\t\t\tvar fno uint16\n\t\t\tif ixfe < len(b.xfs) {\n\t\t\t\tfno = b.xfs[ixfe]\n\t\t\t}\n\t\t\tif b.strings[sstIndex] != \"\" {\n\t\t\t\tres.Put(rowIndex, colIndex, b.strings[sstIndex], fno)\n\t\t\t}\n\t\t\t//log.Printf(\"SST spec: %d %d = [%d] '%s' %d\", rowIndex, colIndex, sstIndex, b.strings[sstIndex], fno)\n\n\t\tcase RecTypeHLink:\n\t\t\tfirstRow := binary.LittleEndian.Uint16(r.Data[:2])\n\t\t\tlastRow := binary.LittleEndian.Uint16(r.Data[2:4])\n\t\t\tfirstCol := binary.LittleEndian.Uint16(r.Data[4:6])\n\t\t\tlastCol := binary.LittleEndian.Uint16(r.Data[6:])\n\t\t\tif int(firstCol) > int(maxCol) {\n\t\t\t\t//log.Println(\"invalid hyperlink column\")\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif int(firstRow) > int(maxRow) {\n\t\t\t\t//log.Println(\"invalid hyperlink row\")\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif lastRow == 0xFFFF { // placeholder value indicate \"last\"\n\t\t\t\tlastRow = uint16(maxRow) - 1\n\t\t\t}\n\t\t\tif lastCol == 0xFF { // placeholder value indicate \"last\"\n\t\t\t\tlastCol = uint16(maxCol) - 1\n\t\t\t}\n\n\t\t\t// decode the hyperlink datastructure and try to find the\n\t\t\t// display text and separate the URL itself.\n\t\t\tdisplayText, linkText, err := decodeHyperlinks(r.Data[8:])\n\t\t\tif err != nil {\n\t\t\t\tlog.Println(err)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// apply merge cell rules (see RecTypeMergeCells below)\n\t\t\tfor rn := int(firstRow); rn <= int(lastRow); rn++ {\n\t\t\t\tfor cn := int(firstCol); cn <= int(lastCol); cn++ {\n\t\t\t\t\tif rn == int(firstRow) && cn == int(firstCol) {\n\t\t\t\t\t\t// TODO: provide custom hooks for how to handle links in output\n\t\t\t\t\t\tres.Put(rn, cn, displayText+\" <\"+linkText+\">\", 0)\n\t\t\t\t\t} else if cn == int(firstCol) {\n\t\t\t\t\t\t// first and last column MAY be the same\n\t\t\t\t\t\tif rn == int(lastRow) {\n\t\t\t\t\t\t\tres.Put(rn, cn, grate.EndRowMerged, 0)\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tres.Put(rn, cn, grate.ContinueRowMerged, 0)\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if cn == int(lastCol) {\n\t\t\t\t\t\t// first and last column are NOT the same\n\t\t\t\t\t\tres.Put(rn, cn, grate.EndColumnMerged, 0)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tres.Put(rn, cn, grate.ContinueColumnMerged, 0)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\tcase RecTypeMergeCells:\n\t\t\t// To keep cells aligned, Merged cells are handled by placing\n\t\t\t// special characters in each cell covered by the merge block.\n\t\t\t//\n\t\t\t// The contents of the cell are always in the top left position.\n\t\t\t// A \"down arrow\" (↓) indicates the left side of the merge block, and a\n\t\t\t// \"down arrow with stop line\" (⤓) indicates the last row of the merge.\n\t\t\t// A \"right arrow\" (→) indicates that the columns span horizontally,\n\t\t\t// and a \"right arrow with stop line\" (⇥) indicates the rightmost\n\t\t\t// column of the merge.\n\t\t\t//\n\n\t\t\tcmcs := binary.LittleEndian.Uint16(r.Data[:2])\n\t\t\traw := r.Data[2:]\n\t\t\tfor i := 0; i < int(cmcs); i++ {\n\t\t\t\tfirstRow := binary.LittleEndian.Uint16(raw[:2])\n\t\t\t\tlastRow := binary.LittleEndian.Uint16(raw[2:4])\n\t\t\t\tfirstCol := binary.LittleEndian.Uint16(raw[4:6])\n\t\t\t\tlastCol := binary.LittleEndian.Uint16(raw[6:])\n\t\t\t\traw = raw[8:]\n\n\t\t\t\tif lastRow == 0xFFFF { // placeholder value indicate \"last\"\n\t\t\t\t\tlastRow = uint16(maxRow) - 1\n\t\t\t\t}\n\t\t\t\tif lastCol == 0xFF { // placeholder value indicate \"last\"\n\t\t\t\t\tlastCol = uint16(maxCol) - 1\n\t\t\t\t}\n\t\t\t\tfor rn := int(firstRow); rn <= int(lastRow); rn++ {\n\t\t\t\t\tfor cn := int(firstCol); cn <= int(lastCol); cn++ {\n\t\t\t\t\t\tif rn == int(firstRow) && cn == int(firstCol) {\n\t\t\t\t\t\t\t// should be a value there already!\n\t\t\t\t\t\t} else if cn == int(firstCol) {\n\t\t\t\t\t\t\t// first and last column MAY be the same\n\t\t\t\t\t\t\tif rn == int(lastRow) {\n\t\t\t\t\t\t\t\tres.Put(rn, cn, grate.EndRowMerged, 0)\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tres.Put(rn, cn, grate.ContinueRowMerged, 0)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else if cn == int(lastCol) {\n\t\t\t\t\t\t\t// first and last column are NOT the same\n\t\t\t\t\t\t\tres.Put(rn, cn, grate.EndColumnMerged, 0)\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tres.Put(rn, cn, grate.ContinueColumnMerged, 0)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t/*\n\t\t\t\tcase RecTypeBlank, RecTypeMulBlank:\n\t\t\t\t\t// cells default value is blank, no need for these\n\n\t\t\t\tcase RecTypeContinue:\n\t\t\t\t\t// the only situation so far is when used in RecTypeString above\n\n\t\t\t\tcase RecTypeRow, RecTypeDimensions, RecTypeEOF, RecTypeWsBool:\n\t\t\t\t\t// handled in initial pass\n\n\t\t\t\tdefault:\n\t\t\t\t\tif grate.Debug {\n\t\t\t\t\t\tlog.Println(\"    Unhandled sheet record type:\", r.RecType, ridx)\n\t\t\t\t\t}\n\t\t\t*/\n\t\t}\n\t}\n\treturn res, nil\n}\n\nvar berrLookup = map[byte]string{\n\t0x00: \"#NULL!\",\n\t0x07: \"#DIV/0!\",\n\t0x0F: \"#VALUE!\",\n\t0x17: \"#REF!\",\n\t0x1D: \"#NAME?\",\n\t0x24: \"#NUM!\",\n\t0x2A: \"#N/A\",\n\t0x2B: \"#GETTING_DATA\",\n}\n"
  },
  {
    "path": "xls/simple_test.go",
    "content": "package xls\n\nimport (\n\t\"bufio\"\n\t\"log\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/pbnjay/grate/commonxl\"\n)\n\nvar testFilePairs = [][]string{\n\t{\"../testdata/basic.xls\", \"../testdata/basic.tsv\"},\n\t{\"../testdata/testing.xls\", \"../testdata/testing.tsv\"},\n\n\t// TODO: custom formatter support\n\t//{\"../testdata/basic2.xls\", \"../testdata/basic2.tsv\"},\n\n\t// TODO: datetime and fraction formatter support\n\t//{\"../testdata/multi_test.xls\", \"../testdata/multi_test.tsv\"},\n}\n\nfunc loadTestData(fn string, ff *commonxl.Formatter) (*commonxl.Sheet, error) {\n\tf, err := os.Open(fn)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\txs := &commonxl.Sheet{\n\t\tFormatter: ff,\n\t}\n\n\trow := 0\n\ts := bufio.NewScanner(f)\n\tfor s.Scan() {\n\t\trecord := strings.Split(s.Text(), \"\\t\")\n\t\tfor i, val := range record {\n\t\t\txs.Put(row, i, val, 0)\n\t\t}\n\t\trow++\n\t}\n\treturn xs, f.Close()\n}\n\nfunc TestBasic(t *testing.T) {\n\tfor _, fnames := range testFilePairs {\n\t\tvar trueData *commonxl.Sheet\n\t\tlog.Println(\"Testing \", fnames[0])\n\n\t\twb, err := Open(fnames[0])\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tsheets, err := wb.List()\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tfirstLoad := true\n\t\tfor _, s := range sheets {\n\t\t\tsheet, err := wb.Get(s)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\txsheet := sheet.(*commonxl.Sheet)\n\t\t\tif firstLoad {\n\t\t\t\ttrueData, err = loadTestData(fnames[1], xsheet.Formatter)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\t\t\t\tfirstLoad = false\n\t\t\t}\n\n\t\t\tfor xrow, xdata := range xsheet.Rows {\n\t\t\t\tfor xcol, xval := range xdata {\n\t\t\t\t\t//t.Logf(\"at %s (%d,%d) expect '%v'\", fnames[0], xrow, xcol, trueData.Rows[xrow][xcol])\n\t\t\t\t\tif !trueData.Rows[xrow][xcol].Equal(xval) {\n\t\t\t\t\t\tt.Logf(\"mismatch at %s (%d,%d): '%v' <> '%v' expected\", fnames[0], xrow, xcol,\n\t\t\t\t\t\t\txval, trueData.Rows[xrow][xcol])\n\t\t\t\t\t\tt.Fail()\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\terr = wb.Close()\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "xls/strings.go",
    "content": "package xls\n\nimport (\n\t\"encoding/binary\"\n\t\"errors\"\n\t\"io\"\n\t\"io/ioutil\"\n\t\"unicode/utf16\"\n)\n\n// 2.5.240\nfunc decodeShortXLUnicodeString(raw []byte) (string, int, error) {\n\t// identical to decodeXLUnicodeString except for cch=8bits instead of 16\n\tcch := int(raw[0])\n\tflags := raw[1]\n\traw = raw[2:]\n\n\tcontent := make([]uint16, cch)\n\tif (flags & 0x1) == 0 {\n\t\t// 16-bit characters but only the bottom 8bits\n\t\tcontentBytes := raw[:cch]\n\t\tfor i, x := range contentBytes {\n\t\t\tcontent[i] = uint16(x)\n\t\t}\n\t\tcch += 2 // to return the offset\n\t} else {\n\t\t// 16-bit characters\n\t\tfor i := 0; i < cch; i++ {\n\t\t\tcontent[i] = binary.LittleEndian.Uint16(raw[:2])\n\t\t\traw = raw[2:]\n\t\t}\n\t\tcch += cch + 2 // to return the offset\n\t}\n\treturn string(utf16.Decode(content)), cch, nil\n}\n\n// 2.5.294\nfunc decodeXLUnicodeString(raw []byte) (string, int, error) {\n\t// identical to decodeShortXLUnicodeString except for cch=16bits instead of 8\n\tcch := int(binary.LittleEndian.Uint16(raw[:2]))\n\tflags := raw[2]\n\traw = raw[3:]\n\n\tcontent := make([]uint16, cch)\n\tif (flags & 0x1) == 0 {\n\t\t// 16-bit characters but only the bottom 8bits\n\t\tcontentBytes := raw[:cch]\n\t\tfor i, x := range contentBytes {\n\t\t\tcontent[i] = uint16(x)\n\t\t}\n\t\tcch += 3 // to return the offset\n\t} else {\n\t\t// 16-bit characters\n\t\tfor i := 0; i < cch; i++ {\n\t\t\tcontent[i] = binary.LittleEndian.Uint16(raw[:2])\n\t\t\traw = raw[2:]\n\t\t}\n\t\tcch += cch + 3 // to return the offset\n\t}\n\treturn string(utf16.Decode(content)), cch, nil\n}\n\n// 2.5.293\nfunc decodeXLUnicodeRichExtendedString(r io.Reader) (string, error) {\n\tvar cch, cRun uint16\n\tvar flags uint8\n\tvar cbExtRs int32\n\terr := binary.Read(r, binary.LittleEndian, &cch)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\terr = binary.Read(r, binary.LittleEndian, &flags)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif (flags & 0x8) != 0 {\n\t\t// rich formating data is present\n\t\terr = binary.Read(r, binary.LittleEndian, &cRun)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\tif (flags & 0x4) != 0 {\n\t\t// phonetic string data is present\n\t\terr = binary.Read(r, binary.LittleEndian, &cbExtRs)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\n\tcontent := make([]uint16, cch)\n\tif (flags & 0x1) == 0 {\n\t\t// 16-bit characters but only the bottom 8bits\n\t\tcontentBytes := make([]byte, cch)\n\t\tn, err2 := io.ReadFull(r, contentBytes)\n\t\tif n == 0 && err2 != io.ErrUnexpectedEOF {\n\t\t\terr = err2\n\t\t}\n\t\tif uint16(n) < cch {\n\t\t\tcontentBytes = contentBytes[:n]\n\t\t\tcontent = content[:n]\n\t\t}\n\n\t\tfor i, x := range contentBytes {\n\t\t\tcontent[i] = uint16(x)\n\t\t}\n\n\t} else {\n\t\t// 16-bit characters\n\t\terr = binary.Read(r, binary.LittleEndian, content)\n\t}\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\t//////\n\n\tif cRun > 0 {\n\t\t// rich formating data is present\n\t\t_, err = io.CopyN(ioutil.Discard, r, int64(cRun)*4)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\tif cbExtRs > 0 {\n\t\t// phonetic string data is present\n\t\t_, err = io.CopyN(ioutil.Discard, r, int64(cbExtRs))\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\t//////\n\n\treturn string(utf16.Decode(content)), nil\n}\n\n// read in an array of XLUnicodeRichExtendedString s\nfunc parseSST(recs []*rec) ([]string, error) {\n\t// The quirky thing about this code is that when strings cross a record\n\t// boundary, there's an intervening flags byte that MAY change the string\n\t// from an 8-bit encoding to 16-bit or vice versa.\n\n\t//totalRefs := binary.LittleEndian.Uint32(recs[0].Data[0:4])\n\tnumStrings := binary.LittleEndian.Uint32(recs[0].Data[4:8])\n\n\tall := make([]string, 0, numStrings)\n\tcurrent := make([]uint16, 32*1024)\n\n\tbuf := recs[0].Data[8:]\n\tfor i := 0; i < len(recs); {\n\t\tvar cRunBytes int\n\t\tvar flags byte\n\t\tvar cbExtRs uint32\n\n\t\tfor len(buf) > 0 {\n\t\t\tslen := binary.LittleEndian.Uint16(buf)\n\t\t\tbuf = buf[2:]\n\t\t\tflags = buf[0]\n\t\t\tbuf = buf[1:]\n\n\t\t\tif (flags & 0x8) != 0 {\n\t\t\t\t// rich formating data is present\n\t\t\t\tcRun := binary.LittleEndian.Uint16(buf)\n\t\t\t\tcRunBytes = int(cRun) * 4\n\t\t\t\tbuf = buf[2:]\n\t\t\t}\n\t\t\tif (flags & 0x4) != 0 {\n\t\t\t\t// phonetic string data is present\n\t\t\t\tcbExtRs = binary.LittleEndian.Uint32(buf)\n\t\t\t\tbuf = buf[4:]\n\t\t\t}\n\n\t\t\t///////\n\t\t\tblx := len(buf)\n\t\t\tbly := len(buf) - 5\n\t\t\tif blx > 5 {\n\t\t\t\tblx = 5\n\t\t\t}\n\t\t\tif bly < 0 {\n\t\t\t\tbly = 0\n\t\t\t}\n\n\t\t\t// this block will read the string data, but transparently\n\t\t\t// handle continuing across records\n\t\t\tif int(slen) > cap(current) {\n\t\t\t\tcurrent = make([]uint16, slen)\n\t\t\t} else {\n\t\t\t\tcurrent = current[:slen]\n\t\t\t}\n\t\t\tfor j := 0; j < int(slen); j++ {\n\t\t\t\tif len(buf) == 0 {\n\t\t\t\t\ti++\n\t\t\t\t\tif (recs[i].Data[0] & 1) == 0 {\n\t\t\t\t\t\tflags &= 0xFE\n\t\t\t\t\t} else {\n\t\t\t\t\t\tflags |= 1\n\t\t\t\t\t}\n\t\t\t\t\tbuf = recs[i].Data[1:]\n\t\t\t\t}\n\n\t\t\t\tif (flags & 1) == 0 { //8-bit\n\t\t\t\t\tcurrent[j] = uint16(buf[0])\n\t\t\t\t\tbuf = buf[1:]\n\t\t\t\t} else { //16-bit\n\t\t\t\t\tcurrent[j] = uint16(binary.LittleEndian.Uint16(buf[:2]))\n\t\t\t\t\tbuf = buf[2:]\n\t\t\t\t\tif len(buf) == 1 {\n\t\t\t\t\t\treturn nil, errors.New(\"xls: off by one\")\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\ts := string(utf16.Decode(current))\n\t\t\tall = append(all, s)\n\n\t\t\t///////\n\n\t\t\tfor cRunBytes > 0 {\n\t\t\t\tif len(buf) >= int(cRunBytes) {\n\t\t\t\t\tbuf = buf[cRunBytes:]\n\t\t\t\t\tcRunBytes = 0\n\t\t\t\t} else {\n\t\t\t\t\tcRunBytes -= len(buf)\n\t\t\t\t\ti++\n\t\t\t\t\tbuf = recs[i].Data\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfor cbExtRs > 0 {\n\t\t\t\tif len(buf) >= int(cbExtRs) {\n\t\t\t\t\tbuf = buf[cbExtRs:]\n\t\t\t\t\tcbExtRs = 0\n\t\t\t\t} else {\n\t\t\t\t\tcbExtRs -= uint32(len(buf))\n\t\t\t\t\ti++\n\t\t\t\t\tbuf = recs[i].Data\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\ti++\n\t\tif i < len(recs) {\n\t\t\tbuf = recs[i].Data\n\t\t}\n\t}\n\n\treturn all, nil\n}\n"
  },
  {
    "path": "xls/structs.go",
    "content": "package xls\n\nimport (\n\t\"fmt\"\n\t\"math\"\n)\n\ntype header struct {\n\tVersion  uint16 // An unsigned integer that specifies the BIFF version of the file. The value MUST be 0x0600.\n\tDocType  uint16 //An unsigned integer that specifies the document type of the substream of records following this record. For more information about the layout of the sub-streams in the workbook stream see File Structure.\n\tRupBuild uint16 // An unsigned integer that specifies the build identifier.\n\tRupYear  uint16 // An unsigned integer that specifies the year when this BIFF version was first created. The value MUST be 0x07CC or 0x07CD.\n\tMiscBits uint64 // lots of miscellaneous bits and flags we're not going to check\n}\n\n// 2.1.4\ntype rec struct {\n\tRecType recordType //\n\tRecSize uint16     // must be between 0 and 8224\n\tData    []byte     // len(rec.data) = rec.recsize\n}\n\ntype boundSheet struct {\n\tPosition    uint32 // A FilePointer as specified in [MS-OSHARED] section 2.2.1.5 that specifies the stream position of the start of the BOF record for the sheet.\n\tHiddenState byte   // (2 bits) An unsigned integer that specifies the hidden state of the sheet. MUST be a value from the following table:\n\tSheetType   byte   // An unsigned integer that specifies the sheet type. 00=worksheet\n\tName        string\n}\n\n///////\ntype shRow struct {\n\tRowIndex uint16 // 0-based\n\tFirstCol uint16 // 0-based\n\tLastCol  uint16 // 1-based!\n\tHeight   uint16\n\tReserved uint32\n\tFlags    uint32\n}\n\ntype shRef8 struct {\n\tFirstRow uint16 // 0-based\n\tLastRow  uint16 // 0-based\n\tFirstCol uint16 // 0-based\n\tLastCol  uint16 // 0-based\n}\ntype shMulRK struct {\n\tRowIndex uint16 // 0-based\n\tFirstCol uint16 // 0-based\n\tValues   []RkRec\n\tLastCol  uint16 // 0-based?\n}\ntype RkRec struct {\n\tIXFCell uint16\n\tValue   RKNumber\n}\n\ntype shRK struct {\n\tRowIndex uint16 // 0-based\n\tCol      uint16 // 0-based\n\tIXFCell  uint16\n\tValue    RKNumber\n}\n\ntype RKNumber uint32\n\nfunc (r RKNumber) IsInteger() bool {\n\tif (r & 1) != 0 {\n\t\t// has 2 decimals\n\t\treturn false\n\t}\n\tif (r & 2) == 0 {\n\t\t// is part of a float\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc (r RKNumber) Int() int {\n\tval := int32(r) >> 2\n\tif (r&1) == 0 && (r&2) != 0 {\n\t\treturn int(val)\n\t}\n\tif (r&1) != 0 && (r&2) != 0 {\n\t\treturn int(val / 100)\n\t}\n\treturn 0\n}\n\nfunc (r RKNumber) Float64() float64 {\n\tval := int32(r) >> 2\n\tv2 := math.Float64frombits(uint64(val) << 34)\n\n\tif (r&1) == 0 && (r&2) == 0 {\n\t\treturn v2\n\t}\n\tif (r&1) != 0 && (r&2) == 0 {\n\t\treturn v2 / 100.0\n\t}\n\treturn 0.0\n}\n\nfunc (r RKNumber) String() string {\n\tif r.IsInteger() {\n\t\treturn fmt.Sprint(r.Int())\n\t}\n\treturn fmt.Sprint(r.Float64())\n}\n"
  },
  {
    "path": "xls/xls.go",
    "content": "// Package xls implements the Microsoft Excel Binary File Format (.xls) Structure.\n// More specifically, it contains just enough detail to extract cell contents,\n// data types, and last-calculated formula values. In particular, it does NOT\n// implement formatting or formula calculations.\npackage xls\n\n// https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-xls/cd03cb5f-ca02-4934-a391-bb674cb8aa06\n\nimport (\n\t\"context\"\n\t\"encoding/binary\"\n\t\"errors\"\n\t\"io\"\n\t\"log\"\n\t\"sync\"\n\n\t\"github.com/pbnjay/grate\"\n\t\"github.com/pbnjay/grate/commonxl\"\n\t\"github.com/pbnjay/grate/xls/cfb\"\n\t\"github.com/pbnjay/grate/xls/crypto\"\n)\n\nvar _ = grate.Register(\"xls\", 1, Open)\n\n// WorkBook represents an Excel workbook containing 1 or more sheets.\ntype WorkBook struct {\n\tfilename string\n\tctx      context.Context\n\tdoc      *cfb.Document\n\n\tprot     bool\n\th        *header\n\tsheets   []*boundSheet\n\tcodepage uint16\n\tdateMode uint16\n\tstrings  []string\n\n\tpassword   string\n\tsubstreams [][]*rec\n\n\tfpos          int64\n\tpos2substream map[int64]int\n\n\tnfmt commonxl.Formatter\n\txfs  []uint16\n}\n\nfunc (b *WorkBook) IsProtected() bool {\n\treturn b.prot\n}\n\nfunc Open(filename string) (grate.Source, error) {\n\tdoc, err := cfb.Open(filename)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tb := &WorkBook{\n\t\tfilename: filename,\n\t\tdoc:      doc,\n\n\t\tpos2substream: make(map[int64]int, 16),\n\t\txfs:           make([]uint16, 0, 128),\n\t}\n\n\trdr, err := doc.Open(\"Workbook\")\n\tif err != nil {\n\t\treturn nil, grate.WrapErr(err, grate.ErrNotInFormat)\n\t}\n\traw, err := io.ReadAll(rdr)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\terr = b.loadFromStream(raw)\n\treturn b, err\n}\n\nfunc (b *WorkBook) loadFromStream(raw []byte) error {\n\treturn b.loadFromStream2(raw, false)\n}\n\nfunc (b *WorkBook) loadFromStreamWithDecryptor(raw []byte, dec crypto.Decryptor) error {\n\t// interestingly (insecurely) BIFF8 keeps Record Types and sizes in the clear,\n\t// has a few records that are not encrypted, and has 1 record type that does\n\t// not encrypt the 32bit integer position at the beginning (while encrypting\n\t// the rest). It also resets the encryption block counter every 1024 bytes\n\t// (counting all the \"skipped\" bytes described above).\n\t//\n\t// So this code streams the records through the decryption, but also records\n\t// a set of overlays applied to the final result which restore the\n\t// \"cleartext\" contents in line with the decrypted content.\n\n\tif grate.Debug {\n\t\tlog.Println(\"  Decrypting xls stream with standard RC4\")\n\t}\n\n\tpos := 0\n\tzeros := [8224]byte{}\n\n\ttype overlay struct {\n\t\tPos int\n\n\t\tRecType   recordType\n\t\tDataBytes uint16\n\t\tData      []byte // NB len() not necessarily = DataBytes\n\t}\n\treplaceBlocks := []overlay{}\n\n\tvar err error\n\tfor err == nil && len(raw[pos:]) > 4 {\n\t\to := overlay{}\n\t\to.Pos = pos\n\t\to.RecType = recordType(binary.LittleEndian.Uint16(raw[pos : pos+2]))\n\t\to.DataBytes = binary.LittleEndian.Uint16(raw[pos+2 : pos+4])\n\t\tpos += 4\n\n\t\t// copy to output and decryption stream\n\t\tbinary.Write(dec, binary.LittleEndian, o.RecType)\n\t\tbinary.Write(dec, binary.LittleEndian, o.DataBytes)\n\t\ttocopy := int(o.DataBytes)\n\n\t\tswitch o.RecType {\n\t\tcase RecTypeBOF, RecTypeFilePass, RecTypeUsrExcl, RecTypeFileLock, RecTypeInterfaceHdr, RecTypeRRDInfo, RecTypeRRDHead:\n\t\t\t// untouched data goes directly into output\n\t\t\to.Data = raw[pos : pos+int(o.DataBytes)]\n\t\t\tpos += int(o.DataBytes)\n\t\t\tdec.Write(zeros[:int(o.DataBytes)])\n\t\t\ttocopy = 0\n\n\t\tcase RecTypeBoundSheet8:\n\t\t\t// copy 32-bit position to output\n\t\t\to.Data = raw[pos : pos+4]\n\t\t\tpos += 4\n\t\t\tdec.Write(zeros[:4])\n\t\t\ttocopy -= 4\n\t\t}\n\n\t\tif tocopy > 0 {\n\t\t\t_, err = dec.Write(raw[pos : pos+tocopy])\n\t\t\tpos += tocopy\n\t\t}\n\t\treplaceBlocks = append(replaceBlocks, o)\n\t}\n\tdec.Flush()\n\n\talldata := dec.Bytes()\n\tfor _, o := range replaceBlocks {\n\t\toffs := int(o.Pos)\n\t\tbinary.LittleEndian.PutUint16(alldata[offs:], uint16(o.RecType))\n\t\tbinary.LittleEndian.PutUint16(alldata[offs+2:], uint16(o.DataBytes))\n\t\tif len(o.Data) > 0 {\n\t\t\toffs += 4\n\t\t\tcopy(alldata[offs:], o.Data)\n\t\t}\n\t}\n\n\t// recurse into the stream parser now that things are decrypted\n\treturn b.loadFromStream2(alldata, true)\n}\n\nfunc (b *WorkBook) Close() error {\n\t// return records to the pool for reuse\n\tfor i, sub := range b.substreams {\n\t\tfor _, r := range sub {\n\t\t\tr.Data = nil // allow GC\n\t\t\trecPool.Put(r)\n\t\t}\n\t\tb.substreams[i] = b.substreams[i][:0]\n\t}\n\tb.substreams = b.substreams[:0]\n\treturn nil\n}\n\nfunc (b *WorkBook) loadFromStream2(raw []byte, isDecrypted bool) error {\n\tb.h = &header{}\n\tsubstr := -1\n\tnestedBOF := 0\n\tb.pos2substream = make(map[int64]int, 10)\n\tb.fpos = 0\n\n\t// IMPORTANT: if there are any existing records, we need to return them to the pool\n\tfor i, sub := range b.substreams {\n\t\tfor _, r := range sub {\n\t\t\trecPool.Put(r)\n\t\t}\n\t\tb.substreams[i] = b.substreams[i][:0]\n\t}\n\tb.substreams = b.substreams[:0]\n\n\trawfull := raw\n\tnr, no, err := b.nextRecord(raw)\n\tfor err == nil {\n\t\traw = raw[no:]\n\t\tswitch nr.RecType {\n\t\tcase RecTypeEOF:\n\t\t\tnestedBOF--\n\t\tcase RecTypeBOF:\n\t\t\t// when substreams are nested, keep them in the same grouping\n\t\t\tif nestedBOF == 0 {\n\t\t\t\tsubstr = len(b.substreams)\n\t\t\t\tb.substreams = append(b.substreams, []*rec{})\n\t\t\t\tb.pos2substream[b.fpos] = substr\n\t\t\t}\n\t\t\tnestedBOF++\n\t\t}\n\t\tb.fpos += int64(4 + len(nr.Data))\n\n\t\t// if there's a FilePass record, the data is encrypted\n\t\tif nr.RecType == RecTypeFilePass && !isDecrypted {\n\t\t\tetype := binary.LittleEndian.Uint16(nr.Data)\n\t\t\tswitch etype {\n\t\t\tcase 1:\n\t\t\t\tdec, err := crypto.NewBasicRC4(nr.Data[2:])\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Println(\"xls: rc4 encryption failed to set up\", err)\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treturn b.loadFromStreamWithDecryptor(rawfull, dec)\n\t\t\tcase 2, 3, 4:\n\t\t\t\tlog.Println(\"need Crypto API RC4 decryptor\")\n\t\t\t\treturn errors.New(\"xls: unsupported Crypto API encryption method\")\n\t\t\tdefault:\n\t\t\t\treturn errors.New(\"xls: unsupported encryption method\")\n\t\t\t}\n\t\t}\n\n\t\tb.substreams[substr] = append(b.substreams[substr], nr)\n\t\tnr, no, err = b.nextRecord(raw)\n\t}\n\tif err == io.EOF {\n\t\terr = nil\n\t}\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor ss, records := range b.substreams {\n\t\tif grate.Debug {\n\t\t\tlog.Printf(\"  Processing substream %d/%d (%d records)\", ss, len(b.substreams), len(records))\n\t\t}\n\t\tfor i, nr := range records {\n\t\t\tif len(nr.Data) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tswitch nr.RecType {\n\t\t\tcase RecTypeSST:\n\t\t\t\t// Shared String Table is often continued across multiple records,\n\t\t\t\t// so we want to gather them all before starting to parse (some\n\t\t\t\t// strings may span the gap between records)\n\t\t\t\trecSet := []*rec{nr}\n\n\t\t\t\tlastIndex := i\n\t\t\t\tfor len(records) > (lastIndex+1) && records[lastIndex+1].RecType == RecTypeContinue {\n\t\t\t\t\tlastIndex++\n\t\t\t\t\trecSet = append(recSet, records[lastIndex])\n\t\t\t\t}\n\n\t\t\t\tb.strings, err = parseSST(recSet)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\tcase RecTypeContinue:\n\t\t\t\t// no-op (used above)\n\t\t\tcase RecTypeEOF:\n\t\t\t\t// done\n\n\t\t\tcase RecTypeBOF:\n\t\t\t\tb.h = &header{\n\t\t\t\t\tVersion:  binary.LittleEndian.Uint16(nr.Data[0:2]),\n\t\t\t\t\tDocType:  binary.LittleEndian.Uint16(nr.Data[2:4]),\n\t\t\t\t\tRupBuild: binary.LittleEndian.Uint16(nr.Data[4:6]),\n\t\t\t\t\tRupYear:  binary.LittleEndian.Uint16(nr.Data[6:8]),\n\t\t\t\t\tMiscBits: binary.LittleEndian.Uint64(nr.Data[8:16]),\n\t\t\t\t}\n\n\t\t\t\tif b.h.Version != 0x0600 {\n\t\t\t\t\treturn errors.New(\"xls: invalid file version\")\n\t\t\t\t}\n\t\t\t\tif b.h.RupYear != 0x07CC && b.h.RupYear != 0x07CD {\n\t\t\t\t\treturn errors.New(\"xls: unsupported biff version\")\n\t\t\t\t}\n\t\t\t\t/*\n\t\t\t\t\tif b.h.DocType != 0x0005 && b.h.DocType != 0x0010 {\n\t\t\t\t\t\t// we only support the workbook or worksheet substreams\n\t\t\t\t\t\tlog.Println(\"xls: unsupported document type\")\n\t\t\t\t\t\t//break\n\t\t\t\t\t}\n\t\t\t\t*/\n\n\t\t\tcase RecTypeCodePage:\n\t\t\t\t// BIFF8 is entirely UTF-16LE so this is actually ignored\n\t\t\t\tb.codepage = binary.LittleEndian.Uint16(nr.Data)\n\n\t\t\tcase RecTypeDate1904:\n\t\t\t\tb.dateMode = binary.LittleEndian.Uint16(nr.Data)\n\n\t\t\tcase RecTypeFormat:\n\t\t\t\t// Format maps a format ID to a code string\n\t\t\t\tfmtNo := binary.LittleEndian.Uint16(nr.Data)\n\t\t\t\tformatStr, _, err := decodeXLUnicodeString(nr.Data[2:])\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Println(\"fail2\", err)\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tb.nfmt.Add(fmtNo, formatStr)\n\n\t\t\tcase RecTypeXF:\n\t\t\t\t// XF records merge multiple style and format directives to one ID\n\t\t\t\t// ignore font id at nr.Data[0:2]\n\t\t\t\tfmtNo := binary.LittleEndian.Uint16(nr.Data[2:])\n\t\t\t\tb.xfs = append(b.xfs, fmtNo)\n\n\t\t\tcase RecTypeBoundSheet8:\n\t\t\t\t// Identifies the postition within the stream, visibility state,\n\t\t\t\t// and name of a worksheet\n\t\t\t\tbs := &boundSheet{}\n\t\t\t\tbs.Position = binary.LittleEndian.Uint32(nr.Data[:4])\n\t\t\t\tbs.HiddenState = nr.Data[4]\n\t\t\t\tbs.SheetType = nr.Data[5]\n\n\t\t\t\tbs.Name, _, err = decodeShortXLUnicodeString(nr.Data[6:])\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tb.sheets = append(b.sheets, bs)\n\t\t\tdefault:\n\t\t\t\tif grate.Debug && ss == 0 {\n\t\t\t\t\tlog.Println(\"    Unhandled record type:\", nr.RecType, i)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn err\n}\n\nvar recPool = sync.Pool{\n\tNew: func() interface{} {\n\t\treturn &rec{}\n\t},\n}\n\nfunc (b *WorkBook) nextRecord(raw []byte) (*rec, int, error) {\n\tif len(raw) < 4 {\n\t\treturn nil, 0, io.EOF\n\t}\n\trec := recPool.Get().(*rec)\n\n\trec.RecType = recordType(binary.LittleEndian.Uint16(raw[:2]))\n\trec.RecSize = binary.LittleEndian.Uint16(raw[2:4])\n\tif len(raw[4:]) < int(rec.RecSize) {\n\t\trecPool.Put(rec)\n\t\treturn nil, 4, io.ErrUnexpectedEOF\n\t}\n\trec.Data = raw[4 : 4+rec.RecSize]\n\treturn rec, int(4 + rec.RecSize), nil\n}\n"
  },
  {
    "path": "xlsx/comp_test.go",
    "content": "package xlsx\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestAllFiles(t *testing.T) {\n\terr := filepath.Walk(\"../testdata\", func(p string, info os.FileInfo, err error) error {\n\t\tif info.IsDir() {\n\t\t\treturn nil\n\t\t}\n\t\tif !strings.HasSuffix(info.Name(), \".xlsx\") {\n\t\t\treturn nil\n\t\t}\n\t\twb, err := Open(p)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tsheets, err := wb.List()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfor _, s := range sheets {\n\t\t\tsheet, err := wb.Get(s)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tfor sheet.Next() {\n\t\t\t\tsheet.Strings()\n\t\t\t}\n\t\t}\n\n\t\treturn wb.Close()\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n"
  },
  {
    "path": "xlsx/sheets.go",
    "content": "package xlsx\n\nimport (\n\t\"encoding/xml\"\n\t\"errors\"\n\t\"io\"\n\t\"log\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/pbnjay/grate\"\n\t\"github.com/pbnjay/grate/commonxl\"\n)\n\ntype Sheet struct {\n\td       *Document\n\trelID   string\n\tname    string\n\tdocname string\n\n\terr error\n\n\twrapped *commonxl.Sheet\n}\n\nvar errNotLoaded = errors.New(\"xlsx: sheet not loaded\")\n\nfunc (s *Sheet) parseSheet() error {\n\ts.wrapped = &commonxl.Sheet{\n\t\tFormatter: &s.d.fmt,\n\t}\n\tlinkmap := make(map[string]string)\n\tbase := filepath.Base(s.docname)\n\tsub := strings.TrimSuffix(s.docname, base)\n\trelsname := filepath.Join(sub, \"_rels\", base+\".rels\")\n\tdec, clo, err := s.d.openXML(relsname)\n\tif err == nil {\n\t\t// rels might not exist for every sheet\n\t\ttok, err := dec.RawToken()\n\t\tfor ; err == nil; tok, err = dec.RawToken() {\n\t\t\tif v, ok := tok.(xml.StartElement); ok && v.Name.Local == \"Relationship\" {\n\t\t\t\tax := getAttrs(v.Attr, \"Id\", \"Type\", \"Target\", \"TargetMode\")\n\t\t\t\tif ax[3] == \"External\" && ax[1] == \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink\" {\n\t\t\t\t\tlinkmap[ax[0]] = ax[2]\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tclo.Close()\n\t}\n\n\tdec, clo, err = s.d.openXML(s.docname)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer clo.Close()\n\n\tcurrentCellType := BlankCellType\n\tcurrentCell := \"\"\n\tvar fno uint16\n\tvar maxCol, maxRow int\n\n\ttok, err := dec.RawToken()\n\tfor ; err == nil; tok, err = dec.RawToken() {\n\t\tswitch v := tok.(type) {\n\t\tcase xml.CharData:\n\t\t\tif currentCell == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tc, r := refToIndexes(currentCell)\n\t\t\tif c >= 0 && r >= 0 {\n\t\t\t\tvar val interface{} = string(v)\n\n\t\t\t\tswitch currentCellType {\n\t\t\t\tcase BooleanCellType:\n\t\t\t\t\tif v[0] == '1' {\n\t\t\t\t\t\tval = true\n\t\t\t\t\t} else {\n\t\t\t\t\t\tval = false\n\t\t\t\t\t}\n\t\t\t\tcase DateCellType:\n\t\t\t\t\tlog.Println(\"CELL DATE\", val, fno)\n\t\t\t\tcase NumberCellType:\n\t\t\t\t\tfval, err := strconv.ParseFloat(string(v), 64)\n\t\t\t\t\tif err == nil {\n\t\t\t\t\t\tval = fval\n\t\t\t\t\t}\n\t\t\t\t\t//log.Println(\"CELL NUMBER\", val, numFormat)\n\t\t\t\tcase SharedStringCellType:\n\t\t\t\t\t//log.Println(\"CELL SHSTR\", val, currentCellType, numFormat)\n\t\t\t\t\tsi, _ := strconv.ParseInt(string(v), 10, 64)\n\t\t\t\t\tval = s.d.strings[si]\n\t\t\t\tcase BlankCellType:\n\t\t\t\t\t//log.Println(\"CELL BLANK\")\n\t\t\t\t\t// don't place any values\n\t\t\t\t\tcontinue\n\t\t\t\tcase ErrorCellType, FormulaStringCellType, InlineStringCellType:\n\t\t\t\t\t//log.Println(\"CELL ERR/FORM/INLINE\", val, currentCellType)\n\t\t\t\tdefault:\n\t\t\t\t\tlog.Println(\"CELL UNKNOWN\", val, currentCellType, fno)\n\t\t\t\t}\n\t\t\t\ts.wrapped.Put(r, c, val, fno)\n\t\t\t} else {\n\t\t\t\t//log.Println(\"FAIL row/col: \", currentCell)\n\t\t\t}\n\t\tcase xml.StartElement:\n\t\t\tswitch v.Name.Local {\n\t\t\tcase \"dimension\":\n\t\t\t\tax := getAttrs(v.Attr, \"ref\")\n\t\t\t\tif ax[0] == \"A1\" {\n\t\t\t\t\tmaxCol, maxRow = 1, 1\n\t\t\t\t\t// short-circuit empty sheet\n\t\t\t\t\ts.wrapped.Resize(1, 1)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tdims := strings.Split(ax[0], \":\")\n\t\t\t\tif len(dims) == 1 {\n\t\t\t\t\tmaxCol, maxRow = refToIndexes(dims[0])\n\t\t\t\t} else {\n\t\t\t\t\t//minCol, minRow := refToIndexes(dims[0])\n\t\t\t\t\tmaxCol, maxRow = refToIndexes(dims[1])\n\t\t\t\t}\n\t\t\t\ts.wrapped.Resize(maxRow, maxCol)\n\t\t\t\t//log.Println(\"DIMENSION:\", s.minRow, s.minCol, \">\", s.maxRow, s.maxCol)\n\t\t\tcase \"row\":\n\t\t\t\t//currentRow = ax[\"r\"] // unsigned int row index\n\t\t\t\t//log.Println(\"ROW\", currentRow)\n\t\t\tcase \"c\":\n\t\t\t\tax := getAttrs(v.Attr, \"t\", \"r\", \"s\")\n\t\t\t\tcurrentCellType = CellType(ax[0])\n\t\t\t\tif currentCellType == BlankCellType {\n\t\t\t\t\tcurrentCellType = NumberCellType\n\t\t\t\t}\n\t\t\t\tcurrentCell = ax[1] // always an A1 style reference\n\t\t\t\tstyle := ax[2]\n\t\t\t\tsid, _ := strconv.ParseInt(style, 10, 64)\n\t\t\t\tif len(s.d.xfs) > int(sid) {\n\t\t\t\t\tfno = s.d.xfs[sid]\n\t\t\t\t} else {\n\t\t\t\t\tfno = 0\n\t\t\t\t}\n\t\t\t\t//log.Println(\"CELL\", currentCell, sid, numFormat, currentCellType)\n\t\t\tcase \"v\":\n\t\t\t\t//log.Println(\"CELL VALUE\", ax)\n\n\t\t\tcase \"mergeCell\":\n\t\t\t\tax := getAttrs(v.Attr, \"ref\")\n\t\t\t\tdims := strings.Split(ax[0], \":\")\n\t\t\t\tstartCol, startRow := refToIndexes(dims[0])\n\t\t\t\tendCol, endRow := startCol, startRow\n\t\t\t\tif len(dims) > 1 {\n\t\t\t\t\tendCol, endRow = refToIndexes(dims[1])\n\t\t\t\t}\n\t\t\t\tif endRow > maxRow {\n\t\t\t\t\tendRow = maxRow\n\t\t\t\t}\n\t\t\t\tif endCol > maxCol {\n\t\t\t\t\tendCol = maxCol\n\t\t\t\t}\n\t\t\t\tfor r := startRow; r <= endRow; r++ {\n\t\t\t\t\tfor c := startCol; c <= endCol; c++ {\n\t\t\t\t\t\tif r == startRow && c == startCol {\n\t\t\t\t\t\t\t// has data already!\n\t\t\t\t\t\t} else if c == startCol {\n\t\t\t\t\t\t\t// first and last column MAY be the same\n\t\t\t\t\t\t\tif r == endRow {\n\t\t\t\t\t\t\t\ts.wrapped.Put(r, c, grate.EndRowMerged, 0)\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\ts.wrapped.Put(r, c, grate.ContinueRowMerged, 0)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else if c == endCol {\n\t\t\t\t\t\t\t// first and last column are NOT the same\n\t\t\t\t\t\t\ts.wrapped.Put(r, c, grate.EndColumnMerged, 0)\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\ts.wrapped.Put(r, c, grate.ContinueColumnMerged, 0)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\tcase \"hyperlink\":\n\t\t\t\tax := getAttrs(v.Attr, \"ref\", \"id\")\n\t\t\t\tcol, row := refToIndexes(ax[0])\n\t\t\t\tlink := linkmap[ax[1]]\n\t\t\t\ts.wrapped.Put(row, col, link, 0)\n\t\t\t\ts.wrapped.SetURL(row, col, link)\n\n\t\t\tcase \"worksheet\", \"mergeCells\", \"hyperlinks\":\n\t\t\t\t// containers\n\t\t\tcase \"f\":\n\t\t\t\t//log.Println(\"start: \", v.Name.Local, v.Attr)\n\t\t\tdefault:\n\t\t\t\tif grate.Debug {\n\t\t\t\t\tlog.Println(\"      Unhandled sheet xml tag\", v.Name.Local, v.Attr)\n\t\t\t\t}\n\t\t\t}\n\t\tcase xml.EndElement:\n\n\t\t\tswitch v.Name.Local {\n\t\t\tcase \"c\":\n\t\t\t\tcurrentCell = \"\"\n\t\t\tcase \"row\":\n\t\t\t\t//currentRow = \"\"\n\t\t\t}\n\t\tdefault:\n\t\t\tif grate.Debug {\n\t\t\t\tlog.Printf(\"      Unhandled sheet xml tokens %T %+v\", tok, tok)\n\t\t\t}\n\t\t}\n\t}\n\tif err == io.EOF {\n\t\terr = nil\n\t}\n\treturn err\n}\n"
  },
  {
    "path": "xlsx/simple_test.go",
    "content": "package xlsx\n\nimport (\n\t\"bufio\"\n\t\"log\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/pbnjay/grate/commonxl\"\n)\n\nvar testFilePairs = [][]string{\n\t{\"../testdata/basic.xlsx\", \"../testdata/basic.tsv\"},\n\n\t// TODO: custom formatter support\n\t//{\"../testdata/basic2.xlsx\", \"../testdata/basic2.tsv\"},\n\n\t// TODO: datetime and fraction formatter support\n\t//{\"../testdata/multi_test.xlsx\", \"../testdata/multi_test.tsv\"},\n}\n\nfunc loadTestData(fn string, ff *commonxl.Formatter) (*commonxl.Sheet, error) {\n\tf, err := os.Open(fn)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\txs := &commonxl.Sheet{\n\t\tFormatter: ff,\n\t}\n\n\trow := 0\n\ts := bufio.NewScanner(f)\n\tfor s.Scan() {\n\t\trecord := strings.Split(s.Text(), \"\\t\")\n\t\tfor i, val := range record {\n\t\t\txs.Put(row, i, val, 0)\n\t\t}\n\t\trow++\n\t}\n\treturn xs, f.Close()\n}\n\nfunc TestBasic(t *testing.T) {\n\tfor _, fnames := range testFilePairs {\n\t\tvar trueData *commonxl.Sheet\n\t\tlog.Println(\"Testing \", fnames[0])\n\n\t\twb, err := Open(fnames[0])\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tsheets, err := wb.List()\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tfirstLoad := true\n\t\tfor _, s := range sheets {\n\t\t\tsheet, err := wb.Get(s)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\txsheet := sheet.(*commonxl.Sheet)\n\t\t\tif firstLoad {\n\t\t\t\ttrueData, err = loadTestData(fnames[1], xsheet.Formatter)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\t\t\t\tfirstLoad = false\n\t\t\t}\n\n\t\t\tfor xrow, xdata := range xsheet.Rows {\n\t\t\t\tfor xcol, xval := range xdata {\n\t\t\t\t\t//t.Logf(\"at %s (%d,%d) expect '%v'\", fnames[0], xrow, xcol, trueData.Rows[xrow][xcol])\n\t\t\t\t\tif !trueData.Rows[xrow][xcol].Equal(xval) {\n\t\t\t\t\t\tt.Logf(\"mismatch at %s (%d,%d): '%v' <> '%v' expected\", fnames[0], xrow, xcol,\n\t\t\t\t\t\t\txval, trueData.Rows[xrow][xcol])\n\t\t\t\t\t\tt.Fail()\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\terr = wb.Close()\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "xlsx/types.go",
    "content": "package xlsx\n\nimport (\n\t\"encoding/xml\"\n\t\"strconv\"\n\t\"strings\"\n)\n\ntype CellType string\n\n// CellTypes define data type in section 18.18.11\nconst (\n\tBlankCellType         CellType = \"\"\n\tBooleanCellType       CellType = \"b\"\n\tDateCellType          CellType = \"d\"\n\tErrorCellType         CellType = \"e\"\n\tNumberCellType        CellType = \"n\"\n\tSharedStringCellType  CellType = \"s\"\n\tFormulaStringCellType CellType = \"str\"\n\tInlineStringCellType  CellType = \"inlineStr\"\n)\n\ntype staticCellType rune\n\nconst (\n\tstaticBlank staticCellType = 0\n\n\t// marks a continuation column within a merged cell.\n\tcontinueColumnMerged staticCellType = '→'\n\t// marks the last column of a merged cell.\n\tendColumnMerged staticCellType = '⇥'\n\n\t// marks a continuation row within a merged cell.\n\tcontinueRowMerged staticCellType = '↓'\n\t// marks the last row of a merged cell.\n\tendRowMerged staticCellType = '⤓'\n)\n\nfunc (s staticCellType) String() string {\n\tif s == 0 {\n\t\treturn \"\"\n\t}\n\treturn string([]rune{rune(s)})\n}\n\n// returns the 0-based index of the column string:\n//    \"A\"=0, \"B\"=1, \"AA\"=26, \"BB\"=53\nfunc col2int(col string) int {\n\tidx := 0\n\tfor _, c := range col {\n\t\tidx *= 26\n\t\tidx += int(c - '@')\n\t}\n\treturn idx - 1\n}\n\nfunc refToIndexes(r string) (column, row int) {\n\tif len(r) < 2 {\n\t\treturn -1, -1\n\t}\n\ti1 := strings.IndexAny(r, \"0123456789\")\n\tif i1 <= 0 {\n\t\treturn -1, -1\n\t}\n\n\t// A1 Reference mode\n\tcol1 := r[:i1]\n\ti2 := strings.IndexByte(r[i1:], 'C')\n\tif i2 == -1 {\n\t\trn, _ := strconv.ParseInt(r[i1:], 10, 64)\n\t\treturn col2int(col1), int(rn) - 1\n\t}\n\n\t// R1C1 Reference Mode\n\tcol1 = r[i1:i2]\n\trow1 := r[i2+1:]\n\tcn, _ := strconv.ParseInt(col1, 10, 64)\n\trn, _ := strconv.ParseInt(row1, 10, 64)\n\treturn int(cn), int(rn) - 1\n}\n\nfunc getAttrs(attrs []xml.Attr, keys ...string) []string {\n\tres := make([]string, len(keys))\n\tfor _, a := range attrs {\n\t\tfor i, k := range keys {\n\t\t\tif a.Name.Local == k {\n\t\t\t\tres[i] = a.Value\n\t\t\t}\n\t\t}\n\t}\n\treturn res\n}\n"
  },
  {
    "path": "xlsx/workbook.go",
    "content": "package xlsx\n\nimport (\n\t\"encoding/xml\"\n\t\"errors\"\n\t\"io\"\n\t\"log\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/pbnjay/grate\"\n)\n\nfunc (d *Document) parseRels(dec *xml.Decoder, basedir string) error {\n\ttok, err := dec.RawToken()\n\tfor ; err == nil; tok, err = dec.RawToken() {\n\t\tswitch v := tok.(type) {\n\t\tcase xml.StartElement:\n\t\t\tswitch v.Name.Local {\n\t\t\tcase \"Relationships\":\n\t\t\t\t// container\n\t\t\tcase \"Relationship\":\n\t\t\t\tvals := make(map[string]string, 5)\n\t\t\t\tfor _, a := range v.Attr {\n\t\t\t\t\tvals[a.Name.Local] = a.Value\n\t\t\t\t}\n\t\t\t\tif _, ok := d.rels[vals[\"Type\"]]; !ok {\n\t\t\t\t\td.rels[vals[\"Type\"]] = make(map[string]string)\n\t\t\t\t}\n\t\t\t\tif strings.HasPrefix(vals[\"Target\"], \"/\") {\n\t\t\t\t\t// handle malformed \"absolute\" paths cleanly\n\t\t\t\t\td.rels[vals[\"Type\"]][vals[\"Id\"]] = vals[\"Target\"][1:]\n\t\t\t\t} else {\n\t\t\t\t\td.rels[vals[\"Type\"]][vals[\"Id\"]] = filepath.Join(basedir, vals[\"Target\"])\n\t\t\t\t}\n\t\t\t\tif vals[\"Type\"] == \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument\" {\n\t\t\t\t\td.primaryDoc = vals[\"Target\"]\n\t\t\t\t}\n\t\t\tdefault:\n\t\t\t\tif grate.Debug {\n\t\t\t\t\tlog.Println(\"      Unhandled relationship xml tag\", v.Name.Local, v.Attr)\n\t\t\t\t}\n\t\t\t}\n\t\tcase xml.EndElement:\n\t\t\t// not needed\n\t\tdefault:\n\t\t\tif grate.Debug {\n\t\t\t\tlog.Printf(\"      Unhandled relationship xml tokens %T %+v\", tok, tok)\n\t\t\t}\n\t\t}\n\t}\n\tif err == io.EOF {\n\t\terr = nil\n\t}\n\treturn err\n}\n\nfunc (d *Document) parseWorkbook(dec *xml.Decoder) error {\n\ttok, err := dec.RawToken()\n\tfor ; err == nil; tok, err = dec.RawToken() {\n\t\tswitch v := tok.(type) {\n\t\tcase xml.StartElement:\n\t\t\tswitch v.Name.Local {\n\t\t\tcase \"sheet\":\n\t\t\t\tvals := make(map[string]string, 5)\n\t\t\t\tfor _, a := range v.Attr {\n\t\t\t\t\tvals[a.Name.Local] = a.Value\n\t\t\t\t}\n\t\t\t\tsheetID, ok1 := vals[\"id\"]\n\t\t\t\tsheetName, ok2 := vals[\"name\"]\n\t\t\t\tif !ok1 || !ok2 {\n\t\t\t\t\treturn errors.New(\"xlsx: invalid sheet definition\")\n\t\t\t\t}\n\t\t\t\ts := &Sheet{\n\t\t\t\t\td:       d,\n\t\t\t\t\trelID:   sheetID,\n\t\t\t\t\tname:    sheetName,\n\t\t\t\t\tdocname: d.rels[\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet\"][sheetID],\n\t\t\t\t\terr:     errNotLoaded,\n\t\t\t\t}\n\t\t\t\td.sheets = append(d.sheets, s)\n\t\t\tcase \"workbook\", \"sheets\":\n\t\t\t\t// containers\n\t\t\tdefault:\n\t\t\t\tif grate.Debug {\n\t\t\t\t\tlog.Println(\"      Unhandled workbook xml tag\", v.Name.Local, v.Attr)\n\t\t\t\t}\n\t\t\t}\n\t\tcase xml.EndElement:\n\t\t\t// not needed\n\t\tdefault:\n\t\t\tif grate.Debug {\n\t\t\t\tlog.Printf(\"      Unhandled workbook xml tokens %T %+v\", tok, tok)\n\t\t\t}\n\t\t}\n\t}\n\tif err == io.EOF {\n\t\terr = nil\n\t}\n\treturn err\n}\n\nfunc (d *Document) parseStyles(dec *xml.Decoder) error {\n\tbaseNumFormats := []string{}\n\td.xfs = d.xfs[:0]\n\n\tsection := 0\n\ttok, err := dec.RawToken()\n\tfor ; err == nil; tok, err = dec.RawToken() {\n\t\tswitch v := tok.(type) {\n\t\tcase xml.StartElement:\n\t\t\tswitch v.Name.Local {\n\t\t\tcase \"styleSheet\":\n\t\t\t\t// container\n\t\t\tcase \"numFmt\":\n\t\t\t\tax := getAttrs(v.Attr, \"numFmtId\", \"formatCode\")\n\t\t\t\tfmtNo, _ := strconv.ParseInt(ax[0], 10, 16)\n\t\t\t\td.fmt.Add(uint16(fmtNo), ax[1])\n\n\t\t\tcase \"cellStyleXfs\":\n\t\t\t\tsection = 1\n\t\t\tcase \"cellXfs\":\n\t\t\t\tsection = 2\n\t\t\t\tax := getAttrs(v.Attr, \"count\")\n\t\t\t\tn, _ := strconv.ParseInt(ax[0], 10, 64)\n\t\t\t\td.xfs = make([]uint16, 0, n)\n\n\t\t\tcase \"xf\":\n\t\t\t\tax := getAttrs(v.Attr, \"numFmtId\", \"applyNumberFormat\", \"xfId\")\n\t\t\t\tif section == 1 {\n\t\t\t\t\t// load base styles, but only save number format\n\t\t\t\t\tif ax[1] == \"0\" {\n\t\t\t\t\t\tbaseNumFormats = append(baseNumFormats, \"0\")\n\t\t\t\t\t} else {\n\t\t\t\t\t\tbaseNumFormats = append(baseNumFormats, ax[0])\n\t\t\t\t\t}\n\t\t\t\t} else if section == 2 {\n\t\t\t\t\t// actual referencable cell styles\n\t\t\t\t\t// 1) get base style so we can inherit format properly\n\t\t\t\t\tbaseID, _ := strconv.ParseInt(ax[2], 10, 64)\n\t\t\t\t\tnumFmtID := \"0\"\n\t\t\t\t\tif len(baseNumFormats) > int(baseID) {\n\t\t\t\t\t\tnumFmtID = baseNumFormats[baseID]\n\t\t\t\t\t}\n\n\t\t\t\t\t// 2) check if this XF overrides the base format\n\t\t\t\t\tif ax[1] == \"0\" {\n\t\t\t\t\t\t// remove the format (if it was inherited)\n\t\t\t\t\t\tnumFmtID = \"0\"\n\t\t\t\t\t} else {\n\t\t\t\t\t\tnumFmtID = ax[0]\n\t\t\t\t\t}\n\n\t\t\t\t\tnfid, _ := strconv.ParseInt(numFmtID, 10, 16)\n\t\t\t\t\td.xfs = append(d.xfs, uint16(nfid))\n\t\t\t\t} else {\n\t\t\t\t\tpanic(\"wheres is this xf??\")\n\t\t\t\t}\n\t\t\tdefault:\n\t\t\t\tif grate.Debug {\n\t\t\t\t\tlog.Println(\"  Unhandled style xml tag\", v.Name.Local, v.Attr)\n\t\t\t\t}\n\t\t\t}\n\t\tcase xml.EndElement:\n\t\t\tswitch v.Name.Local {\n\t\t\tcase \"cellStyleXfs\":\n\t\t\t\tsection = 0\n\t\t\tcase \"cellXfs\":\n\t\t\t\tsection = 0\n\t\t\t}\n\t\tdefault:\n\t\t\tif grate.Debug {\n\t\t\t\tlog.Printf(\"      Unhandled style xml tokens %T %+v\", tok, tok)\n\t\t\t}\n\t\t}\n\t}\n\tif err == io.EOF {\n\t\terr = nil\n\t}\n\treturn err\n}\n\nfunc (d *Document) parseSharedStrings(dec *xml.Decoder) error {\n\tval := \"\"\n\ttok, err := dec.RawToken()\n\tfor ; err == nil; tok, err = dec.RawToken() {\n\t\tswitch v := tok.(type) {\n\t\tcase xml.CharData:\n\t\t\tval += string(v)\n\t\tcase xml.StartElement:\n\t\t\tswitch v.Name.Local {\n\t\t\tcase \"si\":\n\t\t\t\tval = \"\"\n\t\t\tcase \"t\":\n\t\t\t\t// no attributes to parse, we only want the CharData ...\n\t\t\tcase \"sst\":\n\t\t\t\t// main container\n\t\t\tdefault:\n\t\t\t\tif grate.Debug {\n\t\t\t\t\tlog.Println(\"  Unhandled SST xml tag\", v.Name.Local, v.Attr)\n\t\t\t\t}\n\t\t\t}\n\t\tcase xml.EndElement:\n\t\t\tif v.Name.Local == \"si\" {\n\t\t\t\td.strings = append(d.strings, val)\n\t\t\t\tcontinue\n\t\t\t}\n\t\tdefault:\n\t\t\tif grate.Debug {\n\t\t\t\tlog.Printf(\"    Unhandled SST xml token %T %+v\", tok, tok)\n\t\t\t}\n\t\t}\n\t}\n\tif err == io.EOF {\n\t\terr = nil\n\t}\n\treturn err\n}\n"
  },
  {
    "path": "xlsx/xlsx.go",
    "content": "package xlsx\n\nimport (\n\t\"archive/zip\"\n\t\"encoding/xml\"\n\t\"errors\"\n\t\"io\"\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/pbnjay/grate\"\n\t\"github.com/pbnjay/grate/commonxl\"\n)\n\nvar _ = grate.Register(\"xlsx\", 5, Open)\n\n// Document contains an Office Open XML document.\ntype Document struct {\n\tfilename   string\n\tf          *os.File\n\tr          *zip.Reader\n\tprimaryDoc string\n\n\t// type => id => filename\n\trels    map[string]map[string]string\n\tsheets  []*Sheet\n\tstrings []string\n\txfs     []uint16\n\tfmt     commonxl.Formatter\n}\n\nfunc (d *Document) Close() error {\n\td.xfs = d.xfs[:0]\n\td.xfs = nil\n\td.strings = d.strings[:0]\n\td.strings = nil\n\td.sheets = d.sheets[:0]\n\td.sheets = nil\n\treturn d.f.Close()\n}\n\nfunc Open(filename string) (grate.Source, error) {\n\tf, err := os.Open(filename)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tinfo, err := f.Stat()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tz, err := zip.NewReader(f, info.Size())\n\tif err != nil {\n\t\treturn nil, grate.WrapErr(err, grate.ErrNotInFormat)\n\t}\n\td := &Document{\n\t\tfilename: filename,\n\t\tf:        f,\n\t\tr:        z,\n\t}\n\n\td.rels = make(map[string]map[string]string, 4)\n\n\t// parse the primary relationships\n\tdec, c, err := d.openXML(\"_rels/.rels\")\n\tif err != nil {\n\t\treturn nil, grate.WrapErr(err, grate.ErrNotInFormat)\n\t}\n\terr = d.parseRels(dec, \"\")\n\tc.Close()\n\tif err != nil {\n\t\treturn nil, grate.WrapErr(err, grate.ErrNotInFormat)\n\t}\n\tif d.primaryDoc == \"\" {\n\t\treturn nil, errors.New(\"xlsx: invalid document\")\n\t}\n\n\t// parse the secondary relationships to primary doc\n\tbase := filepath.Base(d.primaryDoc)\n\tsub := strings.TrimSuffix(d.primaryDoc, base)\n\trelfn := filepath.Join(sub, \"_rels\", base+\".rels\")\n\tdec, c, err = d.openXML(relfn)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\terr = d.parseRels(dec, sub)\n\tc.Close()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// parse the workbook structure\n\tdec, c, err = d.openXML(d.primaryDoc)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\terr = d.parseWorkbook(dec)\n\tc.Close()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tstyn := d.rels[\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles\"]\n\tfor _, sst := range styn {\n\t\t// parse the shared string table\n\t\tdec, c, err = d.openXML(sst)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\terr = d.parseStyles(dec)\n\t\tc.Close()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tssn := d.rels[\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings\"]\n\tfor _, sst := range ssn {\n\t\t// parse the shared string table\n\t\tdec, c, err = d.openXML(sst)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\terr = d.parseSharedStrings(dec)\n\t\tc.Close()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn d, nil\n}\n\nfunc (d *Document) openXML(name string) (*xml.Decoder, io.Closer, error) {\n\tif grate.Debug {\n\t\tlog.Println(\"    openXML\", name)\n\t}\n\tfor _, zf := range d.r.File {\n\t\tif zf.Name == name {\n\t\t\tzfr, err := zf.Open()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, nil, err\n\t\t\t}\n\t\t\tdec := xml.NewDecoder(zfr)\n\t\t\treturn dec, zfr, nil\n\t\t}\n\t}\n\treturn nil, nil, io.EOF\n}\n\nfunc (d *Document) List() ([]string, error) {\n\tres := make([]string, 0, len(d.sheets))\n\tfor _, s := range d.sheets {\n\t\tres = append(res, s.name)\n\t}\n\treturn res, nil\n}\n\nfunc (d *Document) Get(sheetName string) (grate.Collection, error) {\n\tfor _, s := range d.sheets {\n\t\tif s.name == sheetName {\n\t\t\tif s.err == errNotLoaded {\n\t\t\t\ts.err = s.parseSheet()\n\t\t\t}\n\t\t\treturn s.wrapped, s.err\n\t\t}\n\t}\n\treturn nil, errors.New(\"xlsx: sheet not found\")\n}\n"
  }
]