Full Code of pbnjay/grate for AI

main 3f8e65d74a14 cached
40 files
174.9 KB
59.3k tokens
577 symbols
1 requests
Download .txt
Repository: pbnjay/grate
Branch: main
Commit: 3f8e65d74a14
Files: 40
Total size: 174.9 KB

Directory structure:
gitextract_81jad5jf/

├── .github/
│   └── workflows/
│       └── go.yml
├── .gitignore
├── LICENSE
├── README.md
├── cmd/
│   ├── grate2tsv/
│   │   └── main.go
│   └── grater/
│       └── main.go
├── commonxl/
│   ├── cell.go
│   ├── dates.go
│   ├── fmt.go
│   ├── fmt_test.go
│   ├── formats.go
│   ├── frac_test.go
│   ├── numbers.go
│   └── sheet.go
├── errs.go
├── go.mod
├── grate.go
├── simple/
│   ├── csv.go
│   ├── simple.go
│   └── tsv.go
├── xls/
│   ├── cfb/
│   │   ├── cfb.go
│   │   ├── interface.go
│   │   ├── simple_test.go
│   │   └── slicereader.go
│   ├── comp_test.go
│   ├── crypto/
│   │   ├── crypto.go
│   │   └── rc4.go
│   ├── hyperlinks.go
│   ├── records.go
│   ├── sheets.go
│   ├── simple_test.go
│   ├── strings.go
│   ├── structs.go
│   └── xls.go
└── xlsx/
    ├── comp_test.go
    ├── sheets.go
    ├── simple_test.go
    ├── types.go
    ├── workbook.go
    └── xlsx.go

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/go.yml
================================================
name: Go

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:

  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2

    - name: Set up Go
      uses: actions/setup-go@v2
      with:
        go-version: 1.17

    - name: Build
      run: go build -v ./...

    - name: Test XLS
      run: go test -v ./xls

    - name: Test XLSX
      run: go test -v ./xlsx

    - name: Test CommonXL
      run: go test -v ./commonxl


================================================
FILE: .gitignore
================================================
cmd/grate2tsv/results
testdata

*.pprof
*.pdf


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2021 Jeremy Jay

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

================================================
FILE: README.md
================================================
# grate

A Go native tabular data extraction package. Currently supports `.xls`, `.xlsx`, `.csv`, `.tsv` formats.

# Why?

Grate 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.

There 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.

# Usage

Grate provides a simple standard interface for all supported filetypes, allowing access to both named worksheets in spreadsheets and single tables in plaintext formats.

```go
package main

import (
    "fmt"
    "os"
    "strings"

    "github.com/pbnjay/grate"
    _ "github.com/pbnjay/grate/simple" // tsv and csv support
    _ "github.com/pbnjay/grate/xls"
    _ "github.com/pbnjay/grate/xlsx"
)

func main() {
    wb, _ := grate.Open(os.Args[1])  // open the file
    sheets, _ := wb.List()           // list available sheets
    for _, s := range sheets {       // enumerate each sheet name
        sheet, _ := wb.Get(s)        // open the sheet
        for sheet.Next() {           // enumerate each row of data
            row := sheet.Strings()   // get the row's content as []string
            fmt.Println(strings.Join(row, "\t"))
        }
    }
    wb.Close()
}
```

# License

All source code is licensed under the [MIT License](https://raw.github.com/pbnjay/grate/master/LICENSE).


================================================
FILE: cmd/grate2tsv/main.go
================================================
// Command grate2tsv is a highly parallel tabular data extraction tool. It's
// probably not necessary in your situation, but is included here since it
// is a good stress test of the codebase.
//
// Files on the command line will be parsed and extracted to the "results"
// subdirectory under a heirarchical arrangement (to make our filesystems
// more responsive), and a "results.txt" file will be created logging basic
// information and errors for each file.
package main

import (
	"bufio"
	"crypto/md5"
	"flag"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"os"
	"path/filepath"
	"regexp"
	"runtime"
	"runtime/pprof"
	"strings"
	"sync"
	"time"

	"github.com/pbnjay/grate"
	_ "github.com/pbnjay/grate/simple"
	_ "github.com/pbnjay/grate/xls"
	_ "github.com/pbnjay/grate/xlsx"
)

var (
	logfile        = flag.String("l", "", "save processing logs to `filename.txt`")
	pretend        = flag.Bool("p", false, "pretend to output .tsv")
	infoFile       = flag.String("i", "results.txt", "`filename` to record stats about the process")
	removeNewlines = flag.Bool("r", true, "remove embedded tabs, newlines, and condense spaces in cell contents")
	trimSpaces     = flag.Bool("w", true, "trim whitespace from cell contents")
	skipBlanks     = flag.Bool("b", true, "discard blank rows from the output")
	cpuprofile     = flag.String("cpuprofile", "", "write cpu profile to file")
	memprofile     = flag.String("memprofile", "", "write memory profile to file")

	timeFormat = "2006-01-02 15:04:05"
	fstats     *os.File

	procWG  sync.WaitGroup
	cleanup = make(chan *output, 100)
	outpool = sync.Pool{New: func() interface{} {
		return &output{}
	}}
)

type output struct {
	f *os.File
	b *bufio.Writer
}

func main() {
	flag.Parse()

	if *memprofile != "" {
		f, err := os.Create(*memprofile)
		if err != nil {
			log.Fatal(err)
		}
		defer func() {
			runtime.GC()
			pprof.WriteHeapProfile(f)
			f.Close()
		}()
	}

	if *cpuprofile != "" {
		f, err := os.Create(*cpuprofile)
		if err != nil {
			log.Fatal(err)
		}
		pprof.StartCPUProfile(f)
		defer pprof.StopCPUProfile()
	}

	if *logfile != "" {
		fo, err := os.Create(*logfile)
		if err != nil {
			log.Fatal(err)
		}
		defer fo.Close()
		log.SetOutput(fo)
	}

	done := make(chan int)
	go func() {
		for x := range cleanup {
			x.b.Flush()
			x.f.Close()
			outpool.Put(x)
		}
		done <- 1
	}()

	var err error
	fstats, err = os.OpenFile(*infoFile, os.O_CREATE|os.O_RDWR, 0644)
	if err != nil {
		log.Fatal(err)
	}
	defer fstats.Close()
	pos, err := fstats.Seek(0, io.SeekEnd)
	if err != nil {
		log.Fatal(err)
	}
	if pos == 0 {
		fmt.Fprintf(fstats, "time\tfilename\tsheet\trows\tcolumns\terrors\n")
	}

	filenameChan := make(chan string)

	// fan out to 1/2 of CPU cores
	// (e.g. each file-processor can use 2 cpus)
	outMu := &sync.Mutex{}
	nparallel := runtime.NumCPU() / 2
	procWG.Add(nparallel)
	for i := 0; i < nparallel; i++ {
		go runProcessor(filenameChan, outMu)
	}
	for _, fn := range flag.Args() {
		filenameChan <- fn
	}

	close(filenameChan)
	procWG.Wait()
	close(cleanup)
	<-done
}

func runProcessor(from chan string, mu *sync.Mutex) {
	for fn := range from {
		nowFmt := time.Now().Format(timeFormat)
		results, err := processFile(fn)
		mu.Lock()
		if err != nil {
			// returned errors are fatal
			fmt.Fprintf(fstats, "%s\t%s\t-\t-\t-\t%s\n", nowFmt, fn, err.Error())
			mu.Unlock()
			continue
		}

		for _, res := range results {
			e := "-"
			if res.Err != nil {
				e = res.Err.Error()
			}
			fmt.Fprintf(fstats, "%s\t%s\t%s\t%d\t%d\t%s\n", nowFmt, res.Filename, res.SheetName,
				res.NumRows, res.NumCols, e)
		}
		mu.Unlock()
	}
	procWG.Done()
}

var (
	sanitize = regexp.MustCompile("[^a-zA-Z0-9]+")
	newlines = regexp.MustCompile("[ \n\r\t]+")
)

type stats struct {
	Filename  string
	Hash      string
	SheetName string
	NumRows   int
	NumCols   int
	Err       error
}

func processFile(fn string) ([]stats, error) {
	//log.Printf("Opening file '%s' ...", fn)
	wb, err := grate.Open(fn)
	if err != nil {
		return nil, err
	}
	defer wb.Close()

	results := []stats{}

	ext := filepath.Ext(fn)
	fn2 := filepath.Base(strings.TrimSuffix(fn, ext))
	subparts := fmt.Sprintf("%x", md5.Sum([]byte(fn2)))
	subdir := filepath.Join("results", subparts[:2], subparts[2:4])
	os.MkdirAll(subdir, 0755)
	log.Printf(subparts[:8]+"  Processing file '%s'", fn2)

	sheets, err := wb.List()
	if err != nil {
		return nil, err
	}
	for _, s := range sheets {
		ps := stats{
			Filename:  fn,
			Hash:      subparts[:8],
			SheetName: s,
		}
		log.Printf(subparts[:8]+"  Opening Sheet '%s'...", s)
		sheet, err := wb.Get(s)
		if err != nil {
			ps.Err = err
			results = append(results, ps)
			continue
		}
		if sheet.IsEmpty() {
			log.Println(subparts[:8] + "    Empty sheet. Skipping.")
			results = append(results, ps)
			continue
		}
		s2 := sanitize.ReplaceAllString(s, "_")
		if s == fn {
			s2 = "main"
		}
		var ox *output
		var w io.Writer = ioutil.Discard
		if !*pretend {
			f, err := os.Create(subdir + "/" + fn2 + "." + s2 + ".tsv")
			if err != nil {
				return nil, err
			}
			ox = outpool.Get().(*output)
			ox.f = f
			ox.b = bufio.NewWriter(f)
			w = ox.b
		}

		for sheet.Next() {
			row := sheet.Strings()
			nonblank := false
			for i, x := range row {
				if *removeNewlines {
					x = newlines.ReplaceAllString(x, " ")
				}
				if *trimSpaces {
					x = strings.TrimSpace(x)
					row[i] = x
				}
				if x != "" {
					nonblank = true
					if ps.NumCols < i {
						ps.NumCols = i
					}
				}
			}
			if nonblank || !*skipBlanks {
				for i, v := range row {
					if i != 0 {
						w.Write([]byte{'\t'})
					}
					w.Write([]byte(v))
				}
				w.Write([]byte{'\n'})
				ps.NumRows++
			}
		}
		results = append(results, ps)
		if ox != nil {
			cleanup <- ox
		}
	}
	return results, nil
}


================================================
FILE: cmd/grater/main.go
================================================
// Command grater extracts contents of the tabular files to stdout.
package main

import (
	"flag"
	"fmt"
	"os"
	"strings"

	"github.com/pbnjay/grate"
	_ "github.com/pbnjay/grate/simple" // tsv and csv support
	_ "github.com/pbnjay/grate/xls"
	_ "github.com/pbnjay/grate/xlsx"
)

func main() {
	flagDebug := flag.Bool("v", false, "debug log")
	flag.Parse()
	if flag.NArg() < 1 {
		fmt.Fprintf(os.Stderr, "USAGE: %s [file1.xls file2.xlsx file3.tsv ...]\n", os.Args[0])
		fmt.Fprintf(os.Stderr, "       Extracts contents of the tabular files to stdout\n")
		os.Exit(1)
	}
	grate.Debug = *flagDebug
	for _, fn := range flag.Args() {
		wb, err := grate.Open(fn)
		if err != nil {
			fmt.Fprintln(os.Stderr, err)
			continue
		}

		sheets, err := wb.List()
		if err != nil {
			wb.Close()
			fmt.Fprintln(os.Stderr, err)
			continue
		}

		for _, s := range sheets {
			sheet, err := wb.Get(s)
			if err != nil {
				fmt.Fprintln(os.Stderr, err)
				continue
			}

			for sheet.Next() {
				if *flagDebug {
					dtypes := sheet.Types()
					fmt.Println(strings.Join(dtypes, "\t"))
				}
				row := sheet.Strings()
				fmt.Println(strings.Join(row, "\t"))
			}
		}
		wb.Close()
	}
}


================================================
FILE: commonxl/cell.go
================================================
package commonxl

import (
	"fmt"
	"math"
	"net/url"
	"strconv"
	"time"
	"unicode/utf16"
)

// CellType annotates the type of data extracted in the cell.
type CellType uint16

// CellType annotations for various cell value types.
const (
	BlankCell CellType = iota
	IntegerCell
	FloatCell
	StringCell
	BooleanCell
	DateCell

	HyperlinkStringCell // internal type to separate URLs
	StaticCell          // placeholder, internal use only
)

// String returns a string description of the cell data type.
func (c CellType) String() string {
	switch c {
	case BlankCell:
		return "blank"
	case IntegerCell:
		return "integer"
	case FloatCell:
		return "float"
	case BooleanCell:
		return "boolean"
	case DateCell:
		return "date"
	case HyperlinkStringCell:
		return "hyperlink"
	case StaticCell:
		return "static"
	default: // StringCell, StaticCell
		return "string"
	}
}

// Cell represents a single cell value.
type Cell []interface{}

// internally, it is a slice sized 2 or 3
//   [Value, CellType] or [Value, CellType, FormatNumber]
// where FormatNumber is a uint16 if not 0

// Value returns the contents as a generic interface{}.
func (c Cell) Value() interface{} {
	if len(c) == 0 {
		return ""
	}
	return c[0]
}

// SetURL adds a URL hyperlink to the cell.
func (c *Cell) SetURL(link string) {
	(*c)[1] = HyperlinkStringCell
	if len(*c) == 2 {
		*c = append(*c, uint16(0), link)
	} else { // len = 3 already
		*c = append(*c, link)
	}
}

// URL returns the parsed URL when a cell contains a hyperlink.
func (c Cell) URL() (*url.URL, bool) {
	if c.Type() == HyperlinkStringCell && len(c) >= 4 {
		u, err := url.Parse(c[3].(string))
		return u, err == nil
	}
	return nil, false
}

// Type returns the CellType of the value.
func (c Cell) Type() CellType {
	if len(c) < 2 {
		return BlankCell
	}
	return c[1].(CellType)
}

// FormatNo returns the NumberFormat used for display.
func (c Cell) FormatNo() uint16 {
	if len(c) == 3 {
		return c[2].(uint16)
	}
	return 0
}

// Clone returns the new copy of this Cell.
func (c Cell) Clone() Cell {
	c2 := make([]interface{}, len(c))
	for i, x := range c {
		c2[i] = x
	}
	return c2
}

///////

var boolStrings = map[string]bool{
	"yes": true, "true": true, "t": true, "y": true, "1": true, "on": true,
	"no": false, "false": false, "f": false, "n": false, "0": false, "off": false,
	"YES": true, "TRUE": true, "T": true, "Y": true, "1.0": true, "ON": true,
	"NO": false, "FALSE": false, "F": false, "N": false, "0.0": false, "OFF": false,
}

// NewCellWithType creates a new cell value with the given type, coercing as necessary.
func NewCellWithType(value interface{}, t CellType, f *Formatter) Cell {
	c := NewCell(value)
	if c[1] == t {
		// fast path if it was already typed correctly
		return c
	}

	if c[1] == BooleanCell {
		if t == IntegerCell {
			if c[0].(bool) {
				c[0] = int64(1)
			} else {
				c[0] = int64(0)
			}
			c[1] = IntegerCell
		} else if t == FloatCell {
			if c[0].(bool) {
				c[0] = float64(1.0)
			} else {
				c[0] = float64(0.0)
			}
			c[1] = FloatCell
		} else if t == StringCell {
			if c[0].(bool) {
				c[0] = "TRUE"
			} else {
				c[0] = "FALSE"
			}
			c[1] = FloatCell
		}
	}

	if c[1] == FloatCell {
		if t == IntegerCell {
			c[0] = int64(c[0].(float64))
			c[1] = IntegerCell
		} else if t == BooleanCell {
			c[0] = c[0].(float64) != 0.0
			c[1] = BooleanCell
		}
	}
	if c[1] == IntegerCell {
		if t == FloatCell {
			c[0] = float64(c[0].(int64))
			c[1] = FloatCell
		} else if t == BooleanCell {
			c[0] = c[0].(int64) != 0
			c[1] = BooleanCell
		}
	}
	if c[1] == StringCell {
		if t == IntegerCell {
			x, _ := strconv.ParseInt(c[0].(string), 10, 64)
			c[0] = x
			c[1] = IntegerCell
		} else if t == FloatCell {
			x, _ := strconv.ParseFloat(c[0].(string), 64)
			c[0] = x
			c[1] = FloatCell
		} else if t == BooleanCell {
			c[0] = boolStrings[c[0].(string)]
			c[1] = BooleanCell
		}
	}
	if t == StringCell {
		c[0] = fmt.Sprint(c[0])
		c[1] = StringCell
	}
	if t == DateCell {
		if c[1] == FloatCell {
			c[0] = f.ConvertToDate(c[0].(float64))
		} else if c[1] == IntegerCell {
			c[0] = f.ConvertToDate(float64(c[0].(int64)))
		}
		c[1] = DateCell
	}
	return c
}

// NewCell creates a new cell value from any builtin type.
func NewCell(value interface{}) Cell {
	c := make([]interface{}, 2)
	switch v := value.(type) {
	case bool:
		c[0] = v
		c[1] = BooleanCell
	case int:
		c[0] = int64(v)
		c[1] = IntegerCell
	case int8:
		c[0] = int64(v)
		c[1] = IntegerCell
	case int16:
		c[0] = int64(v)
		c[1] = IntegerCell
	case int32:
		c[0] = int64(v)
		c[1] = IntegerCell
	case int64:
		c[0] = int64(v)
		c[1] = IntegerCell
	case uint8:
		c[0] = int64(v)
		c[1] = IntegerCell
	case uint16:
		c[0] = int64(v)
		c[1] = IntegerCell
	case uint32:
		c[0] = int64(v)
		c[1] = IntegerCell

	case uint:
		if int64(v) > int64(math.MaxInt64) {
			c[0] = float64(v)
			c[1] = FloatCell
		} else {
			c[0] = int64(v)
			c[1] = IntegerCell
		}
	case uint64:
		if v > math.MaxInt64 {
			c[0] = float64(v)
			c[1] = FloatCell
		} else {
			c[0] = int64(v)
			c[1] = IntegerCell
		}

	case float32:
		c[0] = float64(v)
		c[1] = FloatCell
	case float64:
		c[0] = float64(v)
		c[1] = FloatCell

	case string:
		if len(v) == 0 {
			c[0] = nil
			c[1] = BlankCell
		} else {
			c[0] = v
			c[1] = StringCell
		}
	case []byte:
		if len(v) == 0 {
			c[0] = nil
			c[1] = BlankCell
		} else {
			c[0] = string(v)
			c[1] = StringCell
		}
	case []uint16:
		if len(v) == 0 {
			c[0] = nil
			c[1] = BlankCell
		} else {
			c[0] = string(utf16.Decode(v))
			c[1] = StringCell
		}
	case []rune:
		if len(v) == 0 {
			c[0] = nil
			c[1] = BlankCell
		} else {
			c[0] = string(v)
			c[1] = StringCell
		}
	case time.Time:
		c[0] = v
		c[1] = DateCell

	case fmt.Stringer:
		s := v.String()
		if len(s) == 0 {
			c[0] = nil
			c[1] = BlankCell
		} else {
			c[0] = s
			c[1] = StringCell
		}
	default:
		panic("grate: data type not handled")
	}
	return Cell(c)
}

// SetFormatNumber changes the number format stored with the cell.
func (c *Cell) SetFormatNumber(f uint16) {
	if f == 0 {
		*c = (*c)[:2]
		return
	}

	if len(*c) == 2 {
		*c = append(*c, f)
	} else {
		(*c)[2] = f
	}
}

func (c Cell) Equal(other Cell) bool {
	if c.Type() == FloatCell || other.Type() == FloatCell ||
		c.Type() == IntegerCell || other.Type() == IntegerCell {
		v1, ok := c[0].(float64)
		v1x, okx := c[0].(int64)
		if okx {
			v1 = float64(v1x)
			ok = true
		}
		if !ok {
			fmt.Sscanf(fmt.Sprint(c[0]), "%g", &v1)
		}
		v2, ok := other[0].(float64)
		v2x, okx := other[0].(int64)
		if okx {
			v2 = float64(v2x)
			ok = true
		}
		if !ok {
			fmt.Sscanf(fmt.Sprint(c[0]), "%g", &v2)
		}
		return v1 == v2
	}

	return c.Less(other) == other.Less(c)
}

func (c Cell) Less(other Cell) bool {
	if len(c) == 0 {
		return false
	}
	switch v1 := c[0].(type) {
	case nil:
		return false
	case bool:
		// F < T = T
		// F < F = F
		// T < T = F
		// T < F = F
		if v1 {
			return false
		}

		// if v2 is truthy, return true
		switch v2 := other[0].(type) {
		case nil:
			return false
		case bool:
			return v2
		case int64:
			return v2 != 0
		case float64:
			return v2 != 0.0
		case string:
			return boolStrings[v2]
		}

	case int64:
		// v1 < v2

		switch v2 := other[0].(type) {
		case nil:
			return false
		case bool:
			x := int64(0)
			if v2 {
				x = 1
			}
			return v1 < x
		case int64:
			return v1 < v2
		case float64:
			if v2 < math.MinInt64 {
				return false
			}
			if v2 > math.MaxInt64 {
				return true
			}
			return float64(v1) < v2
		case string:
			var x int64
			_, err := fmt.Sscanf(v2, "%d", &x)
			if err == nil {
				return v1 < x
			}
			return fmt.Sprint(v1) < v2
		}
	case float64:
		switch v2 := other[0].(type) {
		case nil:
			return false
		case bool:
			x := float64(0.0)
			if v2 {
				x = 1.0
			}
			return v1 < x
		case int64:
			if v1 < math.MinInt64 {
				return true
			}
			if v1 > math.MaxInt64 {
				return false
			}
			return v1 < float64(v2)
		case float64:
			return v1 < v2
		case string:
			var x float64
			_, err := fmt.Sscanf(v2, "%g", &x)
			if err == nil {
				return v1 < x
			}
			return fmt.Sprint(v1) < v2
		}
	case string:
		//return v1 < fmt.Sprint(other[0])

		switch v2 := other[0].(type) {
		case nil:
			return false
		case bool:
			return v2 && !boolStrings[v1]
		case int64:
			var x int64
			_, err := fmt.Sscanf(v1, "%d", &x)
			if err == nil {
				return x < v2
			}
			return v1 < fmt.Sprint(v2)
		case float64:
			var x float64
			_, err := fmt.Sscanf(v1, "%g", &x)
			if err == nil {
				return x < v2
			}
			return v1 < fmt.Sprint(v2)
		case string:
			return v1 < v2
		}

	}

	panic("unable to compare cells (invalid internal type)")
}


================================================
FILE: commonxl/dates.go
================================================
package commonxl

import (
	"strings"
	"time"
)

// ConvertToDate converts a floating-point value using the
// Excel date serialization conventions.
func (x *Formatter) ConvertToDate(val float64) time.Time {
	// http://web.archive.org/web/20190808062235/http://aa.usno.navy.mil/faq/docs/JD_Formula.php
	v := int(val)
	if v < 61 {
		jdate := val + 0.5
		if (x.flags & fMode1904) != 0 {
			jdate += 2416480.5
		} else {
			jdate += 2415018.5
		}
		JD := int(jdate)
		frac := jdate - float64(JD)

		L := JD + 68569
		N := 4 * L / 146097
		L = L - (146097*N+3)/4
		I := 4000 * (L + 1) / 1461001
		L = L - 1461*I/4 + 31
		J := 80 * L / 2447
		day := L - 2447*J/80
		L = J / 11
		month := time.Month(J + 2 - 12*L)
		year := 100*(N-49) + I + L

		t := time.Duration(float64(time.Hour*24) * frac)
		return time.Date(year, month, day, 0, 0, 0, 0, time.UTC).Add(t)
	}
	frac := val - float64(v)
	date := time.Date(1904, 1, 1, 0, 0, 0, 0, time.UTC)
	if (x.flags & fMode1904) == 0 {
		date = time.Date(1899, 12, 30, 0, 0, 0, 0, time.UTC)
	}

	t := time.Duration(float64(time.Hour*24) * frac)
	return date.AddDate(0, 0, v).Add(t)
}

func timeFmtFunc(f string) FmtFunc {
	return func(x *Formatter, v interface{}) string {
		t, ok := v.(time.Time)
		if !ok {
			fval, ok := convertToFloat64(v)
			if !ok {
				return "MUST BE time.Time OR numeric TO FORMAT CORRECTLY"
			}
			t = x.ConvertToDate(fval)
		}
		//log.Println("formatting date", t, "with", f, "=", t.Format(f))
		return t.Format(f)
	}
}

// same as above but replaces "AM" and "PM" with chinese translations.
// TODO: implement others
func cnTimeFmtFunc(f string) FmtFunc {
	return func(x *Formatter, v interface{}) string {
		t, ok := v.(time.Time)
		if !ok {
			fval, ok := convertToFloat64(v)
			if !ok {
				return "MUST BE time.Time OR numeric TO FORMAT CORRECTLY"
			}
			t = x.ConvertToDate(fval)
		}
		s := t.Format(f)
		s = strings.Replace(s, `AM`, `上午`, 1)
		return strings.Replace(s, `PM`, `下午`, 1)
	}
}


================================================
FILE: commonxl/fmt.go
================================================
package commonxl

import (
	"fmt"
	"strconv"
	"strings"
)

// FmtFunc will format a value according to the designated style.
type FmtFunc func(*Formatter, interface{}) string

func staticFmtFunc(s string) FmtFunc {
	return func(x *Formatter, v interface{}) string {
		return s
	}
}

func surround(pre string, ff FmtFunc, post string) FmtFunc {
	return func(x *Formatter, v interface{}) string {
		return pre + ff(x, v) + post
	}
}

func addNegParens(ff FmtFunc) FmtFunc {
	return func(x *Formatter, v interface{}) string {
		s1 := ff(x, v)
		if s1[0] == '-' {
			return "(" + s1[1:] + ")"
		}
		return s1
	}
}

func addCommas(ff FmtFunc) FmtFunc {
	return func(x *Formatter, v interface{}) string {
		s1 := ff(x, v)
		isNeg := false
		if s1[0] == '-' {
			isNeg = true
			s1 = s1[1:]
		}
		endIndex := strings.IndexAny(s1, ".eE")
		if endIndex < 0 {
			endIndex = len(s1)
		}
		for endIndex > 3 {
			endIndex -= 3
			s1 = s1[:endIndex] + "," + s1[endIndex:]
		}
		if isNeg {
			return "-" + s1
		}
		return s1
	}
}

func identFunc(x *Formatter, v interface{}) string {
	switch x := v.(type) {
	case bool:
		if x {
			return "TRUE"
		}
		return "FALSE"
	case int64:
		s := strconv.FormatInt(x, 10)
		if len(s) <= 11 {
			return s
		}
	case float64:
		s := strconv.FormatFloat(x, 'f', -1, 64)
		if len(s) <= 11 || (len(s) == 12 && x < 0) {
			return s
		}
		s = strconv.FormatFloat(x, 'g', 6, 64)
		if len(s) <= 11 {
			return s
		}
	case string:
		return x
	case fmt.Stringer:
		return x.String()
	}
	return fmt.Sprint(v)
}

func sprintfFunc(fs string, mul int) FmtFunc {
	wantInt64 := strings.Contains(fs, "%d")
	return func(x *Formatter, v interface{}) string {
		switch val := v.(type) {
		case int, uint, int64, uint64, int32, uint32, uint16, int16:
			return fmt.Sprintf(fs, v)

		case float64:
			val *= float64(mul)
			if wantInt64 {
				v2 := int64(val)
				return fmt.Sprintf(fs, v2)
			}
			return fmt.Sprintf(fs, val)
		}
		return fmt.Sprint(v)
	}
}

func convertToInt64(v interface{}) (int64, bool) {
	x, ok := convertToFloat64(v)
	return int64(x), ok
}

func convertToFloat64(v interface{}) (float64, bool) {
	switch val := v.(type) {
	case float64:
		return val, true
	case bool:
		if val {
			return 1.0, true
		}
		return 0.0, true
	case int:
		return float64(val), true
	case int8:
		return float64(val), true
	case int16:
		return float64(val), true
	case int32:
		return float64(val), true
	case int64:
		return float64(val), true
	case uint:
		return float64(val), true
	case uint8:
		return float64(val), true
	case uint16:
		return float64(val), true
	case uint32:
		return float64(val), true
	case uint64:
		return float64(val), true
	case float32:
		return float64(val), true
	case string:
		nf, err := strconv.ParseFloat(val, 64)
		return nf, err == nil
	default:
		return 0.0, false
	}
}

// replaces a zero with a dash
func zeroDashFunc(ff FmtFunc) FmtFunc {
	return func(x *Formatter, v interface{}) string {
		fval, ok := convertToFloat64(v)
		if !ok {
			// strings etc returned as-is
			return fmt.Sprint(v)
		}
		if fval == 0.0 {
			return "-"
		}
		return ff(x, v)
	}
}

func fracFmtFunc(n int) FmtFunc {
	return func(x *Formatter, v interface{}) string {
		f, ok := convertToFloat64(v)
		if !ok {
			return "MUST BE numeric TO FORMAT CORRECTLY"
		}
		w, n, d := DecimalToWholeFraction(f, n, n)
		if n == 0 {
			return fmt.Sprintf("%d", w)
		}
		if w == 0 {
			if f < 0 && n > 0 {
				n = -n
			}
			return fmt.Sprintf("%d/%d", n, d)
		}
		return fmt.Sprintf("%d %d/%d", w, n, d)
	}
}

// handle (up to) all four format cases:
// positive;negative;zero;other
func switchFmtFunc(pos FmtFunc, others ...FmtFunc) FmtFunc {
	stringFF := identFunc
	zeroFF := pos
	negFF := pos
	if len(others) > 0 {
		negFF = others[0]
		if len(others) > 1 {
			zeroFF = others[1]
			if len(others) > 2 {
				stringFF = others[2]
			}
		}
	}
	return func(x *Formatter, v interface{}) string {
		val, ok := convertToFloat64(v)
		if !ok {
			return stringFF(x, v)
		}
		if val == 0.0 {
			return zeroFF(x, v)
		}
		if val < 0.0 {
			return negFF(x, v)
		}
		return pos(x, v)
	}
}

// mapping of standard built-ins to Go date format funcs.
var goFormatters = map[uint16]FmtFunc{
	0:  identFunc, // FIXME: better "general" formatter
	49: identFunc,

	14: timeFmtFunc(`01-02-06`),
	15: timeFmtFunc(`2-Jan-06`),
	16: timeFmtFunc(`2-Jan`),
	17: timeFmtFunc(`Jan-06`),
	20: timeFmtFunc(`15:04`),
	21: timeFmtFunc(`15:04:05`),
	22: timeFmtFunc(`1/2/06 15:04`),
	45: timeFmtFunc(`04:05`),
	46: timeFmtFunc(`3:04:05`),
	47: timeFmtFunc(`0405.9`),
	27: timeFmtFunc(`2006"年"1"月"`),
	28: timeFmtFunc(`1"月"2"日"`),
	29: timeFmtFunc(`1"月"2"日"`),
	30: timeFmtFunc(`1-2-06`),
	31: timeFmtFunc(`2006"年"1"月"2"日"`),
	32: timeFmtFunc(`15"时"04"分"`),
	33: timeFmtFunc(`15"时"04"分"05"秒"`),
	36: timeFmtFunc(`2006"年"2"月"`),
	50: timeFmtFunc(`2006"年"2"月"`),
	51: timeFmtFunc(`1"月"2"日"`),
	52: timeFmtFunc(`2006"年"1"月"`),
	53: timeFmtFunc(`1"月"2"日"`),
	54: timeFmtFunc(`1"月"2"日"`),
	57: timeFmtFunc(`2006"年"1"月"`),
	58: timeFmtFunc(`1"月"2"日"`),
	71: timeFmtFunc(`2/1/2006`),
	72: timeFmtFunc(`2-Jan-06`),
	73: timeFmtFunc(`2-Jan`),
	74: timeFmtFunc(`Jan-06`),
	75: timeFmtFunc(`15:04`),
	76: timeFmtFunc(`15:04:05`),
	77: timeFmtFunc(`2/1/2006 15:04`),
	78: timeFmtFunc(`04:05`),
	79: timeFmtFunc(`15:04:05`),
	80: timeFmtFunc(`04:05.9`),
	81: timeFmtFunc(`2/1/06`),
	18: timeFmtFunc(`3:04 PM`),
	19: timeFmtFunc(`3:04:05 PM`),

	34: cnTimeFmtFunc(`PM 3"时"04"分"`),
	35: cnTimeFmtFunc(`PM 3"时"04"分"05"秒"`),
	55: cnTimeFmtFunc(`PM 3"时"04"分"`),
	56: cnTimeFmtFunc(`PM 3"时"04"分"05"秒`),

	12: fracFmtFunc(1),
	13: fracFmtFunc(2),

	69: fracFmtFunc(1),
	70: fracFmtFunc(2),

	1:  sprintfFunc(`%d`, 1),
	2:  sprintfFunc(`%4.2f`, 1),
	59: sprintfFunc(`%d`, 1),
	60: sprintfFunc(`%4.2f`, 1),

	9:  sprintfFunc(`%d%%`, 100),
	10: sprintfFunc(`%4.2f%%`, 100),
	67: sprintfFunc(`%d%%`, 100),
	68: sprintfFunc(`%4.2f%%`, 100),

	3:  addCommas(sprintfFunc("%d", 1)),
	61: addCommas(sprintfFunc("%d", 1)),
	37: addNegParens(addCommas(sprintfFunc("%d", 1))),
	38: addNegParens(addCommas(sprintfFunc("%d", 1))),

	4:  addCommas(sprintfFunc("%4.2f", 1)),
	62: addCommas(sprintfFunc("%4.2f", 1)),
	39: addNegParens(addCommas(sprintfFunc("%4.2f", 1))),
	40: addNegParens(addCommas(sprintfFunc("%4.2f", 1))),

	11: sprintfFunc(`%4.2E`, 1),
	48: sprintfFunc(`%3.1E`, 1),

	41: zeroDashFunc(addCommas(sprintfFunc("%d", 1))),
	43: zeroDashFunc(addCommas(sprintfFunc("%4.2f", 1))),

	42: switchFmtFunc(
		surround("$", addCommas(sprintfFunc("%d", 1)), ""),
		surround("$(", addCommas(sprintfFunc("%d", 1)), ")"),
		staticFmtFunc("$-")),
	44: switchFmtFunc(
		surround("$", addCommas(sprintfFunc("%4.2f", 1)), ""),
		surround("$(", addCommas(sprintfFunc("%4.2f", 1)), ")"),
		staticFmtFunc("$-")),
}


================================================
FILE: commonxl/fmt_test.go
================================================
package commonxl

import (
	"log"
	"testing"
	"time"
)

type testcaseNums struct {
	v interface{}
	s string
}

var commas = []testcaseNums{
	{10, "10"},
	{float64(10), "10"},
	{float64(10) + 0.12345, "10.12345"},
	{-10, "-10"},
	{float64(-10), "-10"},
	{float64(-10) + 0.12345, "-9.87655"},
	{uint16(10), "10"},
	{100, "100"},
	{float64(100), "100"},
	{float64(100) + 0.12345, "100.12345"},
	{-100, "-100"},
	{float64(-100), "-100"},
	{float64(-100) + 0.12345, "-99.87655"},
	{uint16(100), "100"},
	{1000, "1,000"},
	{float64(1000), "1,000"},
	{float64(1000) + 0.12345, "1,000.12345"},
	{-1000, "-1,000"},
	{float64(-1000), "-1,000"},
	{float64(-1000) + 0.12345, "-999.87655"},
	{uint16(1000), "1,000"},
	{10000, "10,000"},
	{float64(10000), "10,000"},
	{float64(10000) + 0.12345, "10,000.12345"},
	{-10000, "-10,000"},
	{float64(-10000), "-10,000"},
	{float64(-10000) + 0.12345, "-9,999.87655"},
	{uint16(10000), "10,000"},
	{100000, "100,000"},
	{float64(100000), "100,000"},
	{float64(100000) + 0.12345, "100,000.12345"},
	{-100000, "-100,000"},
	{float64(-100000), "-100,000"},
	{float64(-100000) + 0.12345, "-99,999.87655"},
	{uint64(100000), "100,000"},
	{1000000, "1,000,000"},
	{float64(1000000), "1e+06"},
	{float64(1000000) + 0.12345, "1.00000012345e+06"},
	{-1000000, "-1,000,000"},
	{float64(-1000000), "-1e+06"},
	{float64(-1000000) + 0.12345, "-999,999.87655"},
	{uint64(1000000), "1,000,000"},
	{10000000, "10,000,000"},
	{float64(10000000), "1e+07"},
	{float64(10000000) + 0.12345, "1.000000012345e+07"},
	{-10000000, "-10,000,000"},
	{float64(-10000000), "-1e+07"},
	{float64(-10000000) + 0.12345, "-9.99999987655e+06"},
	{uint64(10000000), "10,000,000"},
	{100000000, "100,000,000"},
	{float64(100000000), "1e+08"},
	{float64(100000000) + 0.12345, "1.0000000012345e+08"},
	{-100000000, "-100,000,000"},
	{float64(-100000000), "-1e+08"},
	{float64(-100000000) + 0.12345, "-9.999999987655e+07"},
	{uint64(100000000), "100,000,000"},
}

func TestCommas(t *testing.T) {
	cf := addCommas(identFunc)
	for _, c := range commas {
		fs := cf(nil, c.v)
		if c.s != fs {
			t.Fatalf("commas failed: get '%s' but expected '%s' for %T(%v)",
				fs, c.s, c.v, c.v)
		}
	}
}

func TestDateFormats(t *testing.T) {
	var testDates = []time.Time{
		time.Date(1901, 7, 11, 1, 5, 0, 0, time.UTC),
		time.Date(1905, 7, 11, 4, 10, 0, 0, time.UTC),
		time.Date(1904, 7, 11, 8, 15, 0, 0, time.UTC),
		time.Date(1993, 7, 11, 12, 20, 0, 0, time.UTC),
		time.Date(1983, 7, 11, 16, 30, 0, 0, time.UTC),
		time.Date(1983, 7, 11, 20, 45, 0, 0, time.UTC),
		time.Date(2000, 12, 31, 23, 59, 0, 0, time.UTC),
		time.Date(2002, 12, 31, 23, 59, 0, 0, time.UTC),
		time.Date(2012, 3, 10, 9, 30, 0, 0, time.UTC),
		time.Date(2014, 3, 27, 9, 37, 0, 0, time.UTC),
	}

	fx := &Formatter{}
	for _, t := range testDates {
		for fid, ctype := range builtInFormatTypes {
			if ctype != DateCell {
				continue
			}
			ff, _ := goFormatters[fid]
			// mainly testing these don't crash...
			log.Println(ff(fx, t))
		}
	}
}
func TestBoolFormats(t *testing.T) {
	ff, _ := makeFormatter(`"yes";"yes";"no"`)

	if "no" != ff(nil, false) {
		t.Fatal(`false should be "no"`)
	}
	if "no" != ff(nil, 0) {
		t.Fatal(`0 should be "no"`)
	}
	if "no" != ff(nil, 0.0) {
		t.Fatal(`0.0 should be "no"`)
	}

	/////

	if "yes" != ff(nil, true) {
		t.Fatal(`true should be "yes"`)
	}
	if "yes" != ff(nil, 99) {
		t.Fatal(`99 should be "yes"`)
	}
	if "yes" != ff(nil, -4) {
		t.Fatal(`-4 should be "yes"`)
	}

	if "yes" != ff(nil, 4.0) {
		t.Fatal(`4.0 should be "yes"`)
	}
	if "yes" != ff(nil, -99.0) {
		t.Fatal(`-99.0 should be "yes"`)
	}
}


================================================
FILE: commonxl/formats.go
================================================
package commonxl

import (
	"errors"
	"fmt"
	"regexp"
	"strings"
)

// Formatter contains formatting methods common to Excel spreadsheets.
type Formatter struct {
	flags           uint64
	customCodes     map[uint16]FmtFunc
	customCodeTypes map[uint16]CellType
}

const (
	fMode1904 uint64 = 1
)

// Mode1904 indicates that dates start on Jan 1, 1904
// this setting was used in early MacOS Excel applications.
func (x *Formatter) Mode1904(enabled bool) {
	if enabled {
		x.flags |= fMode1904
	} else {
		x.flags = x.flags &^ fMode1904
	}
}

// Add a custom number format to the formatter.
func (x *Formatter) Add(fmtID uint16, formatCode string) error {
	if x.customCodes == nil {
		x.customCodes = make(map[uint16]FmtFunc)
		x.customCodeTypes = make(map[uint16]CellType)
	}
	if strings.ToLower(formatCode) == "general" {
		x.customCodes[fmtID] = goFormatters[0]
		return nil
	}
	_, ok := goFormatters[fmtID]
	if ok {
		return errors.New("grate/commonxl: cannot replace default number formats")
	}

	_, ok2 := x.customCodes[fmtID]
	if ok2 {
		return errors.New("grate/commonxl: cannot replace existing number formats")
	}

	x.customCodes[fmtID], x.customCodeTypes[fmtID] = makeFormatter(formatCode)
	return nil
}

func (x *Formatter) getCellType(fmtID uint16) (CellType, bool) {
	if ct, ok := builtInFormatTypes[fmtID]; ok {
		return ct, true
	}
	if x.customCodeTypes != nil {
		ct, ok := x.customCodeTypes[fmtID]
		return ct, ok
	}
	return 0, false
}

var (
	minsMatch = regexp.MustCompile("h.*m.*s")
	nonEsc    = regexp.MustCompile(`([^"]|^)"`)
	squash    = regexp.MustCompile(`[*_].`)
	fixEsc    = regexp.MustCompile(`\\(.)`)

	formatMatchBrackets    = regexp.MustCompile(`\[[^\]]*\]`)
	formatMatchTextLiteral = regexp.MustCompile(`"[^"]*"`)
)

func makeFormatter(s string) (FmtFunc, CellType) {
	//log.Printf("makeFormatter('%s')", s)
	// remove any coloring marks
	s = formatMatchBrackets.ReplaceAllString(s, "")
	if strings.Contains(s, ";") {
		parts := strings.Split(s, ";")
		posFF, ctypePos := makeFormatter(parts[0])
		rem := make([]FmtFunc, len(parts)-1)
		for i, ps := range parts[1:] {
			rem[i], _ = makeFormatter(ps)
		}
		return switchFmtFunc(posFF, rem...), ctypePos
	}

	// escaped characters, and quoted text
	s2 := fixEsc.ReplaceAllString(s, "")
	s2 = formatMatchTextLiteral.ReplaceAllString(s, "")

	if strings.ContainsAny(s2, "ymdhs") {
		// it's a date/time format

		if loc := minsMatch.FindStringIndex(s); loc != nil {
			// m or mm in loc[0]:loc[1] is a minute format
			inner := s[loc[0]:loc[1]]
			inner = strings.Replace(inner, "mm", "04", 1)
			inner = strings.Replace(inner, "m", "4", 1)
			s = s[:loc[0]] + inner + s[loc[1]:]
		}
		dfreps := [][]string{
			{"hh", "15"}, {"h", "15"},
			{"ss", "05"}, {"s", "5"},
			{"mmmmm", "Jan"}, // super ambiguous, replace with 3-letter month
			{"mmmm", "January"}, {"mmm", "Jan"},
			{"mm", "01"}, {"m", "1"},
			{"dddd", "Monday"}, {"ddd", "Mon"},
			{"dd", "02"}, {"d", "2"},
			{"yyyy", "2006"}, {"yy", "06"},
		}
		if strings.Contains(s, "AM") || strings.Contains(s, "PM") {
			dfreps[0][1] = "03"
			dfreps[1][1] = "3"
		}
		for _, dfr := range dfreps {
			s = strings.Replace(s, dfr[0], dfr[1], 1)
		}

		s = nonEsc.ReplaceAllString(s, `$1`)
		s = squash.ReplaceAllString(s, ``)
		s = fixEsc.ReplaceAllString(s, `$1`)

		//log.Printf("   made time formatter '%s'", s)
		return timeFmtFunc(s), DateCell
	}

	var ff FmtFunc
	var ctype CellType
	if strings.ContainsAny(s, ".Ee") {
		verb := "f"
		if strings.ContainsAny(s, "Ee") {
			verb = "E"
		}
		s = regexp.MustCompile("[eE]+[+-]0+").ReplaceAllString(s, "")
		s2 := strings.ReplaceAll(s, ",", "")
		i1 := strings.IndexAny(s2, "0")
		i2 := strings.IndexByte(s2, '.')
		i3 := strings.LastIndexAny(s2, "0.")
		mul := 1
		if strings.Contains(s2, "%") {
			mul = 100
		}
		sf := fmt.Sprintf("%%%d.%d%s", i3-i1, i3-i2, verb)
		//log.Printf("   made float formatter '%s'", sf)
		ff = sprintfFunc(sf, mul)
		ctype = FloatCell
	} else {
		s2 := strings.ReplaceAll(s, ",", "")
		i1 := strings.IndexAny(s2, "0")
		i2 := strings.LastIndexAny(s2, "0.")
		mul := 1
		if strings.Contains(s2, "%") {
			mul = 100
		}
		sf := fmt.Sprintf("%%%dd", i2-i1)
		if (i2 - i1) == 0 {
			sf = "%d"
		}
		//log.Printf("   made int formatter '%s'", sf)
		ff = sprintfFunc(sf, mul)
		ctype = IntegerCell
	}

	if strings.Contains(s, ",") {
		ff = addCommas(ff)
		//log.Printf("   added commas")
	}

	surReg := regexp.MustCompile(`[0#?,.]+`)
	prepost := surReg.Split(s, 2)
	if len(prepost) > 0 && len(prepost[0]) > 0 {
		prepost[0] = nonEsc.ReplaceAllString(prepost[0], `$1`)
		prepost[0] = squash.ReplaceAllString(prepost[0], ``)
		prepost[0] = fixEsc.ReplaceAllString(prepost[0], `$1`)
	}
	if len(prepost) == 1 {
		if prepost[0] == "@" {
			return identFunc, StringCell
		}
		//log.Printf("   added static ('%s')", prepost[0])
		return staticFmtFunc(prepost[0]), StringCell
	}
	if len(prepost[0]) > 0 || len(prepost[1]) > 0 {
		prepost[1] = nonEsc.ReplaceAllString(prepost[1], `$1`)
		prepost[1] = squash.ReplaceAllString(prepost[1], ``)
		prepost[1] = fixEsc.ReplaceAllString(prepost[1], `$1`)

		ff = surround(prepost[0], ff, prepost[1])
		//log.Printf("   added surround ('%s' ... '%s')", prepost[0], prepost[1])
	}

	return ff, ctype
}

// Get the number format func to use for formatting values,
// it returns false when fmtID is unknown.
func (x *Formatter) Get(fmtID uint16) (FmtFunc, bool) {
	ff, ok := goFormatters[fmtID]
	if !ok {
		fs, ok2 := x.customCodes[fmtID]
		if ok2 {
			return fs, true
		}
		ff = identFunc
	}

	return ff, ok
}

// Apply the specified number format to the value.
// Returns false when fmtID is unknown.
func (x *Formatter) Apply(fmtID uint16, val interface{}) (string, bool) {
	ff, ok := goFormatters[fmtID]
	if !ok {
		fs, ok2 := x.customCodes[fmtID]
		if ok2 {
			return fs(x, val), true
		}
	}
	return ff(x, val), ok
}

// builtInFormats are all the built-in number formats for XLS/XLSX.
var builtInFormats = map[uint16]string{
	0:  `General`,
	1:  `0`,
	2:  `0.00`,
	3:  `#,##0`,
	4:  `#,##0.00`,
	9:  `0%`,
	10: `0.00%`,

	11: `0.00E+00`,
	12: `# ?/?`,
	13: `# ??/??`,
	14: `mm-dd-yy`,
	15: `d-mmm-yy`,
	16: `d-mmm`,
	17: `mmm-yy`,
	18: `h:mm AM/PM`,
	19: `h:mm:ss AM/PM`,
	20: `h:mm`,
	21: `h:mm:ss`,
	22: `m/d/yy h:mm`,
	37: `#,##0 ;(#,##0)`,
	38: `#,##0 ;[Red](#,##0)`,
	39: `#,##0.00;(#,##0.00)`,
	40: `#,##0.00;[Red](#,##0.00)`,

	41: `_(* #,##0_);_(* \(#,##0\);_(* "-"_);_(@_)`,
	42: `_("$"* #,##0_);_("$"* \(#,##0\);_("$"* "-"_);_(@_)`,
	43: `_(* #,##0.00_);_(* \(#,##0.00\);_(* "-"??_);_(@_)`,
	44: `_("$"* #,##0.00_);_("$"* \(#,##0.00\);_("$"* "-"??_);_(@_)`,

	45: `mm:ss`,
	46: `[h]:mm:ss`,
	47: `mmss.0`,
	48: `##0.0E+0`,
	49: `@`,

	// zh-cn format codes
	27: `yyyy"年"m"月"`,
	28: `m"月"d"日"`,
	29: `m"月"d"日"`,
	30: `m-d-yy`,
	31: `yyyy"年"m"月"d"日"`,
	32: `h"时"mm"分"`,
	33: `h"时"mm"分"ss"秒"`,
	34: `上午/下午 h"时"mm"分"`,
	35: `上午/下午 h"时"mm"分"ss"秒"`,
	36: `yyyy"年"m"月"`,
	50: `yyyy"年"m"月"`,
	51: `m"月"d"日"`,
	52: `yyyy"年"m"月"`,
	53: `m"月"d"日"`,
	54: `m"月"d"日"`,
	55: `上午/下午 h"时"mm"分"`,
	56: `上午/下午 h"时"mm"分"ss"秒`,
	57: `yyyy"年"m"月"`,
	58: `m"月"d"日"`,

	// th-th format codes (in the spec these have a "t" prefix?)
	59: `0`,
	60: `0.00`,
	61: `#,##0`,
	62: `#,##0.00`,
	67: `0%`,
	68: `0.00%`,
	69: `# ?/?`,
	70: `# ??/??`,

	// th format code, but translated to aid the parser
	71: `d/m/yyyy`,      // `ว/ด/ปปปป`,
	72: `d-mmm-yy`,      // `ว-ดดด-ปป`,
	73: `d-mmm`,         // `ว-ดดด`,
	74: `mmm-yy`,        // `ดดด-ปป`,
	75: `h:mm`,          // `ช:นน`,
	76: `h:mm:ss`,       // `ช:นน:ทท`,
	77: `d/m/yyyy h:mm`, // `ว/ด/ปปปป ช:นน`,
	78: `mm:ss`,         // `นน:ทท`,
	79: `[h]:mm:ss`,     // `[ช]:นน:ทท`,
	80: `mm:ss.0`,       // `นน:ทท.0`,
	81: `d/m/bb`,        // `d/m/bb`,
}

// builtInFormatTypes are the underlying datatypes for built-in number formats in XLS/XLSX.
var builtInFormatTypes = map[uint16]CellType{
	// 0 has no defined type
	1:  IntegerCell,
	2:  FloatCell,
	3:  IntegerCell,
	4:  FloatCell,
	9:  FloatCell,
	10: FloatCell,

	11: FloatCell,
	12: FloatCell,
	13: FloatCell,
	14: DateCell,
	15: DateCell,
	16: DateCell,
	17: DateCell,
	18: DateCell,
	19: DateCell,
	20: DateCell,
	21: DateCell,
	22: DateCell,
	37: IntegerCell,
	38: IntegerCell,
	39: FloatCell,
	40: FloatCell,
	41: IntegerCell,
	42: IntegerCell,
	43: FloatCell,
	44: FloatCell,
	45: DateCell, // Durations?
	46: DateCell,
	47: DateCell,
	48: FloatCell,
	49: StringCell,
	27: DateCell,
	28: DateCell,
	29: DateCell,
	30: DateCell,
	31: DateCell,
	32: DateCell,
	33: DateCell,
	34: DateCell,
	35: DateCell,
	36: DateCell,
	50: DateCell,
	51: DateCell,
	52: DateCell,
	53: DateCell,
	54: DateCell,
	55: DateCell,
	56: DateCell,
	57: DateCell,
	58: DateCell,
	59: IntegerCell,
	60: FloatCell,
	61: IntegerCell,
	62: FloatCell,
	67: FloatCell,
	68: FloatCell,
	69: FloatCell,
	70: FloatCell,
	71: DateCell,
	72: DateCell,
	73: DateCell,
	74: DateCell,
	75: DateCell,
	76: DateCell,
	77: DateCell,
	78: DateCell,
	79: DateCell,
	80: DateCell,
	81: DateCell,
}


================================================
FILE: commonxl/frac_test.go
================================================
package commonxl

import (
	"math"
	"testing"
)

type testcaseFrac struct {
	v float64
	s string
	n int
}

var fracs = []testcaseFrac{
	{0, "0", 1},
	{0.5, "1/2", 1},
	{-0.5, "-1/2", 1},
	{0.125, "1/8", 1},

	{10, "10", 1},
	{-10, "-10", 1},
	{10.5, "10 1/2", 1},
	{-10.5, "-10 1/2", 1},

	{10.25, "10 1/4", 1},
	{10.75, "10 3/4", 1},
	{10.667, "10 2/3", 1},

	{-10.25, "-10 1/4", 1},
	{-10.75, "-10 3/4", 1},
	{-10.667, "-10 2/3", 1},

	{3.14159, "3 1/7", 1},
	{3.14159, "3 1/7", 2},
	{3.14159, "3 16/113", 3},
	{3.14159, "3 431/3044", 4},
	{3.14159, "3 3432/24239", 5},
	{3.14159, "3 14159/100000", 6},

	{math.Pi, "3 1/7", 1},
	{math.Pi, "3 1/7", 2},
	{math.Pi, "3 16/113", 3}, // err = 2.6e-7
	{math.Pi, "3 16/113", 4}, // better because 431/3044 err = 2.6e-6
	{math.Pi, "3 14093/99532", 5},
	{math.Pi, "3 14093/99532", 6},

	{-math.Pi, "-3 1/7", 1},
	{-math.Pi, "-3 1/7", 2},
	{-math.Pi, "-3 16/113", 3}, // err = 2.6e-7
	{-math.Pi, "-3 16/113", 4}, // better because 431/3044 err = 2.6e-6
	{-math.Pi, "-3 14093/99532", 5},
	{-math.Pi, "-3 14093/99532", 6},

	// TODO: fixed denominator fractions (e.g. "??/8" )
	// TODO: string interpolations (e.g. '0 "pounds and " ??/100 "pence"')
	// examples: https://bettersolutions.com/excel/formatting/number-tab-fractions.htm
}

func TestFractions(t *testing.T) {
	for _, c := range fracs {
		ff := fracFmtFunc(c.n)
		fs := ff(nil, c.v)
		if c.s != fs {
			t.Fatalf("fractions failed: got: '%s' expected: '%s' for %T(%v)",
				fs, c.s, c.v, c.v)
		}
	}
}


================================================
FILE: commonxl/numbers.go
================================================
package commonxl

import (
	"math"
)

// DecimalToWholeFraction converts a floating point value into a whole
// number and fraction approximation with at most nn digits in the numerator
// and nd digits in the denominator.
func DecimalToWholeFraction(val float64, nn, nd int) (whole, num, den int) {
	wholeF, part := math.Modf(val)
	if part == 0.0 {
		return int(wholeF), 0, 1
	}
	if part < 0.0 {
		part = -part
	}
	whole = int(wholeF)
	num, den = DecimalToFraction(part, nn, nd)
	return
}

// DecimalToFraction converts a floating point value into a fraction
// approximation with at most nn digits in the numerator and nd
// digits in the denominator.
func DecimalToFraction(val float64, nn, nd int) (num, den int) {
	// http://web.archive.org/web/20111027100847/http://homepage.smc.edu/kennedy_john/DEC2FRAC.PDF
	sign := 1
	z := val
	if val < 0 {
		sign = -1
		z = -val
	}
	if nn == 0 {
		nn = 2
	}
	if nd == 0 {
		nd = 2
	}
	maxn := math.Pow(10.0, float64(nn)) // numerator with nn digits
	maxd := math.Pow(10.0, float64(nd)) // denominator with nd digits

	_, fracPart := math.Modf(val)
	if fracPart == 0.0 {
		return int(z) * sign, 1
	}
	if fracPart < 1e-9 {
		return sign, int(1e9)
	}
	if fracPart > 1e9 {
		return int(1e9) * sign, 1
	}

	diff := 1.0
	denom := 1.0
	numer := 0.0
	var lastDenom, lastNumer float64
	for diff > 1e-10 && z != math.Floor(z) {
		z = 1 / (z - math.Floor(z))
		tmp := denom
		denom = (denom * math.Floor(z)) + lastDenom
		lastDenom = tmp
		lastNumer = numer
		numer = math.Round(val * denom)
		if numer >= maxn || denom >= maxd {
			return sign * int(lastNumer), int(lastDenom)
		}
		diff = val - (numer / denom)
		if diff < 0.0 {
			diff = -diff
		}
	}
	return sign * int(numer), int(denom)
}


================================================
FILE: commonxl/sheet.go
================================================
package commonxl

import (
	"fmt"
	"log"
	"time"

	"github.com/pbnjay/grate"
)

// Sheet holds raw and rendered values for a spreadsheet.
type Sheet struct {
	Formatter *Formatter
	NumRows   int
	NumCols   int
	Rows      [][]Cell

	CurRow int
}

// Resize the sheet for the number of rows and cols given.
// Newly added cells default to blank.
func (s *Sheet) Resize(rows, cols int) {
	for i := range s.Rows {
		if i > rows {
			break
		}
		n := cols - len(s.Rows[i])
		if n <= 0 {
			continue
		}
		s.Rows[i] = append(s.Rows[i], make([]Cell, n)...)
	}

	if rows <= 0 {
		rows = 1
	}
	if cols <= 0 {
		cols = 1
	}
	s.CurRow = 0
	s.NumRows = rows
	s.NumCols = cols

	for rows >= len(s.Rows) {
		s.Rows = append(s.Rows, make([]Cell, cols))
	}
}

// Put the value at the cell location given.
func (s *Sheet) Put(row, col int, value interface{}, fmtNum uint16) {
	//log.Println(row, col, value, fmtNum)
	if row >= s.NumRows || col >= s.NumCols {
		if grate.Debug {
			log.Printf("grate: cell out of bounds row %d>=%d, col %d>=%d",
				row, s.NumRows, col, s.NumCols)
		}

		// per the spec, this is an invalid Excel file
		// but we'll resize in place instead of crashing out
		if row >= s.NumRows {
			s.NumRows = row + 1
		}
		if col >= s.NumCols {
			s.NumCols = col + 1
		}
		s.Resize(s.NumRows, s.NumCols)
	}

	if spec, ok := value.(string); ok {
		if spec == grate.EndRowMerged || spec == grate.EndColumnMerged || spec == grate.ContinueRowMerged || spec == grate.ContinueColumnMerged {
			s.Rows[row][col] = NewCell(value)
			s.Rows[row][col][1] = StaticCell
			return
		}
	}

	ct, ok := s.Formatter.getCellType(fmtNum)
	if !ok || fmtNum == 0 {
		s.Rows[row][col] = NewCell(value)
	} else {
		s.Rows[row][col] = NewCellWithType(value, ct, s.Formatter)
	}
	s.Rows[row][col].SetFormatNumber(fmtNum)
}

// Set changes the value in an existing cell location.
// NB Currently only used for populating string results for formulas.
func (s *Sheet) Set(row, col int, value interface{}) {
	if row > s.NumRows || col > s.NumCols {
		log.Println("grate: cell out of bounds")
		return
	}

	s.Rows[row][col][0] = value
	s.Rows[row][col][1] = StringCell
}

// SetURL adds a hyperlink to an existing cell location.
func (s *Sheet) SetURL(row, col int, link string) {
	if row > s.NumRows || col > s.NumCols {
		log.Println("grate: cell out of bounds")
		return
	}

	s.Rows[row][col].SetURL(link)
}

// Next advances to the next record of content.
// It MUST be called prior to any Scan().
func (s *Sheet) Next() bool {
	if (s.CurRow + 1) > len(s.Rows) {
		return false
	}
	s.CurRow++
	return true
}

// Raw extracts the raw Cell interfaces underlying the current row.
func (s *Sheet) Raw() []Cell {
	rr := make([]Cell, s.NumCols)
	for i, cell := range s.Rows[s.CurRow-1] {
		rr[i] = cell.Clone()
	}
	return rr
}

// Strings extracts values from the current record into a list of strings.
func (s *Sheet) Strings() []string {
	res := make([]string, s.NumCols)
	for i, cell := range s.Rows[s.CurRow-1] {
		if cell.Type() == BlankCell {
			res[i] = ""
			continue
		}
		if cell.Type() == StaticCell {
			res[i] = cell.Value().(string)
			continue
		}
		val := cell.Value()
		fs, ok := s.Formatter.Apply(cell.FormatNo(), val)
		if !ok {
			fs = fmt.Sprint(val)
		}
		res[i] = fs
	}
	return res
}

// Types extracts the data types from the current record into a list.
// options: "boolean", "integer", "float", "string", "date",
// and special cases: "blank", "hyperlink" which are string types
func (s *Sheet) Types() []string {
	res := make([]string, s.NumCols)
	for i, cell := range s.Rows[s.CurRow-1] {
		res[i] = cell.Type().String()
	}
	return res
}

// Formats extracts the format code for the current record into a list.
func (s *Sheet) Formats() []string {
	ok := true
	res := make([]string, s.NumCols)
	for i, cell := range s.Rows[s.CurRow-1] {
		res[i], ok = builtInFormats[cell.FormatNo()]
		if !ok {
			res[i] = fmt.Sprint(cell.FormatNo())
		}
	}
	return res
}

// Scan extracts values from the current record into the provided arguments
// Arguments must be pointers to one of 5 supported types:
//     bool, int64, float64, string, or time.Time
// If invalid, returns ErrInvalidScanType
func (s *Sheet) Scan(args ...interface{}) error {
	row := s.Rows[s.CurRow-1]

	for i, a := range args {
		val := row[i].Value()

		switch v := a.(type) {
		case bool, int64, float64, string, time.Time:
			return fmt.Errorf("scan destinations must be pointer (arg %d is not)", i)
		case *bool:
			if x, ok := val.(bool); ok {
				*v = x
			} else {
				return fmt.Errorf("scan destination %d expected *%T, not *bool", i, val)
			}
		case *int64:
			if x, ok := val.(int64); ok {
				*v = x
			} else {
				return fmt.Errorf("scan destination %d expected *%T, not *int64", i, val)
			}
		case *float64:
			if x, ok := val.(float64); ok {
				*v = x
			} else {
				return fmt.Errorf("scan destination %d expected *%T, not *float64", i, val)
			}
		case *string:
			if x, ok := val.(string); ok {
				*v = x
			} else {
				return fmt.Errorf("scan destination %d expected *%T, not *string", i, val)
			}
		case *time.Time:
			if x, ok := val.(time.Time); ok {
				*v = x
			} else {
				return fmt.Errorf("scan destination %d expected *%T, not *time.Time", i, val)
			}
		default:
			return fmt.Errorf("scan destination for arg %d is not supported (%T)", i, a)
		}
	}
	return nil
}

// IsEmpty returns true if there are no data values.
func (s *Sheet) IsEmpty() bool {
	return (s.NumCols <= 1 && s.NumRows <= 1)
}

// Err returns the last error that occured.
func (s *Sheet) Err() error {
	return nil
}


================================================
FILE: errs.go
================================================
package grate

import "errors"

var (
	// configure at build time by adding go build arguments:
	//   -ldflags="-X github.com/pbnjay/grate.loglevel=debug"
	loglevel string = "warn"

	// Debug should be set to true to expose detailed logging.
	Debug bool = (loglevel == "debug")
)

// ErrInvalidScanType is returned by Scan for invalid arguments.
var ErrInvalidScanType = errors.New("grate: Scan only supports *bool, *int, *float64, *string, *time.Time arguments")

// ErrNotInFormat is used to auto-detect file types using the defined OpenFunc
// It is returned by OpenFunc when the code does not detect correct file formats.
var ErrNotInFormat = errors.New("grate: file is not in this format")

// ErrUnknownFormat is used when grate does not know how to open a file format.
var ErrUnknownFormat = errors.New("grate: file format is not known/supported")

type errx struct {
	errs []error
}

func (e errx) Error() string {
	return e.errs[0].Error()
}
func (e errx) Unwrap() error {
	if len(e.errs) > 1 {
		return e.errs[1]
	}
	return nil
}

// WrapErr wraps a set of errors.
func WrapErr(e ...error) error {
	if len(e) == 1 {
		return e[0]
	}
	return errx{errs: e}
}


================================================
FILE: go.mod
================================================
module github.com/pbnjay/grate

go 1.16


================================================
FILE: grate.go
================================================
// Package grate opens tabular data files (such as spreadsheets and delimited plaintext files)
// and allows programmatic access to the data contents in a consistent interface.
package grate

import (
	"errors"
	"log"
	"sort"
)

// Source represents a set of data collections.
type Source interface {
	// List the individual data tables within this source.
	List() ([]string, error)

	// Get a Collection from the source by name.
	Get(name string) (Collection, error)

	// Close the source and discard memory.
	Close() error
}

// Collection represents an iterable collection of records.
type Collection interface {
	// Next advances to the next record of content.
	// It MUST be called prior to any Scan().
	Next() bool

	// Strings extracts values from the current record into a list of strings.
	Strings() []string

	// Types extracts the data types from the current record into a list.
	// options: "boolean", "integer", "float", "string", "date",
	// and special cases: "blank", "hyperlink" which are string types
	Types() []string

	// Formats extracts the format codes for the current record into a list.
	Formats() []string

	// Scan extracts values from the current record into the provided arguments
	// Arguments must be pointers to one of 5 supported types:
	//     bool, int64, float64, string, or time.Time
	// If invalid, returns ErrInvalidScanType
	Scan(args ...interface{}) error

	// IsEmpty returns true if there are no data values.
	IsEmpty() bool

	// Err returns the last error that occured.
	Err() error
}

// OpenFunc defines a Source's instantiation function.
// It should return ErrNotInFormat immediately if filename is not of the correct file type.
type OpenFunc func(filename string) (Source, error)

// Open a tabular data file and return a Source for accessing it's contents.
func Open(filename string) (Source, error) {
	for _, o := range srcTable {
		src, err := o.op(filename)
		if err == nil {
			return src, nil
		}
		if !errors.Is(err, ErrNotInFormat) {
			return nil, err
		}
		if Debug {
			log.Println(" ", filename, "is not in", o.name, "format")
		}
	}
	return nil, ErrUnknownFormat
}

type srcOpenTab struct {
	name string
	pri  int
	op   OpenFunc
}

var srcTable = make([]*srcOpenTab, 0, 20)

// Register the named source as a grate datasource implementation.
func Register(name string, priority int, opener OpenFunc) error {
	if Debug {
		log.Println("Registering the", name, "format at priority", priority)
	}
	srcTable = append(srcTable, &srcOpenTab{name: name, pri: priority, op: opener})
	sort.Slice(srcTable, func(i, j int) bool {
		return srcTable[i].pri < srcTable[j].pri
	})
	return nil
}

const (
	// ContinueColumnMerged marks a continuation column within a merged cell.
	ContinueColumnMerged = "→"
	// EndColumnMerged marks the last column of a merged cell.
	EndColumnMerged = "⇥"

	// ContinueRowMerged marks a continuation row within a merged cell.
	ContinueRowMerged = "↓"
	// EndRowMerged marks the last row of a merged cell.
	EndRowMerged = "⤓"
)


================================================
FILE: simple/csv.go
================================================
package simple

import (
	"encoding/csv"
	"os"

	"github.com/pbnjay/grate"
)

var _ = grate.Register("csv", 15, OpenCSV)

// OpenCSV defines a Source's instantiation function.
// It should return ErrNotInFormat immediately if filename is not of the correct file type.
func OpenCSV(filename string) (grate.Source, error) {
	f, err := os.Open(filename)
	if err != nil {
		return nil, err
	}
	defer f.Close()
	t := &simpleFile{
		filename: filename,
		iterRow:  -1,
	}

	s := csv.NewReader(f)
	s.FieldsPerRecord = -1

	total := 0
	ncols := make(map[int]int)
	rec, err := s.Read()
	for ; err == nil; rec, err = s.Read() {
		ncols[len(rec)]++
		total++
		t.rows = append(t.rows, rec)
	}
	if err != nil {
		switch perr := err.(type) {
		case *csv.ParseError:
			return nil, grate.WrapErr(perr, grate.ErrNotInFormat)
		}
		if total < 10 {
			// probably? not in this format
			return nil, grate.WrapErr(err, grate.ErrNotInFormat)
		}
		return nil, err
	}

	// kinda arbitrary metrics for detecting CSV
	looksGood := 0
	for c, n := range ncols {
		if c <= 1 {
			continue
		}
		if n > 10 && float64(n)/float64(total) > 0.8 {
			// more than 80% of rows have the same number of columns, we're good
			looksGood = 2
		} else if n > 25 && looksGood == 0 {
			looksGood = 1
		}
	}
	if looksGood == 1 {
		return t, grate.ErrNotInFormat
	}

	return t, nil
}


================================================
FILE: simple/simple.go
================================================
package simple

import (
	"errors"
	"fmt"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"github.com/pbnjay/grate"
)

// represents a set of data collections.
type simpleFile struct {
	filename string
	rows     [][]string
	iterRow  int
}

// List the individual data tables within this source.
func (t *simpleFile) List() ([]string, error) {
	return []string{filepath.Base(t.filename)}, nil
}

func (t *simpleFile) Close() error {
	return nil
}

// Get a Collection from the source by name.
func (t *simpleFile) Get(name string) (grate.Collection, error) {
	return t, nil
}

// Next advances to the next record of content.
// It MUST be called prior to any Scan().
func (t *simpleFile) Next() bool {
	t.iterRow++
	return t.iterRow < len(t.rows)
}

// Strings extracts values from the current record into a list of strings.
func (t *simpleFile) Strings() []string {
	return t.rows[t.iterRow]
}

// Formats extracts the format code for the current record into a list.
func (t *simpleFile) Formats() []string {
	res := make([]string, len(t.rows[t.iterRow]))
	for i := range res {
		res[i] = "General"
	}
	return res
}

// Types extracts the data types from the current record into a list.
// options: "boolean", "integer", "float", "string", "date",
// and special cases: "blank", "hyperlink" which are string types
func (t *simpleFile) Types() []string {
	res := make([]string, len(t.rows[t.iterRow]))
	for i, v := range t.rows[t.iterRow] {
		if v == "" {
			res[i] = "blank"
		} else {
			res[i] = "string"
		}
	}
	return res
}

// Scan extracts values from the current record into the provided arguments
// Arguments must be pointers to one of 5 supported types:
//     bool, int, float64, string, or time.Time
func (t *simpleFile) Scan(args ...interface{}) error {
	var err error
	row := t.rows[t.iterRow]
	if len(row) != len(args) {
		return fmt.Errorf("grate/simple: expected %d Scan destinations, got %d", len(row), len(args))
	}

	for i, a := range args {
		switch v := a.(type) {
		case *bool:
			switch strings.ToLower(row[i]) {
			case "1", "t", "true", "y", "yes":
				*v = true
			default:
				*v = false
			}
		case *int:
			var n int64
			n, err = strconv.ParseInt(row[i], 10, 64)
			*v = int(n)
		case *float64:
			*v, err = strconv.ParseFloat(row[i], 64)
		case *string:
			*v = row[i]
		case *time.Time:
			return errors.New("grate/simple: time.Time not supported, you must parse date strings manually")
		default:
			return grate.ErrInvalidScanType
		}
		if err != nil {
			return err
		}
	}
	return nil
}

// IsEmpty returns true if there are no data values.
func (t *simpleFile) IsEmpty() bool {
	return len(t.rows) == 0
}

// Err returns the last error that occured.
func (t *simpleFile) Err() error {
	return nil
}


================================================
FILE: simple/tsv.go
================================================
package simple

import (
	"bufio"
	"os"
	"strings"

	"github.com/pbnjay/grate"
)

var _ = grate.Register("tsv", 10, OpenTSV)

// OpenTSV defines a Source's instantiation function.
// It should return ErrNotInFormat immediately if filename is not of the correct file type.
func OpenTSV(filename string) (grate.Source, error) {
	f, err := os.Open(filename)
	if err != nil {
		return nil, err
	}
	defer f.Close()
	t := &simpleFile{
		filename: filename,
		iterRow:  -1,
	}

	s := bufio.NewScanner(f)
	total := 0
	ncols := make(map[int]int)
	for s.Scan() {
		r := strings.Split(s.Text(), "\t")
		ncols[len(r)]++
		total++
		t.rows = append(t.rows, r)
	}
	if s.Err() != nil {
		// this can only be read errors, not format
		return nil, s.Err()
	}

	// kinda arbitrary metrics for detecting TSV
	looksGood := 0
	for c, n := range ncols {
		if c <= 1 {
			continue
		}
		if n > 10 && float64(n)/float64(total) > 0.8 {
			// more than 80% of rows have the same number of columns, we're good
			looksGood = 2
		} else if n > 25 && looksGood == 0 {
			looksGood = 1
		}
	}
	if looksGood == 1 {
		return t, grate.ErrNotInFormat
	}

	return t, nil
}


================================================
FILE: xls/cfb/cfb.go
================================================
// Package cfb implements the Microsoft Compound File Binary File Format.
package cfb

// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-cfb/53989ce4-7b05-4f8d-829b-d08d6148375b
// Note for myself:
//   Storage = Directory
//   Stream = File

import (
	"bytes"
	"encoding/binary"
	"errors"
	"io"
	"io/ioutil"
	"log"
	"unicode/utf16"

	"github.com/pbnjay/grate"
)

const fullAssertions = true

const (
	secFree       uint32 = 0xFFFFFFFF // FREESECT
	secEndOfChain uint32 = 0xFFFFFFFE // ENDOFCHAIN
	secFAT        uint32 = 0xFFFFFFFD // FATSECT
	secDIFAT      uint32 = 0xFFFFFFFC // DIFSECT
	secReserved   uint32 = 0xFFFFFFFB
	secMaxRegular uint32 = 0xFFFFFFFA // MAXREGSECT
)

// Header of the Compound File MUST be at the beginning of the file (offset 0).
type header struct {
	Signature                    uint64      // Identification signature for the compound file structure, and MUST be set to the value 0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1.
	ClassID                      [2]uint64   // Reserved and unused class ID that MUST be set to all zeroes (CLSID_NULL).
	MinorVersion                 uint16      // Version number for nonbreaking changes. This field SHOULD be set to 0x003E if the major version field is either 0x0003 or 0x0004.
	MajorVersion                 uint16      // Version number for breaking changes. This field MUST be set to either 0x0003 (version 3) or 0x0004 (version 4).
	ByteOrder                    uint16      // This field MUST be set to 0xFFFE. This field is a byte order mark for all integer fields, specifying little-endian byte order.
	SectorShift                  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.
	MiniSectorShift              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.
	Reserved1                    [6]byte     // This field MUST be set to all zeroes.
	NumDirectorySectors          int32       // This integer field contains the count of the number of directory sectors in the compound file.
	NumFATSectors                int32       // This integer field contains the count of the number of FAT sectors in the compound file.
	FirstDirectorySectorLocation uint32      // This integer field contains the starting sector number for the directory stream.
	TransactionSignature         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>
	MiniStreamCutoffSize         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.
	FirstMiniFATSectorLocation   uint32      // This integer field contains the starting sector number for the mini FAT.
	NumMiniFATSectors            int32       // This integer field contains the count of the number of mini FAT sectors in the compound file.
	FirstDIFATSectorLocation     uint32      // This integer field contains the starting sector number for the DIFAT.
	NumDIFATSectors              int32       // This integer field contains the count of the number of DIFAT sectors in the compound file.
	DIFAT                        [109]uint32 // This array of 32-bit integer fields contains the first 109 FAT sector locations of the compound file.
}

type objectType byte

const (
	typeUnknown     objectType = 0x00
	typeStorage     objectType = 0x01
	typeStream      objectType = 0x02
	typeRootStorage objectType = 0x05
)

type directory struct {
	Name                   [32]uint16 // 32 utf16 characters
	NameByteLen            int16      // length of Name in bytes
	ObjectType             objectType
	ColorFlag              byte   // 0=red, 1=black
	LeftSiblingID          uint32 // stream ids
	RightSiblingID         uint32
	ChildID                uint32
	ClassID                [2]uint64 // GUID
	StateBits              uint32
	CreationTime           int64
	ModifiedTime           int64
	StartingSectorLocation int32
	StreamSize             uint64
}

func (d *directory) String() string {
	if (d.NameByteLen&1) == 1 || d.NameByteLen > 64 {
		return "<invalid utf16 string>"
	}
	r16 := utf16.Decode(d.Name[:int(d.NameByteLen)/2])
	// trim off null terminator
	return string(r16[:len(r16)-1])
}

// Document represents a Compound File Binary Format document.
type Document struct {
	// the entire file, loaded into memory
	data []byte

	// pre-parsed info
	header *header
	dir    []*directory

	// lookup tables for all the sectors
	fat     []uint32
	minifat []uint32

	ministreamstart uint32
	ministreamsize  uint32
}

func (d *Document) load(rx io.ReadSeeker) error {
	var err error
	d.data, err = ioutil.ReadAll(rx)
	if err != nil {
		return err
	}
	br := bytes.NewReader(d.data)

	h := &header{}
	err = binary.Read(br, binary.LittleEndian, h)
	if h.Signature != 0xe11ab1a1e011cfd0 {
		return grate.ErrNotInFormat // errors.New("ole2: invalid format")
	}
	if h.ByteOrder != 0xFFFE {
		return grate.ErrNotInFormat //errors.New("ole2: invalid format")
	}
	if fullAssertions {
		if h.ClassID[0] != 0 || h.ClassID[1] != 0 {
			return grate.ErrNotInFormat //errors.New("ole2: invalid CLSID")
		}
		if h.MajorVersion != 3 && h.MajorVersion != 4 {
			return errors.New("ole2: unknown major version")
		}
		if h.MinorVersion != 0x3B && h.MinorVersion != 0x3E {
			log.Printf("WARNING MinorVersion = 0x%02x NOT 0x3E", h.MinorVersion)
			//return errors.New("ole2: unknown minor version")
		}

		for _, v := range h.Reserved1 {
			if v != 0 {
				return errors.New("ole2: reserved section is non-zero")
			}
		}
		if h.MajorVersion == 3 {
			if h.SectorShift != 9 {
				return errors.New("ole2: invalid sector size")
			}
			if h.NumDirectorySectors != 0 {
				return errors.New("ole2: version 3 does not support directory sectors")
			}
		}
		if h.MajorVersion == 4 {
			if h.SectorShift != 12 {
				return errors.New("ole2: invalid sector size")
			}
		}
		if h.MiniSectorShift != 6 {
			return errors.New("ole2: invalid mini sector size")
		}
		if h.MiniStreamCutoffSize != 0x00001000 {
			return errors.New("ole2: invalid mini sector cutoff")
		}
	}
	d.header = h

	numFATentries := (1 << (h.SectorShift - 2))
	le := binary.LittleEndian
	d.fat = make([]uint32, 0, numFATentries*int(1+d.header.NumFATSectors))
	d.minifat = make([]uint32, 0, numFATentries*int(1+h.NumMiniFATSectors))

	// step 1: read the DIFAT sector list
	for i := 0; i < 109; i++ {
		sid := h.DIFAT[i]
		if sid == secFree {
			break
		}
		offs := int64(1+sid) << int32(h.SectorShift)
		if offs >= int64(len(d.data)) {
			return errors.New("xls/cfb: unable to load file")
		}
		sector := d.data[offs:]
		for j := 0; j < numFATentries; j++ {
			sid2 := le.Uint32(sector)
			d.fat = append(d.fat, sid2)
			sector = sector[4:]
		}
	}
	if h.NumDIFATSectors > 0 {
		sid1 := h.FirstDIFATSectorLocation

		for sid1 != secEndOfChain {
			offs := int64(1+sid1) << int32(h.SectorShift)
			difatSector := d.data[offs:]

			for i := 0; i < numFATentries-1; i++ {
				sid2 := le.Uint32(difatSector)
				if sid2 == secFree || sid2 == secEndOfChain {
					difatSector = difatSector[4:]
					continue
				}

				offs := int64(1+sid2) << int32(h.SectorShift)
				if offs >= int64(len(d.data)) {
					return errors.New("xls/cfb: unable to load file")
				}
				sector := d.data[offs:]
				for j := 0; j < numFATentries; j++ {
					sid3 := le.Uint32(sector)
					d.fat = append(d.fat, sid3)
					sector = sector[4:]
				}

				difatSector = difatSector[4:]
			}
			// chain the next DIFAT sector
			sid1 = le.Uint32(difatSector)
		}
	}

	// step 2: read the mini FAT
	sid := h.FirstMiniFATSectorLocation
	for sid != secEndOfChain {
		offs := int64(1+sid) << int32(h.SectorShift)
		if offs >= int64(len(d.data)) {
			return errors.New("xls/cfb: unable to load file")
		}
		sector := d.data[offs:]
		for j := 0; j < numFATentries; j++ {
			sid = le.Uint32(sector)
			d.minifat = append(d.minifat, sid)
			sector = sector[4:]
		}

		if len(d.minifat) >= int(h.NumMiniFATSectors) {
			break
		}

		// chain the next mini FAT sector
		sid = le.Uint32(sector)
	}

	// step 3: read the Directory Entries
	err = d.buildDirs(br)

	return err
}

func (d *Document) buildDirs(br *bytes.Reader) error {
	h := d.header
	le := binary.LittleEndian

	// step 2: read the Directory
	sid := h.FirstDirectorySectorLocation
	offs := int64(1+sid) << int64(h.SectorShift)
	br.Seek(offs, io.SeekStart)

	for j := 0; j < 4; j++ {
		dirent := &directory{}
		binary.Read(br, le, dirent)
		if d.header.MajorVersion == 3 {
			// mask out upper 32bits
			dirent.StreamSize = dirent.StreamSize & 0xFFFFFFFF
		}

		switch dirent.ObjectType {
		case typeRootStorage:
			d.ministreamstart = uint32(dirent.StartingSectorLocation)
			d.ministreamsize = uint32(dirent.StreamSize)
		case typeStorage:
			//log.Println("got a storage? what to do now?")
		case typeStream:
			/*
				var freader io.Reader
				if dirent.StreamSize < uint64(d.header.MiniStreamCutoffSize) {
					freader = d.getMiniStreamReader(uint32(dirent.StartingSectorLocation), dirent.StreamSize)
				} else if dirent.StreamSize != 0 {
					freader = d.getStreamReader(uint32(dirent.StartingSectorLocation), dirent.StreamSize)
				}
			*/
		case typeUnknown:
			return nil
		}
		d.dir = append(d.dir, dirent)
	}

	return nil
}

func (d *Document) getStreamReader(sid uint32, size uint64) (io.ReadSeeker, error) {
	// NB streamData is a slice of slices of the raw data, so this is the
	// only allocation - for the (much smaller) list of sector slices
	streamData := make([][]byte, 1+(size>>d.header.SectorShift))

	x := 0
	secSize := int64(1) << int32(d.header.SectorShift)
	for sid != secEndOfChain && sid != secFree {
		offs := int64(1+sid) << int64(d.header.SectorShift)
		if offs > int64(len(d.data)) {
			return nil, errors.New("ole2: corrupt data format")
		}
		slice := d.data[offs : offs+secSize]
		if size < uint64(len(slice)) {
			slice = slice[:size]
			size = 0
		} else {
			size -= uint64(len(slice))
		}
		streamData[x] = slice
		if size == 0 {
			break
		}
		sid = d.fat[sid]
		x++
	}
	if size != 0 {
		return nil, errors.New("ole2: incomplete read")
	}

	return &SliceReader{Data: streamData}, nil
}

func (d *Document) getMiniStreamReader(sid uint32, size uint64) (io.ReadSeeker, error) {
	// TODO: move into a separate cache so we don't recalculate it each time
	fatStreamData := make([][]byte, 1+(d.ministreamsize>>d.header.SectorShift))

	// NB streamData is a slice of slices of the raw data, so this is the
	// only allocation - for the (much smaller) list of sector slices
	streamData := make([][]byte, 1+(size>>d.header.MiniSectorShift))

	x := 0
	fsid := d.ministreamstart
	fsize := uint64(d.ministreamsize)
	secSize := int64(1) << int64(d.header.SectorShift)
	for fsid != secEndOfChain && fsid != secFree {
		offs := int64(1+fsid) << int64(d.header.SectorShift)
		slice := d.data[offs : offs+secSize]
		if fsize < uint64(len(slice)) {
			slice = slice[:fsize]
			fsize = 0
		} else {
			fsize -= uint64(len(slice))
		}
		fatStreamData[x] = slice
		x++
		fsid = d.fat[fsid]
	}

	x = 0
	miniSecSize := int64(1) << int64(d.header.MiniSectorShift)
	for sid != secEndOfChain && sid != secFree {
		offs := int64(sid) << int64(d.header.MiniSectorShift)

		so, si := offs/secSize, offs%secSize
		data := fatStreamData[so]

		slice := data[si : si+miniSecSize]
		if size < uint64(len(slice)) {
			slice = slice[:size]
			size = 0
		} else {
			size -= uint64(len(slice))
		}
		streamData[x] = slice
		x++
		sid = d.minifat[sid]
	}

	return &SliceReader{Data: streamData}, nil
}


================================================
FILE: xls/cfb/interface.go
================================================
package cfb

import (
	"fmt"
	"io"
	"os"
)

// Open a Compound File Binary Format document.
func Open(filename string) (*Document, error) {
	d := &Document{}
	f, err := os.Open(filename)
	if err != nil {
		return nil, err
	}
	err = d.load(f)
	if err != nil {
		return nil, err
	}
	return d, nil
}

// List the streams contained in the document.
func (d *Document) List() ([]string, error) {
	var res []string
	for _, e := range d.dir {
		if e.ObjectType == typeStream {
			res = append(res, e.String())
		}
	}
	return res, nil
}

// Open the named stream contained in the document.
func (d *Document) Open(name string) (io.ReadSeeker, error) {
	for _, e := range d.dir {
		if e.String() == name && e.ObjectType == typeStream {
			if e.StreamSize < uint64(d.header.MiniStreamCutoffSize) {
				return d.getMiniStreamReader(uint32(e.StartingSectorLocation), e.StreamSize)
			} else if e.StreamSize != 0 {
				return d.getStreamReader(uint32(e.StartingSectorLocation), e.StreamSize)
			}
		}
	}
	return nil, fmt.Errorf("cfb: stream '%s' not found", name)
}


================================================
FILE: xls/cfb/simple_test.go
================================================
package cfb

import (
	"io"
	"io/ioutil"
	"log"
	"os"
	"testing"
)

func TestHeader(t *testing.T) {
	d := &Document{}
	f, _ := os.Open("../../testdata/test.xls")
	err := d.load(f)
	if err != nil {
		t.Fatal(err)
	}
}

func TestHeader2(t *testing.T) {
	d := &Document{}
	f, _ := os.Open("../../testdata/test2.xls")
	err := d.load(f)
	if err != nil {
		t.Fatal(err)
	}
}

func TestHeader3(t *testing.T) {
	d := &Document{}
	f, _ := os.Open("../../testdata/test3.xls")
	err := d.load(f)
	if err != nil {
		t.Fatal(err)
	}
}

func TestHeader4(t *testing.T) {
	d := &Document{}
	f, _ := os.Open("../../testdata/test4.xls")
	err := d.load(f)
	if err != nil {
		t.Fatal(err)
	}

	log.Println(d.List())

	r, err := d.Open("Workbook")
	if err != nil {
		t.Fatal(err)
	}
	book, err := ioutil.ReadAll(r)
	if err != nil {
		t.Fatal(err)
	}
	log.Println(len(book))

	r, err = d.Open("\x05DocumentSummaryInformation")
	if err != nil {
		t.Fatal(err)
	}
	data, err := ioutil.ReadAll(r)
	if err != nil {
		t.Fatal(err)
	}
	log.Println(len(data))
}

var testSlices = [][]byte{
	{0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
	{10, 11, 12, 13, 14, 15, 16, 17, 18, 19},
	{20, 21, 22, 23, 24, 25, 26, 27, 28, 29},
	{30, 31, 32, 33, 34, 35, 36, 37, 38, 39},
	{40, 41, 42, 43, 44, 45, 46, 47, 48, 49},
}

func TestSliceReader(t *testing.T) {
	sr := &SliceReader{
		Data: testSlices,
	}
	var uno, old [1]byte
	_, err := sr.Read(uno[:])
	for err == nil {
		old[0] = uno[0]
		_, err = sr.Read(uno[:])
		if err == nil && uno[0] != (old[0]+1) {
			log.Printf("read data out of order new=%d, old=%d", old[0], uno[0])
			t.Fail()
		}
	}
	sr.Seek(0, io.SeekStart)
	_, err = sr.Read(uno[:])
	for err == nil {
		old[0] = uno[0]
		_, err = sr.Read(uno[:])
		if err == nil && uno[0] != (old[0]+1) {
			log.Printf("read data out of order new=%d, old=%d", old[0], uno[0])
			t.Fail()
		}
	}
	sr.Seek(10, io.SeekStart)
	_, err = sr.Read(uno[:])
	if uno[0] != 10 {
		log.Printf("unexpected element %d (expected %d)", uno[0], 10)
		t.Fail()
	}
	sr.Seek(35, io.SeekStart)
	_, err = sr.Read(uno[:])
	if uno[0] != 35 {
		log.Printf("unexpected element %d (expected %d)", uno[0], 35)
		t.Fail()
	}
	sr.Seek(7, io.SeekCurrent)
	_, err = sr.Read(uno[:])
	if uno[0] != 43 {
		log.Printf("unexpected element %d (expected %d)", uno[0], 43)
		t.Fail()
	}
	sr.Seek(-9, io.SeekCurrent)
	_, err = sr.Read(uno[:])
	if uno[0] != 35 {
		log.Printf("unexpected element %d (expected %d)", uno[0], 35)
		t.Fail()
	}
}


================================================
FILE: xls/cfb/slicereader.go
================================================
package cfb

import (
	"errors"
	"io"
)

// SliceReader wraps a list of slices as a io.ReadSeeker that
// can transparently merge them into a single coherent stream.
type SliceReader struct {
	CSize  []int64
	Data   [][]byte
	Index  uint
	Offset uint
}

// Read implements the io.Reader interface.
func (s *SliceReader) Read(b []byte) (int, error) {
	if s.Index >= uint(len(s.Data)) {
		return 0, io.EOF
	}
	n := copy(b, s.Data[s.Index][s.Offset:])
	if n > 0 {
		s.Offset += uint(n)
		if s.Offset == uint(len(s.Data[s.Index])) {
			s.Offset = 0
			s.Index++
		}
		return n, nil
	}

	return 0, io.EOF
}

var x io.Seeker

// Seek implements the io.Seeker interface.
func (s *SliceReader) Seek(offset int64, whence int) (int64, error) {
	if len(s.CSize) != len(s.Data) {
		// calculate the cumulative block size cache
		s.CSize = make([]int64, len(s.Data))
		sz := int64(0)
		for i, d := range s.Data {
			s.CSize[i] = sz
			sz += int64(len(d))
		}
	}
	if s.Index >= uint(len(s.CSize)) {
		s.Index = uint(len(s.CSize) - 1)
		s.Offset = uint(len(s.Data[s.Index]))
	}
	// current offset in stream
	trueOffset := int64(s.Offset) + s.CSize[int(s.Index)]
	if offset == 0 && whence == io.SeekCurrent {
		// just asking for current position
		return trueOffset, nil
	}

	switch whence {
	case io.SeekStart:
		if offset < 0 {
			return -1, errors.New("xls: invalid seek offset")
		}
		s.Index = 0
		s.Offset = 0
		trueOffset = 0

	case io.SeekEnd:
		if offset > 0 {
			return -1, errors.New("xls: invalid seek offset")
		}

		s.Index = uint(len(s.Data) - 1)
		s.Offset = uint(len(s.Data[s.Index]))
		trueOffset = int64(s.Offset) + s.CSize[s.Index]

	default:
		// current position already defined
	}

	wantOffset := offset + trueOffset
	for trueOffset != wantOffset {
		loOffset := s.CSize[int(s.Index)]
		hiOffset := s.CSize[int(s.Index)] + int64(len(s.Data[s.Index]))
		if wantOffset > loOffset && wantOffset < hiOffset {
			s.Offset = uint(wantOffset - loOffset)
			return wantOffset, nil
		}

		if trueOffset > wantOffset {
			s.Index--
			s.Offset = 0
			trueOffset = s.CSize[int(s.Index)]
		} else if trueOffset < wantOffset {
			s.Index++
			s.Offset = 0
			trueOffset = s.CSize[int(s.Index)]
		}
	}
	return wantOffset, nil
}


================================================
FILE: xls/comp_test.go
================================================
package xls

import (
	"os"
	"path/filepath"
	"strings"
	"testing"
)

func TestAllFiles(t *testing.T) {
	err := filepath.Walk("../testdata", func(p string, info os.FileInfo, err error) error {
		if info.IsDir() {
			return nil
		}
		if !strings.HasSuffix(info.Name(), ".xls") {
			return nil
		}
		wb, err := Open(p)
		if err != nil {
			return err
		}

		sheets, err := wb.List()
		if err != nil {
			return err
		}
		for _, s := range sheets {
			sheet, err := wb.Get(s)
			if err != nil {
				return err
			}

			for sheet.Next() {
				sheet.Strings()
			}
		}

		return wb.Close()
	})
	if err != nil {
		t.Fatal(err)
	}
}


================================================
FILE: xls/crypto/crypto.go
================================================
// Package crypto implements excel encryption algorithms from the
// MS-OFFCRYPTO design specs. Currently only standard/basic RC4
// "obfuscation" is supported.
package crypto

import (
	"bytes"
	"encoding/binary"
	"fmt"
)

// Decryptor describes methods to decrypt an excel sheet.
type Decryptor interface {
	// SetPassword for the decryption.
	SetPassword(password []byte)

	// Read implements the io.Reader interface.
	Read(p []byte) (n int, err error)

	// Write implements the io.Writer interface.
	Write(p []byte) (n int, err error)

	// Bytes returns the decrypted data.
	Bytes() []byte

	// Flush tells the decryptor to decrypt the latest block.
	Flush()

	// Reset the decryptor, and clear all written and readable data.
	Reset()
}

// Algorithms designed based on specs in MS-OFFCRYPTO:
// https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-offcrypto/3c34d72a-1a61-4b52-a893-196f9157f083

// Important notes from MS-XLS section 2.2.10:
// https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-xls/cd03cb5f-ca02-4934-a391-bb674cb8aa06

// When obfuscating or encrypting BIFF records in these streams the record type and
// record size components MUST NOT be obfuscated or encrypted.
// In addition the following records MUST NOT be obfuscated or encrypted:
// BOF (section 2.4.21), FilePass (section 2.4.117), UsrExcl (section 2.4.339),
// FileLock (section 2.4.116), InterfaceHdr (section 2.4.146), RRDInfo (section 2.4.227),
// and RRDHead (section 2.4.226). Additionally, the lbPlyPos field of the BoundSheet8
// record (section 2.4.28) MUST NOT be encrypted.

// For RC4 encryption and RC4 CryptoAPI encryption, the Unicode password string is used
// to generate the encryption key as specified in [MS-OFFCRYPTO] section 2.3.6.2 or
// [MS-OFFCRYPTO] section 2.3.5.2 depending on the RC4 algorithm used. The record data
// is then encrypted by the specific RC4 algorithm in 1024-byte blocks. The block number
// is set to zero at the beginning of every BIFF record stream, and incremented by one
// at each 1024-byte boundary. Bytes to be encrypted are passed into the RC4 encryption
// function and then written to the stream. For unencrypted records and the record
// headers consisting of the record type and record size, a byte buffer of all zeros,
// of the same size as the section of unencrypted bytes, is passed into the RC4
// encryption function. The results are then ignored and the unencrypted bytes are
// written to the stream.

// DefaultXLSPassword is the default encryption password defined by note
// <100> Section 2.4.191: If the value of the wPassword field of the Password record in
// the Globals Substream is not 0x0000, Excel 97, Excel 2000, Excel 2002, Office Excel
// 2003, Office Excel 2007, and Excel 2010 encrypt the document as specified in [MS-OFFCRYPTO],
// section 2.3. If an encryption password is not specified or the workbook or sheet is only
// protected, the document is encrypted with the default password of:

// DefaultXLSPassword is the default Excel encryption password.
var DefaultXLSPassword = "VelvetSweatshop"

/////////////

// 2.3.6.1
type basicRC4Encryption struct {
	MajorVersion uint16
	MinorVersion uint16
	Salt         [16]byte
	Verifier     [16]byte
	VerifierHash [16]byte
}

// NewBasicRC4 implements the standard RC4 decryption.
func NewBasicRC4(data []byte) (Decryptor, error) {
	h := basicRC4Encryption{}
	b := bytes.NewReader(data)
	err := binary.Read(b, binary.LittleEndian, &h)
	if err != nil {
		return nil, err
	}
	if h.MinorVersion != 1 {
		return nil, fmt.Errorf("xls: unknown basic-RC4 minor version %d (%d byte record)",
			h.MinorVersion, len(data))
	}
	if len(data) != 52 {
		return nil, fmt.Errorf("xls: data length is invalid (expected 52 bytes, got %d)",
			len(data))
	}

	d := &rc4Writer{
		Salt: make([]byte, len(h.Salt)),
	}
	copy(d.Salt, h.Salt[:])

	return d, d.Verify(h.Verifier[:], h.VerifierHash[:])
}


================================================
FILE: xls/crypto/rc4.go
================================================
package crypto

import (
	"bytes"
	"crypto/md5"
	"crypto/rc4"
	"encoding/binary"
	"fmt"
)

var _ Decryptor = &rc4Writer{}

func (d *rc4Writer) Write(data []byte) (n int, err error) {
	x := len(data)
	for len(data) > 0 {
		n := copy(d.bytes[d.offset:], data)
		d.offset += n
		if d.offset >= 1024 {
			if d.offset != 1024 {
				panic("invalid offset from write")
			}
			d.Flush()
		}
		data = data[n:]
	}
	return x, nil
}

func (d *rc4Writer) Read(data []byte) (n int, err error) {
	return d.buf.Read(data)
}

// Reset to block 0, and clear all written and readable data.
func (d *rc4Writer) Reset() {
	d.block = 0
	d.offset = 0
	d.buf.Reset()
}

// Flush tells the decryptor to decrypt the latest block.
func (d *rc4Writer) Flush() {
	var zeros [1024]byte

	endpad := 0
	if d.offset < 1024 {
		endpad = copy(d.bytes[d.offset:], zeros[:])
		d.offset += endpad
	}
	if d.offset != 1024 {
		panic("invalid offset fill")
	}

	// decrypt and write results to output buffer
	d.startBlock()
	d.dec.XORKeyStream(d.bytes[:], d.bytes[:])
	d.buf.Write(d.bytes[:1024-endpad])

	d.offset = 0
	d.block++
}

// SetPassword for the decryption.
func (d *rc4Writer) SetPassword(password []byte) {
	d.Password = make([]rune, len(password))
	for i, p := range password {
		d.Password[i] = rune(p)
	}

	/// compute the first part of the encryption key
	result := generateStd97Key(d.Password, d.Salt)
	d.encKey = make([]byte, len(result))
	copy(d.encKey, result)
}

type rc4Writer struct {
	block  uint32
	offset int
	bytes  [1024]byte

	// records the decrypted data
	buf bytes.Buffer

	///////

	// decrypter for RC4 content streams
	dec *rc4.Cipher

	cipherKey []byte // H1 per 2.3.6.2
	encKey    []byte // Hfinal per 2.3.6.2

	Salt     []byte
	Password []rune
}

func (d *rc4Writer) Bytes() []byte {
	return d.buf.Bytes()
}

func (d *rc4Writer) Verify(everifier, everifierHash []byte) error {
	d.Reset()
	d.startBlock()

	var temp1 [16]byte
	var temp2 [16]byte
	d.dec.XORKeyStream(temp1[:], everifier)
	d.dec.XORKeyStream(temp2[:], everifierHash)

	newhash := md5.Sum(temp1[:])
	for i, c := range newhash {
		if temp2[i] != c {
			return fmt.Errorf("verification failed")
		}
	}
	return nil
}

/////////////////////

func (d *rc4Writer) startBlock() {
	if d.encKey == nil {
		d.SetPassword([]byte(DefaultXLSPassword))
	}

	d.cipherKey = make([]byte, 16)
	copy(d.cipherKey, d.encKey[:5])
	binary.LittleEndian.PutUint32(d.cipherKey[5:], d.block)
	mhash := md5.Sum(d.cipherKey[:9])
	d.dec, _ = rc4.NewCipher(mhash[:])
}

func generateStd97Key(passData []rune, salt []byte) []byte {
	if len(passData) == 0 || len(salt) != 16 {
		panic("invalid keygen material")
	}

	passBytes := make([]byte, len(passData)*2)

	for i, c := range passData {
		binary.LittleEndian.PutUint16(passBytes[2*i:], uint16(c))
	}

	// digest the IV then copy back into pKeyData
	h0 := md5.Sum(passBytes)

	// now do the final set of keygen ops
	msum := md5.New()
	for i := 0; i < 16; i++ {
		msum.Write(h0[:5])
		msum.Write(salt)
	}
	// return H1
	temp := make([]byte, 0, 16)
	temp = msum.Sum(temp)
	return temp
}


================================================
FILE: xls/hyperlinks.go
================================================
package xls

import (
	"encoding/binary"
	"errors"
	"fmt"
	"strings"
	"unicode/utf16"
)

func decodeHyperlinks(raw []byte) (displayText, linkText string, err error) {
	raw = raw[16:] // skip classid
	slen := binary.LittleEndian.Uint32(raw[:4])
	if slen != 2 {
		return "", "", errors.New("xls: unknown hyperlink version")
	}

	flags := binary.LittleEndian.Uint32(raw[4:8])
	raw = raw[8:]
	if (flags & hlstmfHasDisplayName) != 0 {
		slen = binary.LittleEndian.Uint32(raw[:4])
		raw = raw[4:]
		us := make([]uint16, slen)
		for i := 0; i < int(slen); i++ {
			us[i] = binary.LittleEndian.Uint16(raw)
			raw = raw[2:]
		}
		displayText = string(utf16.Decode(us))
	}

	if (flags & hlstmfHasFrameName) != 0 {
		// skip a HyperlinkString containing target Frame
		slen = binary.LittleEndian.Uint32(raw[:4])
		raw = raw[4+(slen*2):]
	}

	if (flags & hlstmfHasMoniker) != 0 {
		if (flags & hlstmfMonikerSavedAsStr) != 0 {
			// read HyperlinkString containing the URL
			slen = binary.LittleEndian.Uint32(raw[:4])
			raw = raw[4:]
			us := make([]uint16, slen)
			for i := 0; i < int(slen); i++ {
				us[i] = binary.LittleEndian.Uint16(raw)
				raw = raw[2:]
			}
			linkText = string(utf16.Decode(us))

		} else {
			n := 0
			var err error
			linkText, n, err = parseHyperlinkMoniker(raw)
			raw = raw[n:]
			if err != nil {
				return "", "", err
			}
		}
	}

	if (flags & hlstmfHasLocationStr) != 0 {
		slen = binary.LittleEndian.Uint32(raw[:4])
		raw = raw[4:]
		us := make([]uint16, slen)
		for i := 0; i < int(slen); i++ {
			us[i] = binary.LittleEndian.Uint16(raw)
			raw = raw[2:]
		}
		linkText = string(utf16.Decode(us))
	}

	linkText = strings.Trim(linkText, " \v\f\t\r\n\x00")
	displayText = strings.Trim(displayText, " \v\f\t\r\n\x00")
	return
}

func parseHyperlinkMoniker(raw []byte) (string, int, error) {
	classid := raw[:16]
	no := 16

	isURLMoniker := true
	isFileMoniker := true
	urlMonikerClassID := [16]byte{0xE0, 0xC9, 0xEA, 0x79, 0xF9, 0xBA, 0xCE, 0x11, 0x8C, 0x82, 0x00, 0xAA, 0x00, 0x4B, 0xA9, 0x0B}
	fileMonikerClassID := [16]byte{0x03, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46}
	for i, b := range classid {
		if urlMonikerClassID[i] != b {
			isURLMoniker = false
		}
		if fileMonikerClassID[i] != b {
			isFileMoniker = false
		}
	}
	if isURLMoniker {
		length := binary.LittleEndian.Uint32(raw[no:])
		no += 4
		length /= 2
		buf := make([]uint16, length)
		for i := 0; i < int(length); i++ {
			buf[i] = binary.LittleEndian.Uint16(raw[no:])
			no += 2
		}
		if length > 12 && buf[length-13] == 0 {
			buf = buf[:length-12]
		}
		return string(utf16.Decode(buf)), no, nil
	}
	if isFileMoniker {
		//x := binary.LittleEndian.Uint16(raw[no:])        //cAnti
		length := binary.LittleEndian.Uint32(raw[no+2:]) //ansiLength
		no += 6
		buf := raw[no : no+int(length)]

		// skip 24 more bytes for misc fixed properties
		no += int(length) + 24

		length = binary.LittleEndian.Uint32(raw[no:]) // cbUnicodePathSize
		no += 4
		if length > 0 {
			no += 6
			length -= 6
			buf2 := make([]uint16, length/2)
			for i := 0; i < int(length/2); i++ {
				buf2[i] = binary.LittleEndian.Uint16(raw[no:])
				no += 2
			}
			return string(utf16.Decode(buf2)), no, nil
		}

		return string(buf), no, nil
	}

	return "", 0, fmt.Errorf("xls: unknown moniker classid")
}

// HLink flags
const (
	hlstmfHasMoniker          = uint32(0x001)
	hlstmfIsAbsolute          = uint32(0x002)
	hlstmfSiteGaveDisplayName = uint32(0x004)
	hlstmfHasLocationStr      = uint32(0x008)
	hlstmfHasDisplayName      = uint32(0x010)
	hlstmfHasGUID             = uint32(0x020)
	hlstmfHasCreationTime     = uint32(0x040)
	hlstmfHasFrameName        = uint32(0x080)
	hlstmfMonikerSavedAsStr   = uint32(0x100)
	hlstmfAbsFromGetdataRel   = uint32(0x200)
)


================================================
FILE: xls/records.go
================================================
package xls

import "fmt"

type recordType uint16

// Record types defined by the XLS specification document, section 2.3/2.4.
// https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-xls/43684742-8fcd-4fcd-92df-157d8d7241f9
const (
	RecTypeFormula              recordType = 6    // per section 2.4.127
	RecTypeEOF                  recordType = 10   // section 2.4.103
	RecTypeCalcCount            recordType = 12   // section 2.4.31
	RecTypeCalcMode             recordType = 13   // section 2.4.34
	RecTypeCalcPrecision        recordType = 14   // section 2.4.35
	RecTypeCalcRefMode          recordType = 15   // section 2.4.36
	RecTypeCalcDelta            recordType = 16   // section 2.4.32
	RecTypeCalcIter             recordType = 17   // section 2.4.33
	RecTypeProtect              recordType = 18   // section 2.4.207
	RecTypePassword             recordType = 19   // section 2.4.191
	RecTypeHeader               recordType = 20   // section 2.4.136
	RecTypeFooter               recordType = 21   // section 2.4.124
	RecTypeExternSheet          recordType = 23   // section 2.4.106
	RecTypeLbl                  recordType = 24   // section 2.4.150
	RecTypeWinProtect           recordType = 25   // section 2.4.347
	RecTypeVerticalPageBreaks   recordType = 26   // section 2.4.343
	RecTypeHorizontalPageBreaks recordType = 27   // section 2.4.142
	RecTypeNote                 recordType = 28   // section 2.4.179
	RecTypeSelection            recordType = 29   // section 2.4.248
	RecTypeDate1904             recordType = 34   // section 2.4.77
	RecTypeExternName           recordType = 35   // section 2.4.105
	RecTypeLeftMargin           recordType = 38   // section 2.4.151
	RecTypeRightMargin          recordType = 39   // section 2.4.219
	RecTypeTopMargin            recordType = 40   // section 2.4.328
	RecTypeBottomMargin         recordType = 41   // section 2.4.27
	RecTypePrintRowCol          recordType = 42   // section 2.4.203
	RecTypePrintGrid            recordType = 43   // section 2.4.202
	RecTypeFilePass             recordType = 47   // section 2.4.117
	RecTypeFont                 recordType = 49   // section 2.4.122
	RecTypePrintSize            recordType = 51   // section 2.4.204
	RecTypeContinue             recordType = 60   // section 2.4.58
	RecTypeWindow1              recordType = 61   // section 2.4.345
	RecTypeBackup               recordType = 64   // section 2.4.14
	RecTypePane                 recordType = 65   // section 2.4.189
	RecTypeCodePage             recordType = 66   // section 2.4.52
	RecTypePls                  recordType = 77   // section 2.4.199
	RecTypeDCon                 recordType = 80   // section 2.4.82
	RecTypeDConRef              recordType = 81   // section 2.4.86
	RecTypeDConName             recordType = 82   // section 2.4.85
	RecTypeDefColWidth          recordType = 85   // section 2.4.89
	RecTypeXCT                  recordType = 89   // section 2.4.352
	RecTypeCRN                  recordType = 90   // section 2.4.65
	RecTypeFileSharing          recordType = 91   // section 2.4.118
	RecTypeWriteAccess          recordType = 92   // section 2.4.349
	RecTypeObj                  recordType = 93   // section 2.4.181
	RecTypeUncalced             recordType = 94   // section 2.4.331
	RecTypeCalcSaveRecalc       recordType = 95   // section 2.4.37
	RecTypeTemplate             recordType = 96   // section 2.4.323
	RecTypeIntl                 recordType = 97   // section 2.4.147
	RecTypeObjProtect           recordType = 99   // section 2.4.183
	RecTypeColInfo              recordType = 125  // section 2.4.53
	RecTypeGuts                 recordType = 128  // section 2.4.134
	RecTypeWsBool               recordType = 129  // section 2.4.351
	RecTypeGridSet              recordType = 130  // section 2.4.132
	RecTypeHCenter              recordType = 131  // section 2.4.135
	RecTypeVCenter              recordType = 132  // section 2.4.342
	RecTypeBoundSheet8          recordType = 133  // section 2.4.28
	RecTypeWriteProtect         recordType = 134  // section 2.4.350
	RecTypeCountry              recordType = 140  // section 2.4.63
	RecTypeHideObj              recordType = 141  // section 2.4.139
	RecTypeSort                 recordType = 144  // section 2.4.263
	RecTypePalette              recordType = 146  // section 2.4.188
	RecTypeSync                 recordType = 151  // section 2.4.318
	RecTypeLPr                  recordType = 152  // section 2.4.158
	RecTypeDxGCol               recordType = 153  // section 2.4.98
	RecTypeFnGroupName          recordType = 154  // section 2.4.120
	RecTypeFilterMode           recordType = 155  // section 2.4.119
	RecTypeBuiltInFnGroupCount  recordType = 156  // section 2.4.30
	RecTypeAutoFilterInfo       recordType = 157  // section 2.4.8
	RecTypeAutoFilter           recordType = 158  // section 2.4.6
	RecTypeScl                  recordType = 160  // section 2.4.247
	RecTypeSetup                recordType = 161  // section 2.4.257
	RecTypeScenMan              recordType = 174  // section 2.4.246
	RecTypeSCENARIO             recordType = 175  // section 2.4.244
	RecTypeSxView               recordType = 176  // section 2.4.313
	RecTypeSxvd                 recordType = 177  // section 2.4.309
	RecTypeSXVI                 recordType = 178  // section 2.4.312
	RecTypeSxIvd                recordType = 180  // section 2.4.292
	RecTypeSXLI                 recordType = 181  // section 2.4.293
	RecTypeSXPI                 recordType = 182  // section 2.4.298
	RecTypeDocRoute             recordType = 184  // section 2.4.91
	RecTypeRecipName            recordType = 185  // section 2.4.216
	RecTypeMulRk                recordType = 189  // section 2.4.175
	RecTypeMulBlank             recordType = 190  // section 2.4.174
	RecTypeMms                  recordType = 193  // section 2.4.169
	RecTypeSXDI                 recordType = 197  // section 2.4.278
	RecTypeSXDB                 recordType = 198  // section 2.4.275
	RecTypeSXFDB                recordType = 199  // section 2.4.283
	RecTypeSXDBB                recordType = 200  // section 2.4.276
	RecTypeSXNum                recordType = 201  // section 2.4.296
	RecTypeSxBool               recordType = 202  // section 2.4.274
	RecTypeSxErr                recordType = 203  // section 2.4.281
	RecTypeSXInt                recordType = 204  // section 2.4.289
	RecTypeSXString             recordType = 205  // section 2.4.304
	RecTypeSXDtr                recordType = 206  // section 2.4.279
	RecTypeSxNil                recordType = 207  // section 2.4.295
	RecTypeSXTbl                recordType = 208  // section 2.4.305
	RecTypeSXTBRGIITM           recordType = 209  // section 2.4.307
	RecTypeSxTbpg               recordType = 210  // section 2.4.306
	RecTypeObProj               recordType = 211  // section 2.4.185
	RecTypeSXStreamID           recordType = 213  // section 2.4.303
	RecTypeDBCell               recordType = 215  // section 2.4.78
	RecTypeSXRng                recordType = 216  // section 2.4.300
	RecTypeSxIsxoper            recordType = 217  // section 2.4.290
	RecTypeBookBool             recordType = 218  // section 2.4.22
	RecTypeDbOrParamQry         recordType = 220  // section 2.4.79
	RecTypeScenarioProtect      recordType = 221  // section 2.4.245
	RecTypeOleObjectSize        recordType = 222  // section 2.4.187
	RecTypeXF                   recordType = 224  // section 2.4.353
	RecTypeInterfaceHdr         recordType = 225  // section 2.4.146
	RecTypeInterfaceEnd         recordType = 226  // section 2.4.145
	RecTypeSXVS                 recordType = 227  // section 2.4.317
	RecTypeMergeCells           recordType = 229  // section 2.4.168
	RecTypeBkHim                recordType = 233  // section 2.4.19
	RecTypeMsoDrawingGroup      recordType = 235  // section 2.4.171
	RecTypeMsoDrawing           recordType = 236  // section 2.4.170
	RecTypeMsoDrawingSelection  recordType = 237  // section 2.4.172
	RecTypePhoneticInfo         recordType = 239  // section 2.4.192
	RecTypeSxRule               recordType = 240  // section 2.4.301
	RecTypeSXEx                 recordType = 241  // section 2.4.282
	RecTypeSxFilt               recordType = 242  // section 2.4.285
	RecTypeSxDXF                recordType = 244  // section 2.4.280
	RecTypeSxItm                recordType = 245  // section 2.4.291
	RecTypeSxName               recordType = 246  // section 2.4.294
	RecTypeSxSelect             recordType = 247  // section 2.4.302
	RecTypeSXPair               recordType = 248  // section 2.4.297
	RecTypeSxFmla               recordType = 249  // section 2.4.286
	RecTypeSxFormat             recordType = 251  // section 2.4.287
	RecTypeSST                  recordType = 252  // section 2.4.265
	RecTypeLabelSst             recordType = 253  // section 2.4.149
	RecTypeExtSST               recordType = 255  // section 2.4.107
	RecTypeSXVDEx               recordType = 256  // section 2.4.310
	RecTypeSXFormula            recordType = 259  // section 2.4.288
	RecTypeSXDBEx               recordType = 290  // section 2.4.277
	RecTypeRRDInsDel            recordType = 311  // section 2.4.228
	RecTypeRRDHead              recordType = 312  // section 2.4.226
	RecTypeRRDChgCell           recordType = 315  // section 2.4.223
	RecTypeRRTabID              recordType = 317  // section 2.4.241
	RecTypeRRDRenSheet          recordType = 318  // section 2.4.234
	RecTypeRRSort               recordType = 319  // section 2.4.240
	RecTypeRRDMove              recordType = 320  // section 2.4.231
	RecTypeRRFormat             recordType = 330  // section 2.4.238
	RecTypeRRAutoFmt            recordType = 331  // section 2.4.222
	RecTypeRRInsertSh           recordType = 333  // section 2.4.239
	RecTypeRRDMoveBegin         recordType = 334  // section 2.4.232
	RecTypeRRDMoveEnd           recordType = 335  // section 2.4.233
	RecTypeRRDInsDelBegin       recordType = 336  // section 2.4.229
	RecTypeRRDInsDelEnd         recordType = 337  // section 2.4.230
	RecTypeRRDConflict          recordType = 338  // section 2.4.224
	RecTypeRRDDefName           recordType = 339  // section 2.4.225
	RecTypeRRDRstEtxp           recordType = 340  // section 2.4.235
	RecTypeLRng                 recordType = 351  // section 2.4.159
	RecTypeUsesELFs             recordType = 352  // section 2.4.337
	RecTypeDSF                  recordType = 353  // section 2.4.94
	RecTypeCUsr                 recordType = 401  // section 2.4.72
	RecTypeCbUsr                recordType = 402  // section 2.4.40
	RecTypeUsrInfo              recordType = 403  // section 2.4.340
	RecTypeUsrExcl              recordType = 404  // section 2.4.339
	RecTypeFileLock             recordType = 405  // section 2.4.116
	RecTypeRRDInfo              recordType = 406  // section 2.4.227
	RecTypeBCUsrs               recordType = 407  // section 2.4.16
	RecTypeUsrChk               recordType = 408  // section 2.4.338
	RecTypeUserBView            recordType = 425  // section 2.4.333
	RecTypeUserSViewBegin       recordType = 426  // section 2.4.334
	RecTypeUserSViewBeginChart  recordType = 426  // section 2.4.335
	RecTypeUserSViewEnd         recordType = 427  // section 2.4.336
	RecTypeRRDUserView          recordType = 428  // section 2.4.237
	RecTypeQsi                  recordType = 429  // section 2.4.208
	RecTypeSupBook              recordType = 430  // section 2.4.271
	RecTypeProt4Rev             recordType = 431  // section 2.4.205
	RecTypeCondFmt              recordType = 432  // section 2.4.56
	RecTypeCF                   recordType = 433  // section 2.4.42
	RecTypeDVal                 recordType = 434  // section 2.4.96
	RecTypeDConBin              recordType = 437  // section 2.4.83
	RecTypeTxO                  recordType = 438  // section 2.4.329
	RecTypeRefreshAll           recordType = 439  // section 2.4.217
	RecTypeHLink                recordType = 440  // section 2.4.140
	RecTypeLel                  recordType = 441  // section 2.4.154
	RecTypeCodeName             recordType = 442  // section 2.4.51
	RecTypeSXFDBType            recordType = 443  // section 2.4.284
	RecTypeProt4RevPass         recordType = 444  // section 2.4.206
	RecTypeObNoMacros           recordType = 445  // section 2.4.184
	RecTypeDv                   recordType = 446  // section 2.4.95
	RecTypeExcel9File           recordType = 448  // section 2.4.104
	RecTypeRecalcID             recordType = 449  // section 2.4.215
	RecTypeEntExU2              recordType = 450  // section 2.4.102
	RecTypeDimensions           recordType = 512  // section 2.4.90
	RecTypeBlank                recordType = 513  // section 2.4.20
	RecTypeNumber               recordType = 515  // section 2.4.180
	RecTypeLabel                recordType = 516  // section 2.4.148
	RecTypeBoolErr              recordType = 517  // section 2.4.24
	RecTypeString               recordType = 519  // section 2.4.268
	RecTypeRow                  recordType = 520  // section 2.4.221
	RecTypeIndex                recordType = 523  // section 2.4.144
	RecTypeArray                recordType = 545  // section 2.4.4
	RecTypeDefaultRowHeight     recordType = 549  // section 2.4.87
	RecTypeTable                recordType = 566  // section 2.4.319
	RecTypeWindow2              recordType = 574  // section 2.4.346
	RecTypeRK                   recordType = 638  // section 2.4.220
	RecTypeStyle                recordType = 659  // section 2.4.269
	RecTypeBigName              recordType = 1048 // section 2.4.18
	RecTypeFormat               recordType = 1054 // section 2.4.126
	RecTypeContinueBigName      recordType = 1084 // section 2.4.59
	RecTypeShrFmla              recordType = 1212 // section 2.4.260
	RecTypeHLinkTooltip         recordType = 2048 // section 2.4.141
	RecTypeWebPub               recordType = 2049 // section 2.4.344
	RecTypeQsiSXTag             recordType = 2050 // section 2.4.211
	RecTypeDBQueryExt           recordType = 2051 // section 2.4.81
	RecTypeExtString            recordType = 2052 // section 2.4.108
	RecTypeTxtQry               recordType = 2053 // section 2.4.330
	RecTypeQsir                 recordType = 2054 // section 2.4.210
	RecTypeQsif                 recordType = 2055 // section 2.4.209
	RecTypeRRDTQSIF             recordType = 2056 // section 2.4.236
	RecTypeBOF                  recordType = 2057 // section 2.4.21
	RecTypeOleDbConn            recordType = 2058 // section 2.4.186
	RecTypeWOpt                 recordType = 2059 // section 2.4.348
	RecTypeSXViewEx             recordType = 2060 // section 2.4.314
	RecTypeSXTH                 recordType = 2061 // section 2.4.308
	RecTypeSXPIEx               recordType = 2062 // section 2.4.299
	RecTypeSXVDTEx              recordType = 2063 // section 2.4.311
	RecTypeSXViewEx9            recordType = 2064 // section 2.4.315
	RecTypeContinueFrt          recordType = 2066 // section 2.4.60
	RecTypeRealTimeData         recordType = 2067 // section 2.4.214
	RecTypeChartFrtInfo         recordType = 2128 // section 2.4.49
	RecTypeFrtWrapper           recordType = 2129 // section 2.4.130
	RecTypeStartBlock           recordType = 2130 // section 2.4.266
	RecTypeEndBlock             recordType = 2131 // section 2.4.100
	RecTypeStartObject          recordType = 2132 // section 2.4.267
	RecTypeEndObject            recordType = 2133 // section 2.4.101
	RecTypeCatLab               recordType = 2134 // section 2.4.38
	RecTypeYMult                recordType = 2135 // section 2.4.356
	RecTypeSXViewLink           recordType = 2136 // section 2.4.316
	RecTypePivotChartBits       recordType = 2137 // section 2.4.196
	RecTypeFrtFontList          recordType = 2138 // section 2.4.129
	RecTypeSheetExt             recordType = 2146 // section 2.4.259
	RecTypeBookExt              recordType = 2147 // section 2.4.23
	RecTypeSXAddl               recordType = 2148 // section 2.4.273.2
	RecTypeCrErr                recordType = 2149 // section 2.4.64
	RecTypeHFPicture            recordType = 2150 // section 2.4.138
	RecTypeFeatHdr              recordType = 2151 // section 2.4.112
	RecTypeFeat                 recordType = 2152 // section 2.4.111
	RecTypeDataLabExt           recordType = 2154 // section 2.4.75
	RecTypeDataLabExtContents   recordType = 2155 // section 2.4.76
	RecTypeCellWatch            recordType = 2156 // section 2.4.41
	RecTypeFeatHdr11            recordType = 2161 // section 2.4.113
	RecTypeFeature11            recordType = 2162 // section 2.4.114
	RecTypeDropDownObjIds       recordType = 2164 // section 2.4.93
	RecTypeContinueFrt11        recordType = 2165 // section 2.4.61
	RecTypeDConn                recordType = 2166 // section 2.4.84
	RecTypeList12               recordType = 2167 // section 2.4.157
	RecTypeFeature12            recordType = 2168 // section 2.4.115
	RecTypeCondFmt12            recordType = 2169 // section 2.4.57
	RecTypeCF12                 recordType = 2170 // section 2.4.43
	RecTypeCFEx                 recordType = 2171 // section 2.4.44
	RecTypeXFCRC                recordType = 2172 // section 2.4.354
	RecTypeXFExt                recordType = 2173 // section 2.4.355
	RecTypeAutoFilter12         recordType = 2174 // section 2.4.7
	RecTypeContinueFrt12        recordType = 2175 // section 2.4.62
	RecTypeMDTInfo              recordType = 2180 // section 2.4.162
	RecTypeMDXStr               recordType = 2181 // section 2.4.166
	RecTypeMDXTuple             recordType = 2182 // section 2.4.167
	RecTypeMDXSet               recordType = 2183 // section 2.4.165
	RecTypeMDXProp              recordType = 2184 // section 2.4.164
	RecTypeMDXKPI               recordType = 2185 // section 2.4.163
	RecTypeMDB                  recordType = 2186 // section 2.4.161
	RecTypePLV                  recordType = 2187 // section 2.4.200
	RecTypeCompat12             recordType = 2188 // section 2.4.54
	RecTypeDXF                  recordType = 2189 // section 2.4.97
	RecTypeTableStyles          recordType = 2190 // section 2.4.322
	RecTypeTableStyle           recordType = 2191 // section 2.4.320
	RecTypeTableStyleElement    recordType = 2192 // section 2.4.321
	RecTypeStyleExt             recordType = 2194 // section 2.4.270
	RecTypeNamePublish          recordType = 2195 // section 2.4.178
	RecTypeNameCmt              recordType = 2196 // section 2.4.176
	RecTypeSortData             recordType = 2197 // section 2.4.264
	RecTypeTheme                recordType = 2198 // section 2.4.326
	RecTypeGUIDTypeLib          recordType = 2199 // section 2.4.133
	RecTypeFnGrp12              recordType = 2200 // section 2.4.121
	RecTypeNameFnGrp12          recordType = 2201 // section 2.4.177
	RecTypeMTRSettings          recordType = 2202 // section 2.4.173
	RecTypeCompressPictures     recordType = 2203 // section 2.4.55
	RecTypeHeaderFooter         recordType = 2204 // section 2.4.137
	RecTypeCrtLayout12          recordType = 2205 // section 2.4.66
	RecTypeCrtMlFrt             recordType = 2206 // section 2.4.70
	RecTypeCrtMlFrtContinue     recordType = 2207 // section 2.4.71
	RecTypeForceFullCalculation recordType = 2211 // section 2.4.125
	RecTypeShapePropsStream     recordType = 2212 // section 2.4.258
	RecTypeTextPropsStream      recordType = 2213 // section 2.4.325
	RecTypeRichTextStream       recordType = 2214 // section 2.4.218
	RecTypeCrtLayout12A         recordType = 2215 // section 2.4.67
	RecTypeUnits                recordType = 4097 // section 2.4.332
	RecTypeChart                recordType = 4098 // section 2.4.45
	RecTypeSeries               recordType = 4099 // section 2.4.252
	RecTypeDataFormat           recordType = 4102 // section 2.4.74
	RecTypeLineFormat           recordType = 4103 // section 2.4.156
	RecTypeMarkerFormat         recordType = 4105 // section 2.4.160
	RecTypeAreaFormat           recordType = 4106 // section 2.4.3
	RecTypePieFormat            recordType = 4107 // section 2.4.195
	RecTypeAttachedLabel        recordType = 4108 // section 2.4.5
	RecTypeSeriesText           recordType = 4109 // section 2.4.254
	RecTypeChartFormat          recordType = 4116 // section 2.4.48
	RecTypeLegend               recordType = 4117 // section 2.4.152
	RecTypeSeriesList           recordType = 4118 // section 2.4.253
	RecTypeBar                  recordType = 4119 // section 2.4.15
	RecTypeLine                 recordType = 4120 // section 2.4.155
	RecTypePie                  recordType = 4121 // section 2.4.194
	RecTypeArea                 recordType = 4122 // section 2.4.2
	RecTypeScatter              recordType = 4123 // section 2.4.243
	RecTypeCrtLine              recordType = 4124 // section 2.4.68
	RecTypeAxis                 recordType = 4125 // section 2.4.11
	RecTypeTick                 recordType = 4126 // section 2.4.327
	RecTypeValueRange           recordType = 4127 // section 2.4.341
	RecTypeCatSerRange          recordType = 4128 // section 2.4.39
	RecTypeAxisLine             recordType = 4129 // section 2.4.12
	RecTypeCrtLink              recordType = 4130 // section 2.4.69
	RecTypeDefaultText          recordType = 4132 // section 2.4.88
	RecTypeText                 recordType = 4133 // section 2.4.324
	RecTypeFontX                recordType = 4134 // section 2.4.123
	RecTypeObjectLink           recordType = 4135 // section 2.4.182
	RecTypeFrame                recordType = 4146 // section 2.4.128
	RecTypeBegin                recordType = 4147 // section 2.4.17
	RecTypeEnd                  recordType = 4148 // section 2.4.99
	RecTypePlotArea             recordType = 4149 // section 2.4.197
	RecTypeChart3d              recordType = 4154 // section 2.4.46
	RecTypePicF                 recordType = 4156 // section 2.4.193
	RecTypeDropBar              recordType = 4157 // section 2.4.92
	RecTypeRadar                recordType = 4158 // section 2.4.212
	RecTypeSurf                 recordType = 4159 // section 2.4.272
	RecTypeRadarArea            recordType = 4160 // section 2.4.213
	RecTypeAxisParent           recordType = 4161 // section 2.4.13
	RecTypeLegendException      recordType = 4163 // section 2.4.153(
	RecTypeShtProps             recordType = 4164 // section 2.4.261
	RecTypeSerToCrt             recordType = 4165 // section 2.4.256
	RecTypeAxesUsed             recordType = 4166 // section 2.4.10
	RecTypeSBaseRef             recordType = 4168 // section 2.4.242
	RecTypeSerParent            recordType = 4170 // section 2.4.255
	RecTypeSerAuxTrend          recordType = 4171 // section 2.4.250
	RecTypeIFmtRecord           recordType = 4174 // section 2.4.143
	RecTypePos                  recordType = 4175 // section 2.4.201
	RecTypeAlRuns               recordType = 4176 // section 2.4.1
	RecTypeBRAI                 recordType = 4177 // section 2.4.29
	RecTypeSerAuxErrBar         recordType = 4187 // section 2.4.249
	RecTypeClrtClient           recordType = 4188 // section 2.4.50
	RecTypeSerFmt               recordType = 4189 // section 2.4.251
	RecTypeChart3DBarShape      recordType = 4191 // section 2.4.47
	RecTypeFbi                  recordType = 4192 // section 2.4.109
	RecTypeBopPop               recordType = 4193 // section 2.4.25
	RecTypeAxcExt               recordType = 4194 // section 2.4.9
	RecTypeDat                  recordType = 4195 // section 2.4.73
	RecTypePlotGrowth           recordType = 4196 // section 2.4.198
	RecTypeSIIndex              recordType = 4197 // section 2.4.262
	RecTypeGelFrame             recordType = 4198 // section 2.4.131
	RecTypeBopPopCustom         recordType = 4199 // section 2.4.26
	RecTypeFbi2                 recordType = 4200 // section 2.4.110
)

func (r recordType) String() string {
	switch r {
	case RecTypeFormula:
		return "Formula (6)"
	case RecTypeEOF:
		return "EOF (10)"
	case RecTypeCalcCount:
		return "CalcCount (12)"
	case RecTypeCalcMode:
		return "CalcMode (13)"
	case RecTypeCalcPrecision:
		return "CalcPrecision (14)"
	case RecTypeCalcRefMode:
		return "CalcRefMode (15)"
	case RecTypeCalcDelta:
		return "CalcDelta (16)"
	case RecTypeCalcIter:
		return "CalcIter (17)"
	case RecTypeProtect:
		return "Protect (18)"
	case RecTypePassword:
		return "Password (19)"
	case RecTypeHeader:
		return "Header (20)"
	case RecTypeFooter:
		return "Footer (21)"
	case RecTypeExternSheet:
		return "ExternSheet (23)"
	case RecTypeLbl:
		return "Lbl (24)"
	case RecTypeWinProtect:
		return "WinProtect (25)"
	case RecTypeVerticalPageBreaks:
		return "VerticalPageBreaks (26)"
	case RecTypeHorizontalPageBreaks:
		return "HorizontalPageBreaks (27)"
	case RecTypeNote:
		return "Note (28)"
	case RecTypeSelection:
		return "Selection (29)"
	case RecTypeDate1904:
		return "Date1904 (34)"
	case RecTypeExternName:
		return "ExternName (35)"
	case RecTypeLeftMargin:
		return "LeftMargin (38)"
	case RecTypeRightMargin:
		return "RightMargin (39)"
	case RecTypeTopMargin:
		return "TopMargin (40)"
	case RecTypeBottomMargin:
		return "BottomMargin (41)"
	case RecTypePrintRowCol:
		return "PrintRowCol (42)"
	case RecTypePrintGrid:
		return "PrintGrid (43)"
	case RecTypeFilePass:
		return "FilePass (47)"
	case RecTypeFont:
		return "Font (49)"
	case RecTypePrintSize:
		return "PrintSize (51)"
	case RecTypeContinue:
		return "Continue (60)"
	case RecTypeWindow1:
		return "Window1 (61)"
	case RecTypeBackup:
		return "Backup (64)"
	case RecTypePane:
		return "Pane (65)"
	case RecTypeCodePage:
		return "CodePage (66)"
	case RecTypePls:
		return "Pls (77)"
	case RecTypeDCon:
		return "DCon (80)"
	case RecTypeDConRef:
		return "DConRef (81)"
	case RecTypeDConName:
		return "DConName (82)"
	case RecTypeDefColWidth:
		return "DefColWidth (85)"
	case RecTypeXCT:
		return "XCT (89)"
	case RecTypeCRN:
		return "CRN (90)"
	case RecTypeFileSharing:
		return "FileSharing (91)"
	case RecTypeWriteAccess:
		return "WriteAccess (92)"
	case RecTypeObj:
		return "Obj (93)"
	case RecTypeUncalced:
		return "Uncalced (94)"
	case RecTypeCalcSaveRecalc:
		return "CalcSaveRecalc (95)"
	case RecTypeTemplate:
		return "Template (96)"
	case RecTypeIntl:
		return "Intl (97)"
	case RecTypeObjProtect:
		return "ObjProtect (99)"
	case RecTypeColInfo:
		return "ColInfo (125)"
	case RecTypeGuts:
		return "Guts (128)"
	case RecTypeWsBool:
		return "WsBool (129)"
	case RecTypeGridSet:
		return "GridSet (130)"
	case RecTypeHCenter:
		return "HCenter (131)"
	case RecTypeVCenter:
		return "VCenter (132)"
	case RecTypeBoundSheet8:
		return "BoundSheet8 (133)"
	case RecTypeWriteProtect:
		return "WriteProtect (134)"
	case RecTypeCountry:
		return "Country (140)"
	case RecTypeHideObj:
		return "HideObj (141)"
	case RecTypeSort:
		return "Sort (144)"
	case RecTypePalette:
		return "Palette (146)"
	case RecTypeSync:
		return "Sync (151)"
	case RecTypeLPr:
		return "LPr (152)"
	case RecTypeDxGCol:
		return "DxGCol (153)"
	case RecTypeFnGroupName:
		return "FnGroupName (154)"
	case RecTypeFilterMode:
		return "FilterMode (155)"
	case RecTypeBuiltInFnGroupCount:
		return "BuiltInFnGroupCount (156)"
	case RecTypeAutoFilterInfo:
		return "AutoFilterInfo (157)"
	case RecTypeAutoFilter:
		return "AutoFilter (158)"
	case RecTypeScl:
		return "Scl (160)"
	case RecTypeSetup:
		return "Setup (161)"
	case RecTypeScenMan:
		return "ScenMan (174)"
	case RecTypeSCENARIO:
		return "SCENARIO (175)"
	case RecTypeSxView:
		return "SxView (176)"
	case RecTypeSxvd:
		return "Sxvd (177)"
	case RecTypeSXVI:
		return "SXVI (178)"
	case RecTypeSxIvd:
		return "SxIvd (180)"
	case RecTypeSXLI:
		return "SXLI (181)"
	case RecTypeSXPI:
		return "SXPI (182)"
	case RecTypeDocRoute:
		return "DocRoute (184)"
	case RecTypeRecipName:
		return "RecipName (185)"
	case RecTypeMulRk:
		return "MulRk (189)"
	case RecTypeMulBlank:
		return "MulBlank (190)"
	case RecTypeMms:
		return "Mms (193)"
	case RecTypeSXDI:
		return "SXDI (197)"
	case RecTypeSXDB:
		return "SXDB (198)"
	case RecTypeSXFDB:
		return "SXFDB (199)"
	case RecTypeSXDBB:
		return "SXDBB (200)"
	case RecTypeSXNum:
		return "SXNum (201)"
	case RecTypeSxBool:
		return "SxBool (202)"
	case RecTypeSxErr:
		return "SxErr (203)"
	case RecTypeSXInt:
		return "SXInt (204)"
	case RecTypeSXString:
		return "SXString (205)"
	case RecTypeSXDtr:
		return "SXDtr (206)"
	case RecTypeSxNil:
		return "SxNil (207)"
	case RecTypeSXTbl:
		return "SXTbl (208)"
	case RecTypeSXTBRGIITM:
		return "SXTBRGIITM (209)"
	case RecTypeSxTbpg:
		return "SxTbpg (210)"
	case RecTypeObProj:
		return "ObProj (211)"
	case RecTypeSXStreamID:
		return "SXStreamID (213)"
	case RecTypeDBCell:
		return "DBCell (215)"
	case RecTypeSXRng:
		return "SXRng (216)"
	case RecTypeSxIsxoper:
		return "SxIsxoper (217)"
	case RecTypeBookBool:
		return "BookBool (218)"
	case RecTypeDbOrParamQry:
		return "DbOrParamQry (220)"
	case RecTypeScenarioProtect:
		return "ScenarioProtect (221)"
	case RecTypeOleObjectSize:
		return "OleObjectSize (222)"
	case RecTypeXF:
		return "XF (224)"
	case RecTypeInterfaceHdr:
		return "InterfaceHdr (225)"
	case RecTypeInterfaceEnd:
		return "InterfaceEnd (226)"
	case RecTypeSXVS:
		return "SXVS (227)"
	case RecTypeMergeCells:
		return "MergeCells (229)"
	case RecTypeBkHim:
		return "BkHim (233)"
	case RecTypeMsoDrawingGroup:
		return "MsoDrawingGroup (235)"
	case RecTypeMsoDrawing:
		return "MsoDrawing (236)"
	case RecTypeMsoDrawingSelection:
		return "MsoDrawingSelection (237)"
	case RecTypePhoneticInfo:
		return "PhoneticInfo (239)"
	case RecTypeSxRule:
		return "SxRule (240)"
	case RecTypeSXEx:
		return "SXEx (241)"
	case RecTypeSxFilt:
		return "SxFilt (242)"
	case RecTypeSxDXF:
		return "SxDXF (244)"
	case RecTypeSxItm:
		return "SxItm (245)"
	case RecTypeSxName:
		return "SxName (246)"
	case RecTypeSxSelect:
		return "SxSelect (247)"
	case RecTypeSXPair:
		return "SXPair (248)"
	case RecTypeSxFmla:
		return "SxFmla (249)"
	case RecTypeSxFormat:
		return "SxFormat (251)"
	case RecTypeSST:
		return "SST (252)"
	case RecTypeLabelSst:
		return "LabelSst (253)"
	case RecTypeExtSST:
		return "ExtSST (255)"
	case RecTypeSXVDEx:
		return "SXVDEx (256)"
	case RecTypeSXFormula:
		return "SXFormula (259)"
	case RecTypeSXDBEx:
		return "SXDBEx (290)"
	case RecTypeRRDInsDel:
		return "RRDInsDel (311)"
	case RecTypeRRDHead:
		return "RRDHead (312)"
	case RecTypeRRDChgCell:
		return "RRDChgCell (315)"
	case RecTypeRRTabID:
		return "RRTabID (317)"
	case RecTypeRRDRenSheet:
		return "RRDRenSheet (318)"
	case RecTypeRRSort:
		return "RRSort (319)"
	case RecTypeRRDMove:
		return "RRDMove (320)"
	case RecTypeRRFormat:
		return "RRFormat (330)"
	case RecTypeRRAutoFmt:
		return "RRAutoFmt (331)"
	case RecTypeRRInsertSh:
		return "RRInsertSh (333)"
	case RecTypeRRDMoveBegin:
		return "RRDMoveBegin (334)"
	case RecTypeRRDMoveEnd:
		return "RRDMoveEnd (335)"
	case RecTypeRRDInsDelBegin:
		return "RRDInsDelBegin (336)"
	case RecTypeRRDInsDelEnd:
		return "RRDInsDelEnd (337)"
	case RecTypeRRDConflict:
		return "RRDConflict (338)"
	case RecTypeRRDDefName:
		return "RRDDefName (339)"
	case RecTypeRRDRstEtxp:
		return "RRDRstEtxp (340)"
	case RecTypeLRng:
		return "LRng (351)"
	case RecTypeUsesELFs:
		return "UsesELFs (352)"
	case RecTypeDSF:
		return "DSF (353)"
	case RecTypeCUsr:
		return "CUsr (401)"
	case RecTypeCbUsr:
		return "CbUsr (402)"
	case RecTypeUsrInfo:
		return "UsrInfo (403)"
	case RecTypeUsrExcl:
		return "UsrExcl (404)"
	case RecTypeFileLock:
		return "FileLock (405)"
	case RecTypeRRDInfo:
		return "RRDInfo (406)"
	case RecTypeBCUsrs:
		return "BCUsrs (407)"
	case RecTypeUsrChk:
		return "UsrChk (408)"
	case RecTypeUserBView:
		return "UserBView (425)"
	case RecTypeUserSViewBegin:
		return "UserSViewBegin[Chart] (426)"
	case RecTypeUserSViewEnd:
		return "UserSViewEnd (427)"
	case RecTypeRRDUserView:
		return "RRDUserView (428)"
	case RecTypeQsi:
		return "Qsi (429)"
	case RecTypeSupBook:
		return "SupBook (430)"
	case RecTypeProt4Rev:
		return "Prot4Rev (431)"
	case RecTypeCondFmt:
		return "CondFmt (432)"
	case RecTypeCF:
		return "CF (433)"
	case RecTypeDVal:
		return "DVal (434)"
	case RecTypeDConBin:
		return "DConBin (437)"
	case RecTypeTxO:
		return "TxO (438)"
	case RecTypeRefreshAll:
		return "RefreshAll (439)"
	case RecTypeHLink:
		return "HLink (440)"
	case RecTypeLel:
		return "Lel (441)"
	case RecTypeCodeName:
		return "CodeName (442)"
	case RecTypeSXFDBType:
		return "SXFDBType (443)"
	case RecTypeProt4RevPass:
		return "Prot4RevPass (444)"
	case RecTypeObNoMacros:
		return "ObNoMacros (445)"
	case RecTypeDv:
		return "Dv (446)"
	case RecTypeExcel9File:
		return "Excel9File (448)"
	case RecTypeRecalcID:
		return "RecalcID (449)"
	case RecTypeEntExU2:
		return "EntExU2 (450)"
	case RecTypeDimensions:
		return "Dimensions (512)"
	case RecTypeBlank:
		return "Blank (513)"
	case RecTypeNumber:
		return "Number (515)"
	case RecTypeLabel:
		return "Label (516)"
	case RecTypeBoolErr:
		return "BoolErr (517)"
	case RecTypeString:
		return "String (519)"
	case RecTypeRow:
		return "Row (520)"
	case RecTypeIndex:
		return "Index (523)"
	case RecTypeArray:
		return "Array (545)"
	case RecTypeDefaultRowHeight:
		return "DefaultRowHeight (549)"
	case RecTypeTable:
		return "Table (566)"
	case RecTypeWindow2:
		return "Window2 (574)"
	case RecTypeRK:
		return "RK (638)"
	case RecTypeStyle:
		return "Style (659)"
	case RecTypeBigName:
		return "BigName (1048)"
	case RecTypeFormat:
		return "Format (1054)"
	case RecTypeContinueBigName:
		return "ContinueBigName (1084)"
	case RecTypeShrFmla:
		return "ShrFmla (1212)"
	case RecTypeHLinkTooltip:
		return "HLinkTooltip (2048)"
	case RecTypeWebPub:
		return "WebPub (2049)"
	case RecTypeQsiSXTag:
		return "QsiSXTag (2050)"
	case RecTypeDBQueryExt:
		return "DBQueryExt (2051)"
	case RecTypeExtString:
		return "ExtString (2052)"
	case RecTypeTxtQry:
		return "TxtQry (2053)"
	case RecTypeQsir:
		return "Qsir (2054)"
	case RecTypeQsif:
		return "Qsif (2055)"
	case RecTypeRRDTQSIF:
		return "RRDTQSIF (2056)"
	case RecTypeBOF:
		return "BOF (2057)"
	case RecTypeOleDbConn:
		return "OleDbConn (2058)"
	case RecTypeWOpt:
		return "WOpt (2059)"
	case RecTypeSXViewEx:
		return "SXViewEx (2060)"
	case RecTypeSXTH:
		return "SXTH (2061)"
	case RecTypeSXPIEx:
		return "SXPIEx (2062)"
	case RecTypeSXVDTEx:
		return "SXVDTEx (2063)"
	case RecTypeSXViewEx9:
		return "SXViewEx9 (2064)"
	case RecTypeContinueFrt:
		return "ContinueFrt (2066)"
	case RecTypeRealTimeData:
		return "RealTimeData (2067)"
	case RecTypeChartFrtInfo:
		return "ChartFrtInfo (2128)"
	case RecTypeFrtWrapper:
		return "FrtWrapper (2129)"
	case RecTypeStartBlock:
		return "StartBlock (2130)"
	case RecTypeEndBlock:
		return "EndBlock (2131)"
	case RecTypeStartObject:
		return "StartObject (2132)"
	case RecTypeEndObject:
		return "EndObject (2133)"
	case RecTypeCatLab:
		return "CatLab (2134)"
	case RecTypeYMult:
		return "YMult (2135)"
	case RecTypeSXViewLink:
		return "SXViewLink (2136)"
	case RecTypePivotChartBits:
		return "PivotChartBits (2137)"
	case RecTypeFrtFontList:
		return "FrtFontList (2138)"
	case RecTypeSheetExt:
		return "SheetExt (2146)"
	case RecTypeBookExt:
		return "BookExt (2147)"
	case RecTypeSXAddl:
		return "SXAddl (2148)"
	case RecTypeCrErr:
		return "CrErr (2149)"
	case RecTypeHFPicture:
		return "HFPicture (2150)"
	case RecTypeFeatHdr:
		return "FeatHdr (2151)"
	case RecTypeFeat:
		return "Feat (2152)"
	case RecTypeDataLabExt:
		return "DataLabExt (2154)"
	case RecTypeDataLabExtContents:
		return "DataLabExtContents (2155)"
	case RecTypeCellWatch:
		return "CellWatch (2156)"
	case RecTypeFeatHdr11:
		return "FeatHdr11 (2161)"
	case RecTypeFeature11:
		return "Feature11 (2162)"
	case RecTypeDropDownObjIds:
		return "DropDownObjIds (2164)"
	case RecTypeContinueFrt11:
		return "ContinueFrt11 (2165)"
	case RecTypeDConn:
		return "DConn (2166)"
	case RecTypeList12:
		return "List12 (2167)"
	case RecTypeFeature12:
		return "Feature12 (2168)"
	case RecTypeCondFmt12:
		return "CondFmt12 (2169)"
	case RecTypeCF12:
		return "CF12 (2170)"
	case RecTypeCFEx:
		return "CFEx (2171)"
	case RecTypeXFCRC:
		return "XFCRC (2172)"
	case RecTypeXFExt:
		return "XFExt (2173)"
	case RecTypeAutoFilter12:
		return "AutoFilter12 (2174)"
	case RecTypeContinueFrt12:
		return "ContinueFrt12 (2175)"
	case RecTypeMDTInfo:
		return "MDTInfo (2180)"
	case RecTypeMDXStr:
		return "MDXStr (2181)"
	case RecTypeMDXTuple:
		return "MDXTuple (2182)"
	case RecTypeMDXSet:
		return "MDXSet (2183)"
	case RecTypeMDXProp:
		return "MDXProp (2184)"
	case RecTypeMDXKPI:
		return "MDXKPI (2185)"
	case RecTypeMDB:
		return "MDB (2186)"
	case RecTypePLV:
		return "PLV (2187)"
	case RecTypeCompat12:
		return "Compat12 (2188)"
	case RecTypeDXF:
		return "DXF (2189)"
	case RecTypeTableStyles:
		return "TableStyles (2190)"
	case RecTypeTableStyle:
		return "TableStyle (2191)"
	case RecTypeTableStyleElement:
		return "TableStyleElement (2192)"
	case RecTypeStyleExt:
		return "StyleExt (2194)"
	case RecTypeNamePublish:
		return "NamePublish (2195)"
	case RecTypeNameCmt:
		return "NameCmt (2196)"
	case RecTypeSortData:
		return "SortData (2197)"
	case RecTypeTheme:
		return "Theme (2198)"
	case RecTypeGUIDTypeLib:
		return "GUIDTypeLib (2199)"
	case RecTypeFnGrp12:
		return "FnGrp12 (2200)"
	case RecTypeNameFnGrp12:
		return "NameFnGrp12 (2201)"
	case RecTypeMTRSettings:
		return "MTRSettings (2202)"
	case RecTypeCompressPictures:
		return "CompressPictures (2203)"
	case RecTypeHeaderFooter:
		return "HeaderFooter (2204)"
	case RecTypeCrtLayout12:
		return "CrtLayout12 (2205)"
	case RecTypeCrtMlFrt:
		return "CrtMlFrt (2206)"
	case RecTypeCrtMlFrtContinue:
		return "CrtMlFrtContinue (2207)"
	case RecTypeForceFullCalculation:
		return "ForceFullCalculation (2211)"
	case RecTypeShapePropsStream:
		return "ShapePropsStream (2212)"
	case RecTypeTextPropsStream:
		return "TextPropsStream (2213)"
	case RecTypeRichTextStream:
		return "RichTextStream (2214)"
	case RecTypeCrtLayout12A:
		return "CrtLayout12A (2215)"
	case RecTypeUnits:
		return "Units (4097)"
	case RecTypeChart:
		return "Chart (4098)"
	case RecTypeSeries:
		return "Series (4099)"
	case RecTypeDataFormat:
		return "DataFormat (4102)"
	case RecTypeLineFormat:
		return "LineFormat (4103)"
	case RecTypeMarkerFormat:
		return "MarkerFormat (4105)"
	case RecTypeAreaFormat:
		return "AreaFormat (4106)"
	case RecTypePieFormat:
		return "PieFormat (4107)"
	case RecTypeAttachedLabel:
		return "AttachedLabel (4108)"
	case RecTypeSeriesText:
		return "SeriesText (4109)"
	case RecTypeChartFormat:
		return "ChartFormat (4116)"
	case RecTypeLegend:
		return "Legend (4117)"
	case RecTypeSeriesList:
		return "SeriesList (4118)"
	case RecTypeBar:
		return "Bar (4119)"
	case RecTypeLine:
		return "Line (4120)"
	case RecTypePie:
		return "Pie (4121)"
	case RecTypeArea:
		return "Area (4122)"
	case RecTypeScatter:
		return "Scatter (4123)"
	case RecTypeCrtLine:
		return "CrtLine (4124)"
	case RecTypeAxis:
		return "Axis (4125)"
	case RecTypeTick:
		return "Tick (4126)"
	case RecTypeValueRange:
		return "ValueRange (4127)"
	case RecTypeCatSerRange:
		return "CatSerRange (4128)"
	case RecTypeAxisLine:
		return "AxisLine (4129)"
	case RecTypeCrtLink:
		return "CrtLink (4130)"
	case RecTypeDefaultText:
		return "DefaultText (4132)"
	case RecTypeText:
		return "Text (4133)"
	case RecTypeFontX:
		return "FontX (4134)"
	case RecTypeObjectLink:
		return "ObjectLink (4135)"
	case RecTypeFrame:
		return "Frame (4146)"
	case RecTypeBegin:
		return "Begin (4147)"
	case RecTypeEnd:
		return "End (4148)"
	case RecTypePlotArea:
		return "PlotArea (4149)"
	case RecTypeChart3d:
		return "Chart3d (4154)"
	case RecTypePicF:
		return "PicF (4156)"
	case RecTypeDropBar:
		return "DropBar (4157)"
	case RecTypeRadar:
		return "Radar (4158)"
	case RecTypeSurf:
		return "Surf (4159)"
	case RecTypeRadarArea:
		return "RadarArea (4160)"
	case RecTypeAxisParent:
		return "AxisParent (4161)"
	case RecTypeLegendException:
		return "LegendException (4163)"
	case RecTypeShtProps:
		return "ShtProps (4164)"
	case RecTypeSerToCrt:
		return "SerToCrt (4165)"
	case RecTypeAxesUsed:
		return "AxesUsed (4166)"
	case RecTypeSBaseRef:
		return "SBaseRef (4168)"
	case RecTypeSerParent:
		return "SerParent (4170)"
	case RecTypeSerAuxTrend:
		return "SerAuxTrend (4171)"
	case RecTypeIFmtRecord:
		return "IFmtRecord (4174)"
	case RecTypePos:
		return "Pos (4175)"
	case RecTypeAlRuns:
		return "AlRuns (4176)"
	case RecTypeBRAI:
		return "BRAI (4177)"
	case RecTypeSerAuxErrBar:
		return "SerAuxErrBar (4187)"
	case RecTypeClrtClient:
		return "ClrtClient (4188)"
	case RecTypeSerFmt:
		return "SerFmt (4189)"
	case RecTypeChart3DBarShape:
		return "Chart3DBarShape (4191)"
	case RecTypeFbi:
		return "Fbi (4192)"
	case RecTypeBopPop:
		return "BopPop (4193)"
	case RecTypeAxcExt:
		return "AxcExt (4194)"
	case RecTypeDat:
		return "Dat (4195)"
	case RecTypePlotGrowth:
		return "PlotGrowth (4196)"
	case RecTypeSIIndex:
		return "SIIndex (4197)"
	case RecTypeGelFrame:
		return "GelFrame (4198)"
	case RecTypeBopPopCustom:
		return "BopPopCustom (4199)"
	case RecTypeFbi2:
		return "Fbi2 (4200)"
	}
	return fmt.Sprintf("unknown (%d 0x%x)", uint16(r), uint16(r))
}


================================================
FILE: xls/sheets.go
================================================
package xls

import (
	"encoding/binary"
	"errors"
	"log"
	"math"
	"unicode/utf16"

	"github.com/pbnjay/grate"
	"github.com/pbnjay/grate/commonxl"
)

// List (visible) sheet names from the workbook.
func (b *WorkBook) List() ([]string, error) {
	res := make([]string, 0, len(b.sheets))
	for _, s := range b.sheets {
		if (s.HiddenState & 0x03) == 0 {
			res = append(res, s.Name)
		}
	}
	return res, nil
}

// ListHidden sheet names in the workbook.
func (b *WorkBook) ListHidden() ([]string, error) {
	res := make([]string, 0, len(b.sheets))
	for _, s := range b.sheets {
		if (s.HiddenState & 0x03) != 0 {
			res = append(res, s.Name)
		}
	}
	return res, nil
}

// Get opens the named worksheet and return an iterator for its contents.
func (b *WorkBook) Get(sheetName string) (grate.Collection, error) {
	for _, s := range b.sheets {
		if s.Name == sheetName {
			ss := b.pos2substream[int64(s.Position)]
			return b.parseSheet(s, ss)
		}
	}
	return nil, errors.New("xls: sheet not found")
}

func (b *WorkBook) parseSheet(s *boundSheet, ss int) (*commonxl.Sheet, error) {
	res := &commonxl.Sheet{
		Formatter: &b.nfmt,
	}
	var minRow, maxRow uint32
	var minCol, maxCol uint16

	// temporary string buffer
	us := make([]uint16, 8224)

	inSubstream := 0
	for idx, r := range b.substreams[ss] {
		if inSubstream > 0 {
			if r.RecType == RecTypeEOF {
				inSubstream--
			}
			continue
		}
		switch r.RecType {
		case RecTypeBOF:
			// a BOF inside a sheet usually means embedded content like a chart
			// (which we aren't interested in). So we we set a flag and wait
			// for the EOF for that content block.
			if idx > 0 {
				inSubstream++
				continue
			}
		case RecTypeWsBool:
			if (r.Data[1] & 0x10) != 0 {
				// it's a dialog
				return nil, nil
			}

		case RecTypeDimensions:
			// max = 0-based index of the row AFTER the last valid index
			minRow = binary.LittleEndian.Uint32(r.Data[:4])
			maxRow = binary.LittleEndian.Uint32(r.Data[4:8]) // max = 0x010000
			minCol = binary.LittleEndian.Uint16(r.Data[8:10])
			maxCol = binary.LittleEndian.Uint16(r.Data[10:12]) // max = 0x000100
			if grate.Debug {
				log.Printf("    Sheet dimensions (%d, %d) - (%d,%d)",
					minCol, minRow, maxCol, maxRow)
			}
			if minRow > 0x0000FFFF || maxRow > 0x00010000 {
				log.Println("invalid dimensions")
			}
			if minCol > 0x00FF || maxCol > 0x0100 {
				log.Println("invalid dimensions")
			}

			// pre-allocate cells
			res.Resize(int(maxRow), int(maxCol))
		}
	}
	inSubstream = 0

	var formulaRow, formulaCol uint16
	for ridx, r := range b.substreams[ss] {
		if inSubstream > 0 {
			if r.RecType == RecTypeEOF {
				inSubstream--
			} else if grate.Debug {
				log.Println("      Unhandled sheet substream record type:", r.RecType, ridx)
			}
			continue
		}

		// sec 2.1.7.20.6 Common Productions ABNF:
		/*
			CELLTABLE = 1*(1*Row *CELL 1*DBCell) *EntExU2
			CELL = FORMULA / Blank / MulBlank / RK / MulRk / BoolErr / Number / LabelSst
			FORMULA = [Uncalced] Formula [Array / Table / ShrFmla / SUB] [String *Continue]

			Not parsed form the list above:
				DBCell, EntExU2, Uncalced, Array, Table,ShrFmla
				NB: no idea what "SUB" is
		*/

		switch r.RecType {
		case RecTypeBOF:
			if ridx > 0 {
				inSubstream++
				continue
			}

		case RecTypeBoolErr:
			rowIndex := int(binary.LittleEndian.Uint16(r.Data[:2]))
			colIndex := int(binary.LittleEndian.Uint16(r.Data[2:4]))
			ixfe := int(binary.LittleEndian.Uint16(r.Data[4:6]))
			if r.Data[7] == 0 {
				// Boolean value
				bv := false
				if r.Data[6] == 1 {
					bv = true
				}
				var fno uint16
				if ixfe < len(b.xfs) {
					fno = b.xfs[ixfe]
				}
				res.Put(rowIndex, colIndex, bv, fno)
				//log.Printf("bool/error spec: %d %d %+v", rowIndex, colIndex, bv)
			} else {
				// it's an error, load the label
				be, ok := berrLookup[r.Data[6]]
				if !ok {
					be = "<unknown error>"
				}
				res.Put(rowIndex, colIndex, be, 0)
				//log.Printf("bool/error spec: %d %d %s", rowIndex, colIndex, be)
			}

		case RecTypeMulRk:
			// MulRk encodes multiple RK values in a row
			nrk := int((r.RecSize - 6) / 6)
			rowIndex := int(binary.LittleEndian.Uint16(r.Data[:2]))
			colIndex := int(binary.LittleEndian.Uint16(r.Data[2:4]))
			for i := 0; i < nrk; i++ {
				off := 4 + i*6
				ixfe := int(binary.LittleEndian.Uint16(r.Data[off:]))
				value := RKNumber(binary.LittleEndian.Uint32(r.Data[off+2:]))

				var rval interface{}
				if value.IsInteger() {
					rval = value.Int()
				} else {
					rval = value.Float64()
				}
				var fno uint16
				if ixfe < len(b.xfs) {
					fno = b.xfs[ixfe]
				}
				res.Put(rowIndex, colIndex+i, rval, fno)
			}
			//log.Printf("mulrow spec: %+v", *mr)

		case RecTypeNumber:
			rowIndex := int(binary.LittleEndian.Uint16(r.Data[:2]))
			colIndex := int(binary.LittleEndian.Uint16(r.Data[2:4]))
			ixfe := int(binary.LittleEndian.Uint16(r.Data[4:6]))
			xnum := binary.LittleEndian.Uint64(r.Data[6:])

			value := math.Float64frombits(xnum)
			var fno uint16
			if ixfe < len(b.xfs) {
				fno = b.xfs[ixfe]
			}
			res.Put(rowIndex, colIndex, value, fno)
			//log.Printf("Number spec: %d %d = %f", rowIndex, colIndex, value)

		case RecTypeRK:
			rowIndex := int(binary.LittleEndian.Uint16(r.Data[:2]))
			colIndex := int(binary.LittleEndian.Uint16(r.Data[2:4]))
			ixfe := int(binary.LittleEndian.Uint16(r.Data[4:]))
			value := RKNumber(binary.LittleEndian.Uint32(r.Data[6:]))

			var rval interface{}
			if value.IsInteger() {
				rval = value.Int()
			} else {
				rval = value.Float64()
			}
			var fno uint16
			if ixfe < len(b.xfs) {
				fno = b.xfs[ixfe]
			}
			res.Put(rowIndex, colIndex, rval, fno)
			//log.Printf("RK spec: %d %d = %+v", rowIndex, colIndex, rval)

		case RecTypeFormula:
			formulaRow = binary.LittleEndian.Uint16(r.Data[:2])
			formulaCol = binary.LittleEndian.Uint16(r.Data[2:4])
			ixfe := int(binary.LittleEndian.Uint16(r.Data[4:6]))
			fdata := r.Data[6:]
			var fno uint16
			if ixfe < len(b.xfs) {
				fno = b.xfs[ixfe]
			}
			if fdata[6] == 0xFF && fdata[7] == 0xFF {
				switch fdata[0] {
				case 0:
					// string in next record
					// put placeholder now to record the numFmt
					res.Put(int(formulaRow), int(formulaCol), "", fno)
				case 1:
					// boolean
					bv := false
					if fdata[2] != 0 {
						bv = true
					}
					res.Put(int(formulaRow), int(formulaCol), bv, fno)
				case 2:
					// error value
					be, ok := berrLookup[fdata[2]]
					if !ok {
						be = "<unknown error>"
					}
					res.Put(int(formulaRow), int(formulaCol), be, 0)
				case 3:
					// blank string
				default:
					log.Printf("unknown formula value type %d", fdata[0])
				}
			} else {
				xnum := binary.LittleEndian.Uint64(fdata)
				value := math.Float64frombits(xnum)
				res.Put(int(formulaRow), int(formulaCol), value, fno)
			}
			//log.Printf("formula spec: %d %d ~~ %+v", formulaRow, formulaCol, r.Data)

		case RecTypeString:
			// String is the previously rendered value of a formula
			// NB similar to the workbook SST, this can continue over
			// addition records up to 32k characters. A 1-byte flag
			// at each gap indicates if the encoding switches
			// to/from 8/16-bit characters.

			charCount := binary.LittleEndian.Uint16(r.Data[:2])
			flags := r.Data[2]
			fstr := ""
			if (flags & 1) == 0 {
				fstr = string(r.Data[3:])
			} else {
				raw := r.Data[3:]
				if int(charCount) > cap(us) {
					us = make([]uint16, charCount)
				}
				us = us[:charCount]
				for i := 0; i < int(charCount); i++ {
					us[i] = binary.LittleEndian.Uint16(raw)
					raw = raw[2:]
				}
				fstr = string(utf16.Decode(us))
			}

			if (ridx + 1) < len(b.substreams[ss]) {
				ridx2 := ridx + 1
				nrecs := len(b.substreams[ss])
				for ridx2 < nrecs {
					r2 := b.substreams[ss][ridx2]
					if r2.RecType != RecTypeContinue {
						break
					}
					if (r2.Data[0] & 1) == 0 {
						fstr += string(r2.Data[1:])
					} else {
						raw := r2.Data[1:]
						slen := len(raw) / 2
						us = us[:slen]
						for i := 0; i < slen; i++ {
							us[i] = binary.LittleEndian.Uint16(raw)
							raw = raw[2:]
						}
						fstr += string(utf16.Decode(us))
					}
					ridx2++
				}
			}
			res.Set(int(formulaRow), int(formulaCol), fstr)
			//log.Printf("String direct: %d %d '%s'", int(formulaRow), int(formulaCol), fstr)

		case RecTypeLabelSst:
			rowIndex := int(binary.LittleEndian.Uint16(r.Data[:2]))
			colIndex := int(binary.LittleEndian.Uint16(r.Data[2:4]))
			ixfe := int(binary.LittleEndian.Uint16(r.Data[4:6]))
			sstIndex := int(binary.LittleEndian.Uint32(r.Data[6:]))
			if sstIndex > len(b.strings) {
				return nil, errors.New("xls: invalid sst index")
			}
			var fno uint16
			if ixfe < len(b.xfs) {
				fno = b.xfs[ixfe]
			}
			if b.strings[sstIndex] != "" {
				res.Put(rowIndex, colIndex, b.strings[sstIndex], fno)
			}
			//log.Printf("SST spec: %d %d = [%d] '%s' %d", rowIndex, colIndex, sstIndex, b.strings[sstIndex], fno)

		case RecTypeHLink:
			firstRow := binary.LittleEndian.Uint16(r.Data[:2])
			lastRow := binary.LittleEndian.Uint16(r.Data[2:4])
			firstCol := binary.LittleEndian.Uint16(r.Data[4:6])
			lastCol := binary.LittleEndian.Uint16(r.Data[6:])
			if int(firstCol) > int(maxCol) {
				//log.Println("invalid hyperlink column")
				continue
			}
			if int(firstRow) > int(maxRow) {
				//log.Println("invalid hyperlink row")
				continue
			}
			if lastRow == 0xFFFF { // placeholder value indicate "last"
				lastRow = uint16(maxRow) - 1
			}
			if lastCol == 0xFF { // placeholder value indicate "last"
				lastCol = uint16(maxCol) - 1
			}

			// decode the hyperlink datastructure and try to find the
			// display text and separate the URL itself.
			displayText, linkText, err := decodeHyperlinks(r.Data[8:])
			if err != nil {
				log.Println(err)
				continue
			}

			// apply merge cell rules (see RecTypeMergeCells below)
			for rn := int(firstRow); rn <= int(lastRow); rn++ {
				for cn := int(firstCol); cn <= int(lastCol); cn++ {
					if rn == int(firstRow) && cn == int(firstCol) {
						// TODO: provide custom hooks for how to handle links in output
						res.Put(rn, cn, displayText+" <"+linkText+">", 0)
					} else if cn == int(firstCol) {
						// first and last column MAY be the same
						if rn == int(lastRow) {
							res.Put(rn, cn, grate.EndRowMerged, 0)
						} else {
							res.Put(rn, cn, grate.ContinueRowMerged, 0)
						}
					} else if cn == int(lastCol) {
						// first and last column are NOT the same
						res.Put(rn, cn, grate.EndColumnMerged, 0)
					} else {
						res.Put(rn, cn, grate.ContinueColumnMerged, 0)
					}
				}
			}

		case RecTypeMergeCells:
			// To keep cells aligned, Merged cells are handled by placing
			// special characters in each cell covered by the merge block.
			//
			// The contents of the cell are always in the top left position.
			// A "down arrow" (↓) indicates the left side of the merge block, and a
			// "down arrow with stop line" (⤓) indicates the last row of the merge.
			// A "right arrow" (→) indicates that the columns span horizontally,
			// and a "right arrow with stop line" (⇥) indicates the rightmost
			// column of the merge.
			//

			cmcs := binary.LittleEndian.Uint16(r.Data[:2])
			raw := r.Data[2:]
			for i := 0; i < int(cmcs); i++ {
				firstRow := binary.LittleEndian.Uint16(raw[:2])
				lastRow := binary.LittleEndian.Uint16(raw[2:4])
				firstCol := binary.LittleEndian.Uint16(raw[4:6])
				lastCol := binary.LittleEndian.Uint16(raw[6:])
				raw = raw[8:]

				if lastRow == 0xFFFF { // placeholder value indicate "last"
					lastRow = uint16(maxRow) - 1
				}
				if lastCol == 0xFF { // placeholder value indicate "last"
					lastCol = uint16(maxCol) - 1
				}
				for rn := int(firstRow); rn <= int(lastRow); rn++ {
					for cn := int(firstCol); cn <= int(lastCol); cn++ {
						if rn == int(firstRow) && cn == int(firstCol) {
							// should be a value there already!
						} else if cn == int(firstCol) {
							// first and last column MAY be the same
							if rn == int(lastRow) {
								res.Put(rn, cn, grate.EndRowMerged, 0)
							} else {
								res.Put(rn, cn, grate.ContinueRowMerged, 0)
							}
						} else if cn == int(lastCol) {
							// first and last column are NOT the same
							res.Put(rn, cn, grate.EndColumnMerged, 0)
						} else {
							res.Put(rn, cn, grate.ContinueColumnMerged, 0)
						}
					}
				}
			}
			/*
				case RecTypeBlank, RecTypeMulBlank:
					// cells default value is blank, no need for these

				case RecTypeContinue:
					// the only situation so far is when used in RecTypeString above

				case RecTypeRow, RecTypeDimensions, RecTypeEOF, RecTypeWsBool:
					// handled in initial pass

				default:
					if grate.Debug {
						log.Println("    Unhandled sheet record type:", r.RecType, ridx)
					}
			*/
		}
	}
	return res, nil
}

var berrLookup = map[byte]string{
	0x00: "#NULL!",
	0x07: "#DIV/0!",
	0x0F: "#VALUE!",
	0x17: "#REF!",
	0x1D: "#NAME?",
	0x24: "#NUM!",
	0x2A: "#N/A",
	0x2B: "#GETTING_DATA",
}


================================================
FILE: xls/simple_test.go
================================================
package xls

import (
	"bufio"
	"log"
	"os"
	"strings"
	"testing"

	"github.com/pbnjay/grate/commonxl"
)

var testFilePairs = [][]string{
	{"../testdata/basic.xls", "../testdata/basic.tsv"},
	{"../testdata/testing.xls", "../testdata/testing.tsv"},

	// TODO: custom formatter support
	//{"../testdata/basic2.xls", "../testdata/basic2.tsv"},

	// TODO: datetime and fraction formatter support
	//{"../testdata/multi_test.xls", "../testdata/multi_test.tsv"},
}

func loadTestData(fn string, ff *commonxl.Formatter) (*commonxl.Sheet, error) {
	f, err := os.Open(fn)
	if err != nil {
		return nil, err
	}
	xs := &commonxl.Sheet{
		Formatter: ff,
	}

	row := 0
	s := bufio.NewScanner(f)
	for s.Scan() {
		record := strings.Split(s.Text(), "\t")
		for i, val := range record {
			xs.Put(row, i, val, 0)
		}
		row++
	}
	return xs, f.Close()
}

func TestBasic(t *testing.T) {
	for _, fnames := range testFilePairs {
		var trueData *commonxl.Sheet
		log.Println("Testing ", fnames[0])

		wb, err := Open(fnames[0])
		if err != nil {
			t.Fatal(err)
		}

		sheets, err := wb.List()
		if err != nil {
			t.Fatal(err)
		}
		firstLoad := true
		for _, s := range sheets {
			sheet, err := wb.Get(s)
			if err != nil {
				t.Fatal(err)
			}
			xsheet := sheet.(*commonxl.Sheet)
			if firstLoad {
				trueData, err = loadTestData(fnames[1], xsheet.Formatter)
				if err != nil {
					t.Fatal(err)
				}
				firstLoad = false
			}

			for xrow, xdata := range xsheet.Rows {
				for xcol, xval := range xdata {
					//t.Logf("at %s (%d,%d) expect '%v'", fnames[0], xrow, xcol, trueData.Rows[xrow][xcol])
					if !trueData.Rows[xrow][xcol].Equal(xval) {
						t.Logf("mismatch at %s (%d,%d): '%v' <> '%v' expected", fnames[0], xrow, xcol,
							xval, trueData.Rows[xrow][xcol])
						t.Fail()
					}
				}
			}
		}

		err = wb.Close()
		if err != nil {
			t.Fatal(err)
		}
	}
}


================================================
FILE: xls/strings.go
================================================
package xls

import (
	"encoding/binary"
	"errors"
	"io"
	"io/ioutil"
	"unicode/utf16"
)

// 2.5.240
func decodeShortXLUnicodeString(raw []byte) (string, int, error) {
	// identical to decodeXLUnicodeString except for cch=8bits instead of 16
	cch := int(raw[0])
	flags := raw[1]
	raw = raw[2:]

	content := make([]uint16, cch)
	if (flags & 0x1) == 0 {
		// 16-bit characters but only the bottom 8bits
		contentBytes := raw[:cch]
		for i, x := range contentBytes {
			content[i] = uint16(x)
		}
		cch += 2 // to return the offset
	} else {
		// 16-bit characters
		for i := 0; i < cch; i++ {
			content[i] = binary.LittleEndian.Uint16(raw[:2])
			raw = raw[2:]
		}
		cch += cch + 2 // to return the offset
	}
	return string(utf16.Decode(content)), cch, nil
}

// 2.5.294
func decodeXLUnicodeString(raw []byte) (string, int, error) {
	// identical to decodeShortXLUnicodeString except for cch=16bits instead of 8
	cch := int(binary.LittleEndian.Uint16(raw[:2]))
	flags := raw[2]
	raw = raw[3:]

	content := make([]uint16, cch)
	if (flags & 0x1) == 0 {
		// 16-bit characters but only the bottom 8bits
		contentBytes := raw[:cch]
		for i, x := range contentBytes {
			content[i] = uint16(x)
		}
		cch += 3 // to return the offset
	} else {
		// 16-bit characters
		for i := 0; i < cch; i++ {
			content[i] = binary.LittleEndian.Uint16(raw[:2])
			raw = raw[2:]
		}
		cch += cch + 3 // to return the offset
	}
	return string(utf16.Decode(content)), cch, nil
}

// 2.5.293
func decodeXLUnicodeRichExtendedString(r io.Reader) (string, error) {
	var cch, cRun uint16
	var flags uint8
	var cbExtRs int32
	err := binary.Read(r, binary.LittleEndian, &cch)
	if err != nil {
		return "", err
	}
	err = binary.Read(r, binary.LittleEndian, &flags)
	if err != nil {
		return "", err
	}
	if (flags & 0x8) != 0 {
		// rich formating data is present
		err = binary.Read(r, binary.LittleEndian, &cRun)
		if err != nil {
			return "", err
		}
	}
	if (flags & 0x4) != 0 {
		// phonetic string data is present
		err = binary.Read(r, binary.LittleEndian, &cbExtRs)
		if err != nil {
			return "", err
		}
	}

	content := make([]uint16, cch)
	if (flags & 0x1) == 0 {
		// 16-bit characters but only the bottom 8bits
		contentBytes := make([]byte, cch)
		n, err2 := io.ReadFull(r, contentBytes)
		if n == 0 && err2 != io.ErrUnexpectedEOF {
			err = err2
		}
		if uint16(n) < cch {
			contentBytes = contentBytes[:n]
			content = content[:n]
		}

		for i, x := range contentBytes {
			content[i] = uint16(x)
		}

	} else {
		// 16-bit characters
		err = binary.Read(r, binary.LittleEndian, content)
	}
	if err != nil {
		return "", err
	}
	//////

	if cRun > 0 {
		// rich formating data is present
		_, err = io.CopyN(ioutil.Discard, r, int64(cRun)*4)
		if err != nil {
			return "", err
		}
	}
	if cbExtRs > 0 {
		// phonetic string data is present
		_, err = io.CopyN(ioutil.Discard, r, int64(cbExtRs))
		if err != nil {
			return "", err
		}
	}
	//////

	return string(utf16.Decode(content)), nil
}

// read in an array of XLUnicodeRichExtendedString s
func parseSST(recs []*rec) ([]string, error) {
	// The quirky thing about this code is that when strings cross a record
	// boundary, there's an intervening flags byte that MAY change the string
	// from an 8-bit encoding to 16-bit or vice versa.

	//totalRefs := binary.LittleEndian.Uint32(recs[0].Data[0:4])
	numStrings := binary.LittleEndian.Uint32(recs[0].Data[4:8])

	all := make([]string, 0, numStrings)
	current := make([]uint16, 32*1024)

	buf := recs[0].Data[8:]
	for i := 0; i < len(recs); {
		var cRunBytes int
		var flags byte
		var cbExtRs uint32

		for len(buf) > 0 {
			slen := binary.LittleEndian.Uint16(buf)
			buf = buf[2:]
			flags = buf[0]
			buf = buf[1:]

			if (flags & 0x8) != 0 {
				// rich formating data is present
				cRun := binary.LittleEndian.Uint16(buf)
				cRunBytes = int(cRun) * 4
				buf = buf[2:]
			}
			if (flags & 0x4) != 0 {
				// phonetic string data is present
				cbExtRs = binary.LittleEndian.Uint32(buf)
				buf = buf[4:]
			}

			///////
			blx := len(buf)
			bly := len(buf) - 5
			if blx > 5 {
				blx = 5
			}
			if bly < 0 {
				bly = 0
			}

			// this block will read the string data, but transparently
			// handle continuing across records
			if int(slen) > cap(current) {
				current = make([]uint16, slen)
			} else {
				current = current[:slen]
			}
			for j := 0; j < int(slen); j++ {
				if len(buf) == 0 {
					i++
					if (recs[i].Data[0] & 1) == 0 {
						flags &= 0xFE
					} else {
						flags |= 1
					}
					buf = recs[i].Data[1:]
				}

				if (flags & 1) == 0 { //8-bit
					current[j] = uint16(buf[0])
					buf = buf[1:]
				} else { //16-bit
					current[j] = uint16(binary.LittleEndian.Uint16(buf[:2]))
					buf = buf[2:]
					if len(buf) == 1 {
						return nil, errors.New("xls: off by one")
					}
				}
			}

			s := string(utf16.Decode(current))
			all = append(all, s)

			///////

			for cRunBytes > 0 {
				if len(buf) >= int(cRunBytes) {
					buf = buf[cRunBytes:]
					cRunBytes = 0
				} else {
					cRunBytes -= len(buf)
					i++
					buf = recs[i].Data
				}
			}

			for cbExtRs > 0 {
				if len(buf) >= int(cbExtRs) {
					buf = buf[cbExtRs:]
					cbExtRs = 0
				} else {
					cbExtRs -= uint32(len(buf))
					i++
					buf = recs[i].Data
				}
			}
		}
		i++
		if i < len(recs) {
			buf = recs[i].Data
		}
	}

	return all, nil
}


================================================
FILE: xls/structs.go
================================================
package xls

import (
	"fmt"
	"math"
)

type header struct {
	Version  uint16 // An unsigned integer that specifies the BIFF version of the file. The value MUST be 0x0600.
	DocType  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.
	RupBuild uint16 // An unsigned integer that specifies the build identifier.
	RupYear  uint16 // An unsigned integer that specifies the year when this BIFF version was first created. The value MUST be 0x07CC or 0x07CD.
	MiscBits uint64 // lots of miscellaneous bits and flags we're not going to check
}

// 2.1.4
type rec struct {
	RecType recordType //
	RecSize uint16     // must be between 0 and 8224
	Data    []byte     // len(rec.data) = rec.recsize
}

type boundSheet struct {
	Position    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.
	HiddenState byte   // (2 bits) An unsigned integer that specifies the hidden state of the sheet. MUST be a value from the following table:
	SheetType   byte   // An unsigned integer that specifies the sheet type. 00=worksheet
	Name        string
}

///////
type shRow struct {
	RowIndex uint16 // 0-based
	FirstCol uint16 // 0-based
	LastCol  uint16 // 1-based!
	Height   uint16
	Reserved uint32
	Flags    uint32
}

type shRef8 struct {
	FirstRow uint16 // 0-based
	LastRow  uint16 // 0-based
	FirstCol uint16 // 0-based
	LastCol  uint16 // 0-based
}
type shMulRK struct {
	RowIndex uint16 // 0-based
	FirstCol uint16 // 0-based
	Values   []RkRec
	LastCol  uint16 // 0-based?
}
type RkRec struct {
	IXFCell uint16
	Value   RKNumber
}

type shRK struct {
	RowIndex uint16 // 0-based
	Col      uint16 // 0-based
	IXFCell  uint16
	Value    RKNumber
}

type RKNumber uint32

func (r RKNumber) IsInteger() bool {
	if (r & 1) != 0 {
		// has 2 decimals
		return false
	}
	if (r & 2) == 0 {
		// is part of a float
		return false
	}
	return true
}

func (r RKNumber) Int() int {
	val := int32(r) >> 2
	if (r&1) == 0 && (r&2) != 0 {
		return int(val)
	}
	if (r&1) != 0 && (r&2) != 0 {
		return int(val / 100)
	}
	return 0
}

func (r RKNumber) Float64() float64 {
	val := int32(r) >> 2
	v2 := math.Float64frombits(uint64(val) << 34)

	if (r&1) == 0 && (r&2) == 0 {
		return v2
	}
	if (r&1) != 0 && (r&2) == 0 {
		return v2 / 100.0
	}
	return 0.0
}

func (r RKNumber) String() string {
	if r.IsInteger() {
		return fmt.Sprint(r.Int())
	}
	return fmt.Sprint(r.Float64())
}


================================================
FILE: xls/xls.go
================================================
// Package xls implements the Microsoft Excel Binary File Format (.xls) Structure.
// More specifically, it contains just enough detail to extract cell contents,
// data types, and last-calculated formula values. In particular, it does NOT
// implement formatting or formula calculations.
package xls

// https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-xls/cd03cb5f-ca02-4934-a391-bb674cb8aa06

import (
	"context"
	"encoding/binary"
	"errors"
	"io"
	"log"
	"sync"

	"github.com/pbnjay/grate"
	"github.com/pbnjay/grate/commonxl"
	"github.com/pbnjay/grate/xls/cfb"
	"github.com/pbnjay/grate/xls/crypto"
)

var _ = grate.Register("xls", 1, Open)

// WorkBook represents an Excel workbook containing 1 or more sheets.
type WorkBook struct {
	filename string
	ctx      context.Context
	doc      *cfb.Document

	prot     bool
	h        *header
	sheets   []*boundSheet
	codepage uint16
	dateMode uint16
	strings  []string

	password   string
	substreams [][]*rec

	fpos          int64
	pos2substream map[int64]int

	nfmt commonxl.Formatter
	xfs  []uint16
}

func (b *WorkBook) IsProtected() bool {
	return b.prot
}

func Open(filename string) (grate.Source, error) {
	doc, err := cfb.Open(filename)
	if err != nil {
		return nil, err
	}

	b := &WorkBook{
		filename: filename,
		doc:      doc,

		pos2substream: make(map[int64]int, 16),
		xfs:           make([]uint16, 0, 128),
	}

	rdr, err := doc.Open("Workbook")
	if err != nil {
		return nil, grate.WrapErr(err, grate.ErrNotInFormat)
	}
	raw, err := io.ReadAll(rdr)
	if err != nil {
		return nil, err
	}

	err = b.loadFromStream(raw)
	return b, err
}

func (b *WorkBook) loadFromStream(raw []byte) error {
	return b.loadFromStream2(raw, false)
}

func (b *WorkBook) loadFromStreamWithDecryptor(raw []byte, dec crypto.Decryptor) error {
	// interestingly (insecurely) BIFF8 keeps Record Types and sizes in the clear,
	// has a few records that are not encrypted, and has 1 record type that does
	// not encrypt the 32bit integer position at the beginning (while encrypting
	// the rest). It also resets the encryption block counter every 1024 bytes
	// (counting all the "skipped" bytes described above).
	//
	// So this code streams the records through the decryption, but also records
	// a set of overlays applied to the final result which restore the
	// "cleartext" contents in line with the decrypted content.

	if grate.Debug {
		log.Println("  Decrypting xls stream with standard RC4")
	}

	pos := 0
	zeros := [8224]byte{}

	type overlay struct {
		Pos int

		RecType   recordType
		DataBytes uint16
		Data      []byte // NB len() not necessarily = DataBytes
	}
	replaceBlocks := []overlay{}

	var err error
	for err == nil && len(raw[pos:]) > 4 {
		o := overlay{}
		o.Pos = pos
		o.RecType = recordType(binary.LittleEndian.Uint16(raw[pos : pos+2]))
		o.DataBytes = binary.LittleEndian.Uint16(raw[pos+2 : pos+4])
		pos += 4

		// copy to output and decryption stream
		binary.Write(dec, binary.LittleEndian, o.RecType)
		binary.Write(dec, binary.LittleEndian, o.DataBytes)
		tocopy := int(o.DataBytes)

		switch o.RecType {
		case RecTypeBOF, RecTypeFilePass, RecTypeUsrExcl, RecTypeFileLock, RecTypeInterfaceHdr, RecTypeRRDInfo, RecTypeRRDHead:
			// untouched data goes directly into output
			o.Data = raw[pos : pos+int(o.DataBytes)]
			pos += int(o.DataBytes)
			dec.Write(zeros[:int(o.DataBytes)])
			tocopy = 0

		case RecTypeBoundSheet8:
			// copy 32-bit position to output
			o.Data = raw[pos : pos+4]
			pos += 4
			dec.Write(zeros[:4])
			tocopy -= 4
		}

		if tocopy > 0 {
			_, err = dec.Write(raw[pos : pos+tocopy])
			pos += tocopy
		}
		replaceBlocks = append(replaceBlocks, o)
	}
	dec.Flush()

	alldata := dec.Bytes()
	for _, o := range replaceBlocks {
		offs := int(o.Pos)
		binary.LittleEndian.PutUint16(alldata[offs:], uint16(o.RecType))
		binary.LittleEndian.PutUint16(alldata[offs+2:], uint16(o.DataBytes))
		if len(o.Data) > 0 {
			offs += 4
			copy(alldata[offs:], o.Data)
		}
	}

	// recurse into the stream parser now that things are decrypted
	return b.loadFromStream2(alldata, true)
}

func (b *WorkBook) Close() error {
	// return records to the pool for reuse
	for i, sub := range b.substreams {
		for _, r := range sub {
			r.Data = nil // allow GC
			recPool.Put(r)
		}
		b.substreams[i] = b.substreams[i][:0]
	}
	b.substreams = b.substreams[:0]
	return nil
}

func (b *WorkBook) loadFromStream2(raw []byte, isDecrypted bool) error {
	b.h = &header{}
	substr := -1
	nestedBOF := 0
	b.pos2substream = make(map[int64]int, 10)
	b.fpos = 0

	// IMPORTANT: if there are any existing records, we need to return them to the pool
	for i, sub := range b.substreams {
		for _, r := range sub {
			recPool.Put(r)
		}
		b.substreams[i] = b.substreams[i][:0]
	}
	b.substreams = b.substreams[:0]

	rawfull := raw
	nr, no, err := b.nextRecord(raw)
	for err == nil {
		raw = raw[no:]
		switch nr.RecType {
		case RecTypeEOF:
			nestedBOF--
		case RecTypeBOF:
			// when substreams are nested, keep them in the same grouping
			if nestedBOF == 0 {
				substr = len(b.substreams)
				b.substreams = append(b.substreams, []*rec{})
				b.pos2substream[b.fpos] = substr
			}
			nestedBOF++
		}
		b.fpos += int64(4 + len(nr.Data))

		// if there's a FilePass record, the data is encrypted
		if nr.RecType == RecTypeFilePass && !isDecrypted {
			etype := binary.LittleEndian.Uint16(nr.Data)
			switch etype {
			case 1:
				dec, err := crypto.NewBasicRC4(nr.Data[2:])
				if err != nil {
					log.Println("xls: rc4 encryption failed to set up", err)
					return err
				}
				return b.loadFromStreamWithDecryptor(rawfull, dec)
			case 2, 3, 4:
				log.Println("need Crypto API RC4 decryptor")
				return errors.New("xls: unsupported Crypto API encryption method")
			default:
				return errors.New("xls: unsupported encryption method")
			}
		}

		b.substreams[substr] = append(b.substreams[substr], nr)
		nr, no, err = b.nextRecord(raw)
	}
	if err == io.EOF {
		err = nil
	}
	if err != nil {
		return err
	}

	for ss, records := range b.substreams {
		if grate.Debug {
			log.Printf("  Processing substream %d/%d (%d records)", ss, len(b.substreams), len(records))
		}
		for i, nr := range records {
			if len(nr.Data) == 0 {
				continue
			}

			switch nr.RecType {
			case RecTypeSST:
				// Shared String Table is often continued across multiple records,
				// so we want to gather them all before starting to parse (some
				// strings may span the gap between records)
				recSet := []*rec{nr}

				lastIndex := i
				for len(records) > (lastIndex+1) && records[lastIndex+1].RecType == RecTypeContinue {
					lastIndex++
					recSet = append(recSet, records[lastIndex])
				}

				b.strings, err = parseSST(recSet)
				if err != nil {
					return err
				}

			case RecTypeContinue:
				// no-op (used above)
			case RecTypeEOF:
				// done

			case RecTypeBOF:
				b.h = &header{
					Version:  binary.LittleEndian.Uint16(nr.Data[0:2]),
					DocType:  binary.LittleEndian.Uint16(nr.Data[2:4]),
					RupBuild: binary.LittleEndian.Uint16(nr.Data[4:6]),
					RupYear:  binary.LittleEndian.Uint16(nr.Data[6:8]),
					MiscBits: binary.LittleEndian.Uint64(nr.Data[8:16]),
				}

				if b.h.Version != 0x0600 {
					return errors.New("xls: invalid file version")
				}
				if b.h.RupYear != 0x07CC && b.h.RupYear != 0x07CD {
					return errors.New("xls: unsupported biff version")
				}
				/*
					if b.h.DocType != 0x0005 && b.h.DocType != 0x0010 {
						// we only support the workbook or worksheet substreams
						log.Println("xls: unsupported document type")
						//break
					}
				*/

			case RecTypeCodePage:
				// BIFF8 is entirely UTF-16LE so this is actually ignored
				b.codepage = binary.LittleEndian.Uint16(nr.Data)

			case RecTypeDate1904:
				b.dateMode = binary.LittleEndian.Uint16(nr.Data)

			case RecTypeFormat:
				// Format maps a format ID to a code string
				fmtNo := binary.LittleEndian.Uint16(nr.Data)
				formatStr, _, err := decodeXLUnicodeString(nr.Data[2:])
				if err != nil {
					log.Println("fail2", err)
					return err
				}
				b.nfmt.Add(fmtNo, formatStr)

			case RecTypeXF:
				// XF records merge multiple style and format directives to one ID
				// ignore font id at nr.Data[0:2]
				fmtNo := binary.LittleEndian.Uint16(nr.Data[2:])
				b.xfs = append(b.xfs, fmtNo)

			case RecTypeBoundSheet8:
				// Identifies the postition within the stream, visibility state,
				// and name of a worksheet
				bs := &boundSheet{}
				bs.Position = binary.LittleEndian.Uint32(nr.Data[:4])
				bs.HiddenState = nr.Data[4]
				bs.SheetType = nr.Data[5]

				bs.Name, _, err = decodeShortXLUnicodeString(nr.Data[6:])
				if err != nil {
					return err
				}
				b.sheets = append(b.sheets, bs)
			default:
				if grate.Debug && ss == 0 {
					log.Println("    Unhandled record type:", nr.RecType, i)
				}
			}
		}
	}

	return err
}

var recPool = sync.Pool{
	New: func() interface{} {
		return &rec{}
	},
}

func (b *WorkBook) nextRecord(raw []byte) (*rec, int, error) {
	if len(raw) < 4 {
		return nil, 0, io.EOF
	}
	rec := recPool.Get().(*rec)

	rec.RecType = recordType(binary.LittleEndian.Uint16(raw[:2]))
	rec.RecSize = binary.LittleEndian.Uint16(raw[2:4])
	if len(raw[4:]) < int(rec.RecSize) {
		recPool.Put(rec)
		return nil, 4, io.ErrUnexpectedEOF
	}
	rec.Data = raw[4 : 4+rec.RecSize]
	return rec, int(4 + rec.RecSize), nil
}


================================================
FILE: xlsx/comp_test.go
================================================
package xlsx

import (
	"os"
	"path/filepath"
	"strings"
	"testing"
)

func TestAllFiles(t *testing.T) {
	err := filepath.Walk("../testdata", func(p string, info os.FileInfo, err error) error {
		if info.IsDir() {
			return nil
		}
		if !strings.HasSuffix(info.Name(), ".xlsx") {
			return nil
		}
		wb, err := Open(p)
		if err != nil {
			return err
		}

		sheets, err := wb.List()
		if err != nil {
			return err
		}
		for _, s := range sheets {
			sheet, err := wb.Get(s)
			if err != nil {
				return err
			}

			for sheet.Next() {
				sheet.Strings()
			}
		}

		return wb.Close()
	})
	if err != nil {
		t.Fatal(err)
	}
}


================================================
FILE: xlsx/sheets.go
================================================
package xlsx

import (
	"encoding/xml"
	"errors"
	"io"
	"log"
	"path/filepath"
	"strconv"
	"strings"

	"github.com/pbnjay/grate"
	"github.com/pbnjay/grate/commonxl"
)

type Sheet struct {
	d       *Document
	relID   string
	name    string
	docname string

	err error

	wrapped *commonxl.Sheet
}

var errNotLoaded = errors.New("xlsx: sheet not loaded")

func (s *Sheet) parseSheet() error {
	s.wrapped = &commonxl.Sheet{
		Formatter: &s.d.fmt,
	}
	linkmap := make(map[string]string)
	base := filepath.Base(s.docname)
	sub := strings.TrimSuffix(s.docname, base)
	relsname := filepath.Join(sub, "_rels", base+".rels")
	dec, clo, err := s.d.openXML(relsname)
	if err == nil {
		// rels might not exist for every sheet
		tok, err := dec.RawToken()
		for ; err == nil; tok, err = dec.RawToken() {
			if v, ok := tok.(xml.StartElement); ok && v.Name.Local == "Relationship" {
				ax := getAttrs(v.Attr, "Id", "Type", "Target", "TargetMode")
				if ax[3] == "External" && ax[1] == "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" {
					linkmap[ax[0]] = ax[2]
				}
			}
		}
		clo.Close()
	}

	dec, clo, err = s.d.openXML(s.docname)
	if err != nil {
		return err
	}
	defer clo.Close()

	currentCellType := BlankCellType
	currentCell := ""
	var fno uint16
	var maxCol, maxRow int

	tok, err := dec.RawToken()
	for ; err == nil; tok, err = dec.RawToken() {
		switch v := tok.(type) {
		case xml.CharData:
			if currentCell == "" {
				continue
			}
			c, r := refToIndexes(currentCell)
			if c >= 0 && r >= 0 {
				var val interface{} = string(v)

				switch currentCellType {
				case BooleanCellType:
					if v[0] == '1' {
						val = true
					} else {
						val = false
					}
				case DateCellType:
					log.Println("CELL DATE", val, fno)
				case NumberCellType:
					fval, err := strconv.ParseFloat(string(v), 64)
					if err == nil {
						val = fval
					}
					//log.Println("CELL NUMBER", val, numFormat)
				case SharedStringCellType:
					//log.Println("CELL SHSTR", val, currentCellType, numFormat)
					si, _ := strconv.ParseInt(string(v), 10, 64)
					val = s.d.strings[si]
				case BlankCellType:
					//log.Println("CELL BLANK")
					// don't place any values
					continue
				case ErrorCellType, FormulaStringCellType, InlineStringCellType:
					//log.Println("CELL ERR/FORM/INLINE", val, currentCellType)
				default:
					log.Println("CELL UNKNOWN", val, currentCellType, fno)
				}
				s.wrapped.Put(r, c, val, fno)
			} else {
				//log.Println("FAIL row/col: ", currentCell)
			}
		case xml.StartElement:
			switch v.Name.Local {
			case "dimension":
				ax := getAttrs(v.Attr, "ref")
				if ax[0] == "A1" {
					maxCol, maxRow = 1, 1
					// short-circuit empty sheet
					s.wrapped.Resize(1, 1)
					continue
				}
				dims := strings.Split(ax[0], ":")
				if len(dims) == 1 {
					maxCol, maxRow = refToIndexes(dims[0])
				} else {
					//minCol, minRow := refToIndexes(dims[0])
					maxCol, maxRow = refToIndexes(dims[1])
				}
				s.wrapped.Resize(maxRow, maxCol)
				//log.Println("DIMENSION:", s.minRow, s.minCol, ">", s.maxRow, s.maxCol)
			case "row":
				//currentRow = ax["r"] // unsigned int row index
				//log.Println("ROW", currentRow)
			case "c":
				ax := getAttrs(v.Attr, "t", "r", "s")
				currentCellType = CellType(ax[0])
				if currentCellType == BlankCellType {
					currentCellType = NumberCellType
				}
				currentCell = ax[1] // always an A1 style reference
				style := ax[2]
				sid, _ := strconv.ParseInt(style, 10, 64)
				if len(s.d.xfs) > int(sid) {
					fno = s.d.xfs[sid]
				} else {
					fno = 0
				}
				//log.Println("CELL", currentCell, sid, numFormat, currentCellType)
			case "v":
				//log.Println("CELL VALUE", ax)

			case "mergeCell":
				ax := getAttrs(v.Attr, "ref")
				dims := strings.Split(ax[0], ":")
				startCol, startRow := refToIndexes(dims[0])
				endCol, endRow := startCol, startRow
				if len(dims) > 1 {
					endCol, endRow = refToIndexes(dims[1])
				}
				if endRow > maxRow {
					endRow = maxRow
				}
				if endCol > maxCol {
					endCol = maxCol
				}
				for r := startRow; r <= endRow; r++ {
					for c := startCol; c <= endCol; c++ {
						if r == startRow && c == startCol {
							// has data already!
						} else if c == startCol {
							// first and last column MAY be the same
							if r == endRow {
								s.wrapped.Put(r, c, grate.EndRowMerged, 0)
							} else {
								s.wrapped.Put(r, c, grate.ContinueRowMerged, 0)
							}
						} else if c == endCol {
							// first and last column are NOT the same
							s.wrapped.Put(r, c, grate.EndColumnMerged, 0)
						} else {
							s.wrapped.Put(r, c, grate.ContinueColumnMerged, 0)
						}
					}
				}

			case "hyperlink":
				ax := getAttrs(v.Attr, "ref", "id")
				col, row := refToIndexes(ax[0])
				link := linkmap[ax[1]]
				s.wrapped.Put(row, col, link, 0)
				s.wrapped.SetURL(row, col, link)

			case "worksheet", "mergeCells", "hyperlinks":
				// containers
			case "f":
				//log.Println("start: ", v.Name.Local, v.Attr)
			default:
				if grate.Debug {
					log.Println("      Unhandled sheet xml tag", v.Name.Local, v.Attr)
				}
			}
		case xml.EndElement:

			switch v.Name.Local {
			case "c":
				currentCell = ""
			case "row":
				//currentRow = ""
			}
		default:
			if grate.Debug {
				log.Printf("      Unhandled sheet xml tokens %T %+v", tok, tok)
			}
		}
	}
	if err == io.EOF {
		err = nil
	}
	return err
}


================================================
FILE: xlsx/simple_test.go
================================================
package xlsx

import (
	"bufio"
	"log"
	"os"
	"strings"
	"testing"

	"github.com/pbnjay/grate/commonxl"
)

var testFilePairs = [][]string{
	{"../testdata/basic.xlsx", "../testdata/basic.tsv"},

	// TODO: custom formatter support
	//{"../testdata/basic2.xlsx", "../testdata/basic2.tsv"},

	// TODO: datetime and fraction formatter support
	//{"../testdata/multi_test.xlsx", "../testdata/multi_test.tsv"},
}

func loadTestData(fn string, ff *commonxl.Formatter) (*commonxl.Sheet, error) {
	f, err := os.Open(fn)
	if err != nil {
		return nil, err
	}
	xs := &commonxl.Sheet{
		Formatter: ff,
	}

	row := 0
	s := bufio.NewScanner(f)
	for s.Scan() {
		record := strings.Split(s.Text(), "\t")
		for i, val := range record {
			xs.Put(row, i, val, 0)
		}
		row++
	}
	return xs, f.Close()
}

func TestBasic(t *testing.T) {
	for _, fnames := range testFilePairs {
		var trueData *commonxl.Sheet
		log.Println("Testing ", fnames[0])

		wb, err := Open(fnames[0])
		if err != nil {
			t.Fatal(err)
		}

		sheets, err := wb.List()
		if err != nil {
			t.Fatal(err)
		}
		firstLoad := true
		for _, s := range sheets {
			sheet, err := wb.Get(s)
			if err != nil {
				t.Fatal(err)
			}
			xsheet := sheet.(*commonxl.Sheet)
			if firstLoad {
				trueData, err = loadTestData(fnames[1], xsheet.Formatter)
				if err != nil {
					t.Fatal(err)
				}
				firstLoad = false
			}

			for xrow, xdata := range xsheet.Rows {
				for xcol, xval := range xdata {
					//t.Logf("at %s (%d,%d) expect '%v'", fnames[0], xrow, xcol, trueData.Rows[xrow][xcol])
					if !trueData.Rows[xrow][xcol].Equal(xval) {
						t.Logf("mismatch at %s (%d,%d): '%v' <> '%v' expected", fnames[0], xrow, xcol,
							xval, trueData.Rows[xrow][xcol])
						t.Fail()
					}
				}
			}
		}

		err = wb.Close()
		if err != nil {
			t.Fatal(err)
		}
	}
}


================================================
FILE: xlsx/types.go
================================================
package xlsx

import (
	"encoding/xml"
	"strconv"
	"strings"
)

type CellType string

// CellTypes define data type in section 18.18.11
const (
	BlankCellType         CellType = ""
	BooleanCellType       CellType = "b"
	DateCellType          CellType = "d"
	ErrorCellType         CellType = "e"
	NumberCellType        CellType = "n"
	SharedStringCellType  CellType = "s"
	FormulaStringCellType CellType = "str"
	InlineStringCellType  CellType = "inlineStr"
)

type staticCellType rune

const (
	staticBlank staticCellType = 0

	// marks a continuation column within a merged cell.
	continueColumnMerged staticCellType = '→'
	// marks the last column of a merged cell.
	endColumnMerged staticCellType = '⇥'

	// marks a continuation row within a merged cell.
	continueRowMerged staticCellType = '↓'
	// marks the last row of a merged cell.
	endRowMerged staticCellType = '⤓'
)

func (s staticCellType) String() string {
	if s == 0 {
		return ""
	}
	return string([]rune{rune(s)})
}

// returns the 0-based index of the column string:
//    "A"=0, "B"=1, "AA"=26, "BB"=53
func col2int(col string) int {
	idx := 0
	for _, c := range col {
		idx *= 26
		idx += int(c - '@')
	}
	return idx - 1
}

func refToIndexes(r string) (column, row int) {
	if len(r) < 2 {
		return -1, -1
	}
	i1 := strings.IndexAny(r, "0123456789")
	if i1 <= 0 {
		return -1, -1
	}

	// A1 Reference mode
	col1 := r[:i1]
	i2 := strings.IndexByte(r[i1:], 'C')
	if i2 == -1 {
		rn, _ := strconv.ParseInt(r[i1:], 10, 64)
		return col2int(col1), int(rn) - 1
	}

	// R1C1 Reference Mode
	col1 = r[i1:i2]
	row1 := r[i2+1:]
	cn, _ := strconv.ParseInt(col1, 10, 64)
	rn, _ := strconv.ParseInt(row1, 10, 64)
	return int(cn), int(rn) - 1
}

func getAttrs(attrs []xml.Attr, keys ...string) []string {
	res := make([]string, len(keys))
	for _, a := range attrs {
		for i, k := range keys {
			if a.Name.Local == k {
				res[i] = a.Value
			}
		}
	}
	return res
}


================================================
FILE: xlsx/workbook.go
================================================
package xlsx

import (
	"encoding/xml"
	"errors"
	"io"
	"log"
	"path/filepath"
	"strconv"
	"strings"

	"github.com/pbnjay/grate"
)

func (d *Document) parseRels(dec *xml.Decoder, basedir string) error {
	tok, err := dec.RawToken()
	for ; err == nil; tok, err = dec.RawToken() {
		switch v := tok.(type) {
		case xml.StartElement:
			switch v.Name.Local {
			case "Relationships":
				// container
			case "Relationship":
				vals := make(map[string]string, 5)
				for _, a := range v.Attr {
					vals[a.Name.Local] = a.Value
				}
				if _, ok := d.rels[vals["Type"]]; !ok {
					d.rels[vals["Type"]] = make(map[string]string)
				}
				if strings.HasPrefix(vals["Target"], "/") {
					// handle malformed "absolute" paths cleanly
					d.rels[vals["Type"]][vals["Id"]] = vals["Target"][1:]
				} else {
					d.rels[vals["Type"]][vals["Id"]] = filepath.Join(basedir, vals["Target"])
				}
				if vals["Type"] == "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" {
					d.primaryDoc = vals["Target"]
				}
			default:
				if grate.Debug {
					log.Println("      Unhandled relationship xml tag", v.Name.Local, v.Attr)
				}
			}
		case xml.EndElement:
			// not needed
		default:
			if grate.Debug {
				log.Printf("      Unhandled relationship xml tokens %T %+v", tok, tok)
			}
		}
	}
	if err == io.EOF {
		err = nil
	}
	return err
}

func (d *Document) parseWorkbook(dec *xml.Decoder) error {
	tok, err := dec.RawToken()
	for ; err == nil; tok, err = dec.RawToken() {
		switch v := tok.(type) {
		case xml.StartElement:
			switch v.Name.Local {
			case "sheet":
				vals := make(map[string]string, 5)
				for _, a := range v.Attr {
					vals[a.Name.Local] = a.Value
				}
				sheetID, ok1 := vals["id"]
				sheetName, ok2 := vals["name"]
				if !ok1 || !ok2 {
					return errors.New("xlsx: invalid sheet definition")
				}
				s := &Sheet{
					d:       d,
					relID:   sheetID,
					name:    sheetName,
					docname: d.rels["http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet"][sheetID],
					err:     errNotLoaded,
				}
				d.sheets = append(d.sheets, s)
			case "workbook", "sheets":
				// containers
			default:
				if grate.Debug {
					log.Println("      Unhandled workbook xml tag", v.Name.Local, v.Attr)
				}
			}
		case xml.EndElement:
			// not needed
		default:
			if grate.Debug {
				log.Printf("      Unhandled workbook xml tokens %T %+v", tok, tok)
			}
		}
	}
	if err == io.EOF {
		err = nil
	}
	return err
}

func (d *Document) parseStyles(dec *xml.Decoder) error {
	baseNumFormats := []string{}
	d.xfs = d.xfs[:0]

	section := 0
	tok, err := dec.RawToken()
	for ; err == nil; tok, err = dec.RawToken() {
		switch v := tok.(type) {
		case xml.StartElement:
			switch v.Name.Local {
			case "styleSheet":
				// container
			case "numFmt":
				ax := getAttrs(v.Attr, "numFmtId", "formatCode")
				fmtNo, _ := strconv.ParseInt(ax[0], 10, 16)
				d.fmt.Add(uint16(fmtNo), ax[1])

			case "cellStyleXfs":
				section = 1
			case "cellXfs":
				section = 2
				ax := getAttrs(v.Attr, "count")
				n, _ := strconv.ParseInt(ax[0], 10, 64)
				d.xfs = make([]uint16, 0, n)

			case "xf":
				ax := getAttrs(v.Attr, "numFmtId", "applyNumberFormat", "xfId")
				if section == 1 {
					// load base styles, but only save number format
					if ax[1] == "0" {
						baseNumFormats = append(baseNumFormats, "0")
					} else {
						baseNumFormats = append(baseNumFormats, ax[0])
					}
				} else if section == 2 {
					// actual referencable cell styles
					// 1) get base style so we can inherit format properly
					baseID, _ := strconv.ParseInt(ax[2], 10, 64)
					numFmtID := "0"
					if len(baseNumFormats) > int(baseID) {
						numFmtID = baseNumFormats[baseID]
					}

					// 2) check if this XF overrides the base format
					if ax[1] == "0" {
						// remove the format (if it was inherited)
						numFmtID = "0"
					} else {
						numFmtID = ax[0]
					}

					nfid, _ := strconv.ParseInt(numFmtID, 10, 16)
					d.xfs = append(d.xfs, uint16(nfid))
				} else {
					panic("wheres is this xf??")
				}
			default:
				if grate.Debug {
					log.Println("  Unhandled style xml tag", v.Name.Local, v.Attr)
				}
			}
		case xml.EndElement:
			switch v.Name.Local {
			case "cellStyleXfs":
				section = 0
			case "cellXfs":
				section = 0
			}
		default:
			if grate.Debug {
				log.Printf("      Unhandled style xml tokens %T %+v", tok, tok)
			}
		}
	}
	if err == io.EOF {
		err = nil
	}
	return err
}

func (d *Document) parseSharedStrings(dec *xml.Decoder) error {
	val := ""
	tok, err := dec.RawToken()
	for ; err == nil; tok, err = dec.RawToken() {
		switch v := tok.(type) {
		case xml.CharData:
			val += string(v)
		case xml.StartElement:
			switch v.Name.Local {
			case "si":
				val = ""
			case "t":
				// no attributes to parse, we only want the CharData ...
			case "sst":
				// main container
			default:
				if grate.Debug {
					log.Println("  Unhandled SST xml tag", v.Name.Local, v.Attr)
				}
			}
		case xml.EndElement:
			if v.Name.Local == "si" {
				d.strings = append(d.strings, val)
				continue
			}
		default:
			if grate.Debug {
				log.Printf("    Unhandled SST xml token %T %+v", tok, tok)
			}
		}
	}
	if err == io.EOF {
		err = nil
	}
	return err
}


================================================
FILE: xlsx/xlsx.go
================================================
package xlsx

import (
	"archive/zip"
	"encoding/xml"
	"errors"
	"io"
	"log"
	"os"
	"path/filepath"
	"strings"

	"github.com/pbnjay/grate"
	"github.com/pbnjay/grate/commonxl"
)

var _ = grate.Register("xlsx", 5, Open)

// Document contains an Office Open XML document.
type Document struct {
	filename   string
	f          *os.File
	r          *zip.Reader
	primaryDoc string

	// type => id => filename
	rels    map[string]map[string]string
	sheets  []*Sheet
	strings []string
	xfs     []uint16
	fmt     commonxl.Formatter
}

func (d *Document) Close() error {
	d.xfs = d.xfs[:0]
	d.xfs = nil
	d.strings = d.strings[:0]
	d.strings = nil
	d.sheets = d.sheets[:0]
	d.sheets = nil
	return d.f.Close()
}

func Open(filename string) (grate.Source, error) {
	f, err := os.Open(filename)
	if err != nil {
		return nil, err
	}
	info, err := f.Stat()
	if err != nil {
		return nil, err
	}
	z, err := zip.NewReader(f, info.Size())
	if err != nil {
		return nil, grate.WrapErr(err, grate.ErrNotInFormat)
	}
	d := &Document{
		filename: filename,
		f:        f,
		r:        z,
	}

	d.rels = make(map[string]map[string]string, 4)

	// parse the primary relationships
	dec, c, err := d.openXML("_rels/.rels")
	if err != nil {
		return nil, grate.WrapErr(err, grate.ErrNotInFormat)
	}
	err = d.parseRels(dec, "")
	c.Close()
	if err != nil {
		return nil, grate.WrapErr(err, grate.ErrNotInFormat)
	}
	if d.primaryDoc == "" {
		return nil, errors.New("xlsx: invalid document")
	}

	// parse the secondary relationships to primary doc
	base := filepath.Base(d.primaryDoc)
	sub := strings.TrimSuffix(d.primaryDoc, base)
	relfn := filepath.Join(sub, "_rels", base+".rels")
	dec, c, err = d.openXML(relfn)
	if err != nil {
		return nil, err
	}
	err = d.parseRels(dec, sub)
	c.Close()
	if err != nil {
		return nil, err
	}

	// parse the workbook structure
	dec, c, err = d.openXML(d.primaryDoc)
	if err != nil {
		return nil, err
	}
	err = d.parseWorkbook(dec)
	c.Close()
	if err != nil {
		return nil, err
	}

	styn := d.rels["http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles"]
	for _, sst := range styn {
		// parse the shared string table
		dec, c, err = d.openXML(sst)
		if err != nil {
			return nil, err
		}
		err = d.parseStyles(dec)
		c.Close()
		if err != nil {
			return nil, err
		}
	}

	ssn := d.rels["http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings"]
	for _, sst := range ssn {
		// parse the shared string table
		dec, c, err = d.openXML(sst)
		if err != nil {
			return nil, err
		}
		err = d.parseSharedStrings(dec)
		c.Close()
		if err != nil {
			return nil, err
		}
	}

	return d, nil
}

func (d *Document) openXML(name string) (*xml.Decoder, io.Closer, error) {
	if grate.Debug {
		log.Println("    openXML", name)
	}
	for _, zf := range d.r.File {
		if zf.Name == name {
			zfr, err := zf.Open()
			if err != nil {
				return nil, nil, err
			}
			dec := xml.NewDecoder(zfr)
			return dec, zfr, nil
		}
	}
	return nil, nil, io.EOF
}

func (d *Document) List() ([]string, error) {
	res := make([]string, 0, len(d.sheets))
	for _, s := range d.sheets {
		res = append(res, s.name)
	}
	return res, nil
}

func (d *Document) Get(sheetName string) (grate.Collection, error) {
	for _, s := range d.sheets {
		if s.name == sheetName {
			if s.err == errNotLoaded {
				s.err = s.parseSheet()
			}
			return s.wrapped, s.err
		}
	}
	return nil, errors.New("xlsx: sheet not found")
}
Download .txt
gitextract_81jad5jf/

├── .github/
│   └── workflows/
│       └── go.yml
├── .gitignore
├── LICENSE
├── README.md
├── cmd/
│   ├── grate2tsv/
│   │   └── main.go
│   └── grater/
│       └── main.go
├── commonxl/
│   ├── cell.go
│   ├── dates.go
│   ├── fmt.go
│   ├── fmt_test.go
│   ├── formats.go
│   ├── frac_test.go
│   ├── numbers.go
│   └── sheet.go
├── errs.go
├── go.mod
├── grate.go
├── simple/
│   ├── csv.go
│   ├── simple.go
│   └── tsv.go
├── xls/
│   ├── cfb/
│   │   ├── cfb.go
│   │   ├── interface.go
│   │   ├── simple_test.go
│   │   └── slicereader.go
│   ├── comp_test.go
│   ├── crypto/
│   │   ├── crypto.go
│   │   └── rc4.go
│   ├── hyperlinks.go
│   ├── records.go
│   ├── sheets.go
│   ├── simple_test.go
│   ├── strings.go
│   ├── structs.go
│   └── xls.go
└── xlsx/
    ├── comp_test.go
    ├── sheets.go
    ├── simple_test.go
    ├── types.go
    ├── workbook.go
    └── xlsx.go
Download .txt
SYMBOL INDEX (577 symbols across 35 files)

FILE: cmd/grate2tsv/main.go
  type output (line 54) | type output struct
  function main (line 59) | func main() {
  function runProcessor (line 136) | func runProcessor(from chan string, mu *sync.Mutex) {
  type stats (line 166) | type stats struct
  function processFile (line 175) | func processFile(fn string) ([]stats, error) {

FILE: cmd/grater/main.go
  function main (line 16) | func main() {

FILE: commonxl/cell.go
  type CellType (line 13) | type CellType
    method String (line 29) | func (c CellType) String() string {
  constant BlankCell (line 17) | BlankCell CellType = iota
  constant IntegerCell (line 18) | IntegerCell
  constant FloatCell (line 19) | FloatCell
  constant StringCell (line 20) | StringCell
  constant BooleanCell (line 21) | BooleanCell
  constant DateCell (line 22) | DateCell
  constant HyperlinkStringCell (line 24) | HyperlinkStringCell
  constant StaticCell (line 25) | StaticCell
  type Cell (line 51) | type Cell
    method Value (line 58) | func (c Cell) Value() interface{} {
    method SetURL (line 66) | func (c *Cell) SetURL(link string) {
    method URL (line 76) | func (c Cell) URL() (*url.URL, bool) {
    method Type (line 85) | func (c Cell) Type() CellType {
    method FormatNo (line 93) | func (c Cell) FormatNo() uint16 {
    method Clone (line 101) | func (c Cell) Clone() Cell {
    method SetFormatNumber (line 306) | func (c *Cell) SetFormatNumber(f uint16) {
    method Equal (line 319) | func (c Cell) Equal(other Cell) bool {
    method Less (line 346) | func (c Cell) Less(other Cell) bool {
  function NewCellWithType (line 119) | func NewCellWithType(value interface{}, t CellType, f *Formatter) Cell {
  function NewCell (line 199) | func NewCell(value interface{}) Cell {

FILE: commonxl/dates.go
  method ConvertToDate (line 10) | func (x *Formatter) ConvertToDate(val float64) time.Time {
  function timeFmtFunc (line 47) | func timeFmtFunc(f string) FmtFunc {
  function cnTimeFmtFunc (line 64) | func cnTimeFmtFunc(f string) FmtFunc {

FILE: commonxl/fmt.go
  type FmtFunc (line 10) | type FmtFunc
  function staticFmtFunc (line 12) | func staticFmtFunc(s string) FmtFunc {
  function surround (line 18) | func surround(pre string, ff FmtFunc, post string) FmtFunc {
  function addNegParens (line 24) | func addNegParens(ff FmtFunc) FmtFunc {
  function addCommas (line 34) | func addCommas(ff FmtFunc) FmtFunc {
  function identFunc (line 57) | func identFunc(x *Formatter, v interface{}) string {
  function sprintfFunc (line 86) | func sprintfFunc(fs string, mul int) FmtFunc {
  function convertToInt64 (line 105) | func convertToInt64(v interface{}) (int64, bool) {
  function convertToFloat64 (line 110) | func convertToFloat64(v interface{}) (float64, bool) {
  function zeroDashFunc (line 150) | func zeroDashFunc(ff FmtFunc) FmtFunc {
  function fracFmtFunc (line 164) | func fracFmtFunc(n int) FmtFunc {
  function switchFmtFunc (line 186) | func switchFmtFunc(pos FmtFunc, others ...FmtFunc) FmtFunc {

FILE: commonxl/fmt_test.go
  type testcaseNums (line 9) | type testcaseNums struct
  function TestCommas (line 73) | func TestCommas(t *testing.T) {
  function TestDateFormats (line 84) | func TestDateFormats(t *testing.T) {
  function TestBoolFormats (line 110) | func TestBoolFormats(t *testing.T) {

FILE: commonxl/formats.go
  type Formatter (line 11) | type Formatter struct
    method Mode1904 (line 23) | func (x *Formatter) Mode1904(enabled bool) {
    method Add (line 32) | func (x *Formatter) Add(fmtID uint16, formatCode string) error {
    method getCellType (line 55) | func (x *Formatter) getCellType(fmtID uint16) (CellType, bool) {
    method Get (line 200) | func (x *Formatter) Get(fmtID uint16) (FmtFunc, bool) {
    method Apply (line 215) | func (x *Formatter) Apply(fmtID uint16, val interface{}) (string, bool) {
  constant fMode1904 (line 18) | fMode1904 uint64 = 1
  function makeFormatter (line 76) | func makeFormatter(s string) (FmtFunc, CellType) {

FILE: commonxl/frac_test.go
  type testcaseFrac (line 8) | type testcaseFrac struct
  function TestFractions (line 59) | func TestFractions(t *testing.T) {

FILE: commonxl/numbers.go
  function DecimalToWholeFraction (line 10) | func DecimalToWholeFraction(val float64, nn, nd int) (whole, num, den in...
  function DecimalToFraction (line 26) | func DecimalToFraction(val float64, nn, nd int) (num, den int) {

FILE: commonxl/sheet.go
  type Sheet (line 12) | type Sheet struct
    method Resize (line 23) | func (s *Sheet) Resize(rows, cols int) {
    method Put (line 51) | func (s *Sheet) Put(row, col int, value interface{}, fmtNum uint16) {
    method Set (line 89) | func (s *Sheet) Set(row, col int, value interface{}) {
    method SetURL (line 100) | func (s *Sheet) SetURL(row, col int, link string) {
    method Next (line 111) | func (s *Sheet) Next() bool {
    method Raw (line 120) | func (s *Sheet) Raw() []Cell {
    method Strings (line 129) | func (s *Sheet) Strings() []string {
    method Types (line 153) | func (s *Sheet) Types() []string {
    method Formats (line 162) | func (s *Sheet) Formats() []string {
    method Scan (line 178) | func (s *Sheet) Scan(args ...interface{}) error {
    method IsEmpty (line 225) | func (s *Sheet) IsEmpty() bool {
    method Err (line 230) | func (s *Sheet) Err() error {

FILE: errs.go
  type errx (line 24) | type errx struct
    method Error (line 28) | func (e errx) Error() string {
    method Unwrap (line 31) | func (e errx) Unwrap() error {
  function WrapErr (line 39) | func WrapErr(e ...error) error {

FILE: grate.go
  type Source (line 12) | type Source interface
  type Collection (line 24) | type Collection interface
  type OpenFunc (line 55) | type OpenFunc
  function Open (line 58) | func Open(filename string) (Source, error) {
  type srcOpenTab (line 74) | type srcOpenTab struct
  function Register (line 83) | func Register(name string, priority int, opener OpenFunc) error {
  constant ContinueColumnMerged (line 96) | ContinueColumnMerged = "→"
  constant EndColumnMerged (line 98) | EndColumnMerged = "⇥"
  constant ContinueRowMerged (line 101) | ContinueRowMerged = "↓"
  constant EndRowMerged (line 103) | EndRowMerged = "⤓"

FILE: simple/csv.go
  function OpenCSV (line 14) | func OpenCSV(filename string) (grate.Source, error) {

FILE: simple/simple.go
  type simpleFile (line 15) | type simpleFile struct
    method List (line 22) | func (t *simpleFile) List() ([]string, error) {
    method Close (line 26) | func (t *simpleFile) Close() error {
    method Get (line 31) | func (t *simpleFile) Get(name string) (grate.Collection, error) {
    method Next (line 37) | func (t *simpleFile) Next() bool {
    method Strings (line 43) | func (t *simpleFile) Strings() []string {
    method Formats (line 48) | func (t *simpleFile) Formats() []string {
    method Types (line 59) | func (t *simpleFile) Types() []string {
    method Scan (line 74) | func (t *simpleFile) Scan(args ...interface{}) error {
    method IsEmpty (line 111) | func (t *simpleFile) IsEmpty() bool {
    method Err (line 116) | func (t *simpleFile) Err() error {

FILE: simple/tsv.go
  function OpenTSV (line 15) | func OpenTSV(filename string) (grate.Source, error) {

FILE: xls/cfb/cfb.go
  constant fullAssertions (line 21) | fullAssertions = true
  constant secFree (line 24) | secFree       uint32 = 0xFFFFFFFF
  constant secEndOfChain (line 25) | secEndOfChain uint32 = 0xFFFFFFFE
  constant secFAT (line 26) | secFAT        uint32 = 0xFFFFFFFD
  constant secDIFAT (line 27) | secDIFAT      uint32 = 0xFFFFFFFC
  constant secReserved (line 28) | secReserved   uint32 = 0xFFFFFFFB
  constant secMaxRegular (line 29) | secMaxRegular uint32 = 0xFFFFFFFA
  type header (line 33) | type header struct
  type objectType (line 54) | type objectType
  constant typeUnknown (line 57) | typeUnknown     objectType = 0x00
  constant typeStorage (line 58) | typeStorage     objectType = 0x01
  constant typeStream (line 59) | typeStream      objectType = 0x02
  constant typeRootStorage (line 60) | typeRootStorage objectType = 0x05
  type directory (line 63) | type directory struct
    method String (line 79) | func (d *directory) String() string {
  type Document (line 89) | type Document struct
    method load (line 105) | func (d *Document) load(rx io.ReadSeeker) error {
    method buildDirs (line 242) | func (d *Document) buildDirs(br *bytes.Reader) error {
    method getStreamReader (line 283) | func (d *Document) getStreamReader(sid uint32, size uint64) (io.ReadSe...
    method getMiniStreamReader (line 316) | func (d *Document) getMiniStreamReader(sid uint32, size uint64) (io.Re...

FILE: xls/cfb/interface.go
  function Open (line 10) | func Open(filename string) (*Document, error) {
  method List (line 24) | func (d *Document) List() ([]string, error) {
  method Open (line 35) | func (d *Document) Open(name string) (io.ReadSeeker, error) {

FILE: xls/cfb/simple_test.go
  function TestHeader (line 11) | func TestHeader(t *testing.T) {
  function TestHeader2 (line 20) | func TestHeader2(t *testing.T) {
  function TestHeader3 (line 29) | func TestHeader3(t *testing.T) {
  function TestHeader4 (line 38) | func TestHeader4(t *testing.T) {
  function TestSliceReader (line 77) | func TestSliceReader(t *testing.T) {

FILE: xls/cfb/slicereader.go
  type SliceReader (line 10) | type SliceReader struct
    method Read (line 18) | func (s *SliceReader) Read(b []byte) (int, error) {
    method Seek (line 38) | func (s *SliceReader) Seek(offset int64, whence int) (int64, error) {

FILE: xls/comp_test.go
  function TestAllFiles (line 10) | func TestAllFiles(t *testing.T) {

FILE: xls/crypto/crypto.go
  type Decryptor (line 13) | type Decryptor interface
  type basicRC4Encryption (line 72) | type basicRC4Encryption struct
  function NewBasicRC4 (line 81) | func NewBasicRC4(data []byte) (Decryptor, error) {

FILE: xls/crypto/rc4.go
  type rc4Writer (line 75) | type rc4Writer struct
    method Write (line 13) | func (d *rc4Writer) Write(data []byte) (n int, err error) {
    method Read (line 29) | func (d *rc4Writer) Read(data []byte) (n int, err error) {
    method Reset (line 34) | func (d *rc4Writer) Reset() {
    method Flush (line 41) | func (d *rc4Writer) Flush() {
    method SetPassword (line 63) | func (d *rc4Writer) SetPassword(password []byte) {
    method Bytes (line 95) | func (d *rc4Writer) Bytes() []byte {
    method Verify (line 99) | func (d *rc4Writer) Verify(everifier, everifierHash []byte) error {
    method startBlock (line 119) | func (d *rc4Writer) startBlock() {
  function generateStd97Key (line 131) | func generateStd97Key(passData []rune, salt []byte) []byte {

FILE: xls/hyperlinks.go
  function decodeHyperlinks (line 11) | func decodeHyperlinks(raw []byte) (displayText, linkText string, err err...
  function parseHyperlinkMoniker (line 76) | func parseHyperlinkMoniker(raw []byte) (string, int, error) {
  constant hlstmfHasMoniker (line 136) | hlstmfHasMoniker          = uint32(0x001)
  constant hlstmfIsAbsolute (line 137) | hlstmfIsAbsolute          = uint32(0x002)
  constant hlstmfSiteGaveDisplayName (line 138) | hlstmfSiteGaveDisplayName = uint32(0x004)
  constant hlstmfHasLocationStr (line 139) | hlstmfHasLocationStr      = uint32(0x008)
  constant hlstmfHasDisplayName (line 140) | hlstmfHasDisplayName      = uint32(0x010)
  constant hlstmfHasGUID (line 141) | hlstmfHasGUID             = uint32(0x020)
  constant hlstmfHasCreationTime (line 142) | hlstmfHasCreationTime     = uint32(0x040)
  constant hlstmfHasFrameName (line 143) | hlstmfHasFrameName        = uint32(0x080)
  constant hlstmfMonikerSavedAsStr (line 144) | hlstmfMonikerSavedAsStr   = uint32(0x100)
  constant hlstmfAbsFromGetdataRel (line 145) | hlstmfAbsFromGetdataRel   = uint32(0x200)

FILE: xls/records.go
  type recordType (line 5) | type recordType
    method String (line 366) | func (r recordType) String() string {
  constant RecTypeFormula (line 10) | RecTypeFormula              recordType = 6
  constant RecTypeEOF (line 11) | RecTypeEOF                  recordType = 10
  constant RecTypeCalcCount (line 12) | RecTypeCalcCount            recordType = 12
  constant RecTypeCalcMode (line 13) | RecTypeCalcMode             recordType = 13
  constant RecTypeCalcPrecision (line 14) | RecTypeCalcPrecision        recordType = 14
  constant RecTypeCalcRefMode (line 15) | RecTypeCalcRefMode          recordType = 15
  constant RecTypeCalcDelta (line 16) | RecTypeCalcDelta            recordType = 16
  constant RecTypeCalcIter (line 17) | RecTypeCalcIter             recordType = 17
  constant RecTypeProtect (line 18) | RecTypeProtect              recordType = 18
  constant RecTypePassword (line 19) | RecTypePassword             recordType = 19
  constant RecTypeHeader (line 20) | RecTypeHeader               recordType = 20
  constant RecTypeFooter (line 21) | RecTypeFooter               recordType = 21
  constant RecTypeExternSheet (line 22) | RecTypeExternSheet          recordType = 23
  constant RecTypeLbl (line 23) | RecTypeLbl                  recordType = 24
  constant RecTypeWinProtect (line 24) | RecTypeWinProtect           recordType = 25
  constant RecTypeVerticalPageBreaks (line 25) | RecTypeVerticalPageBreaks   recordType = 26
  constant RecTypeHorizontalPageBreaks (line 26) | RecTypeHorizontalPageBreaks recordType = 27
  constant RecTypeNote (line 27) | RecTypeNote                 recordType = 28
  constant RecTypeSelection (line 28) | RecTypeSelection            recordType = 29
  constant RecTypeDate1904 (line 29) | RecTypeDate1904             recordType = 34
  constant RecTypeExternName (line 30) | RecTypeExternName           recordType = 35
  constant RecTypeLeftMargin (line 31) | RecTypeLeftMargin           recordType = 38
  constant RecTypeRightMargin (line 32) | RecTypeRightMargin          recordType = 39
  constant RecTypeTopMargin (line 33) | RecTypeTopMargin            recordType = 40
  constant RecTypeBottomMargin (line 34) | RecTypeBottomMargin         recordType = 41
  constant RecTypePrintRowCol (line 35) | RecTypePrintRowCol          recordType = 42
  constant RecTypePrintGrid (line 36) | RecTypePrintGrid            recordType = 43
  constant RecTypeFilePass (line 37) | RecTypeFilePass             recordType = 47
  constant RecTypeFont (line 38) | RecTypeFont                 recordType = 49
  constant RecTypePrintSize (line 39) | RecTypePrintSize            recordType = 51
  constant RecTypeContinue (line 40) | RecTypeContinue             recordType = 60
  constant RecTypeWindow1 (line 41) | RecTypeWindow1              recordType = 61
  constant RecTypeBackup (line 42) | RecTypeBackup               recordType = 64
  constant RecTypePane (line 43) | RecTypePane                 recordType = 65
  constant RecTypeCodePage (line 44) | RecTypeCodePage             recordType = 66
  constant RecTypePls (line 45) | RecTypePls                  recordType = 77
  constant RecTypeDCon (line 46) | RecTypeDCon                 recordType = 80
  constant RecTypeDConRef (line 47) | RecTypeDConRef              recordType = 81
  constant RecTypeDConName (line 48) | RecTypeDConName             recordType = 82
  constant RecTypeDefColWidth (line 49) | RecTypeDefColWidth          recordType = 85
  constant RecTypeXCT (line 50) | RecTypeXCT                  recordType = 89
  constant RecTypeCRN (line 51) | RecTypeCRN                  recordType = 90
  constant RecTypeFileSharing (line 52) | RecTypeFileSharing          recordType = 91
  constant RecTypeWriteAccess (line 53) | RecTypeWriteAccess          recordType = 92
  constant RecTypeObj (line 54) | RecTypeObj                  recordType = 93
  constant RecTypeUncalced (line 55) | RecTypeUncalced             recordType = 94
  constant RecTypeCalcSaveRecalc (line 56) | RecTypeCalcSaveRecalc       recordType = 95
  constant RecTypeTemplate (line 57) | RecTypeTemplate             recordType = 96
  constant RecTypeIntl (line 58) | RecTypeIntl                 recordType = 97
  constant RecTypeObjProtect (line 59) | RecTypeObjProtect           recordType = 99
  constant RecTypeColInfo (line 60) | RecTypeColInfo              recordType = 125
  constant RecTypeGuts (line 61) | RecTypeGuts                 recordType = 128
  constant RecTypeWsBool (line 62) | RecTypeWsBool               recordType = 129
  constant RecTypeGridSet (line 63) | RecTypeGridSet              recordType = 130
  constant RecTypeHCenter (line 64) | RecTypeHCenter              recordType = 131
  constant RecTypeVCenter (line 65) | RecTypeVCenter              recordType = 132
  constant RecTypeBoundSheet8 (line 66) | RecTypeBoundSheet8          recordType = 133
  constant RecTypeWriteProtect (line 67) | RecTypeWriteProtect         recordType = 134
  constant RecTypeCountry (line 68) | RecTypeCountry              recordType = 140
  constant RecTypeHideObj (line 69) | RecTypeHideObj              recordType = 141
  constant RecTypeSort (line 70) | RecTypeSort                 recordType = 144
  constant RecTypePalette (line 71) | RecTypePalette              recordType = 146
  constant RecTypeSync (line 72) | RecTypeSync                 recordType = 151
  constant RecTypeLPr (line 73) | RecTypeLPr                  recordType = 152
  constant RecTypeDxGCol (line 74) | RecTypeDxGCol               recordType = 153
  constant RecTypeFnGroupName (line 75) | RecTypeFnGroupName          recordType = 154
  constant RecTypeFilterMode (line 76) | RecTypeFilterMode           recordType = 155
  constant RecTypeBuiltInFnGroupCount (line 77) | RecTypeBuiltInFnGroupCount  recordType = 156
  constant RecTypeAutoFilterInfo (line 78) | RecTypeAutoFilterInfo       recordType = 157
  constant RecTypeAutoFilter (line 79) | RecTypeAutoFilter           recordType = 158
  constant RecTypeScl (line 80) | RecTypeScl                  recordType = 160
  constant RecTypeSetup (line 81) | RecTypeSetup                recordType = 161
  constant RecTypeScenMan (line 82) | RecTypeScenMan              recordType = 174
  constant RecTypeSCENARIO (line 83) | RecTypeSCENARIO             recordType = 175
  constant RecTypeSxView (line 84) | RecTypeSxView               recordType = 176
  constant RecTypeSxvd (line 85) | RecTypeSxvd                 recordType = 177
  constant RecTypeSXVI (line 86) | RecTypeSXVI                 recordType = 178
  constant RecTypeSxIvd (line 87) | RecTypeSxIvd                recordType = 180
  constant RecTypeSXLI (line 88) | RecTypeSXLI                 recordType = 181
  constant RecTypeSXPI (line 89) | RecTypeSXPI                 recordType = 182
  constant RecTypeDocRoute (line 90) | RecTypeDocRoute             recordType = 184
  constant RecTypeRecipName (line 91) | RecTypeRecipName            recordType = 185
  constant RecTypeMulRk (line 92) | RecTypeMulRk                recordType = 189
  constant RecTypeMulBlank (line 93) | RecTypeMulBlank             recordType = 190
  constant RecTypeMms (line 94) | RecTypeMms                  recordType = 193
  constant RecTypeSXDI (line 95) | RecTypeSXDI                 recordType = 197
  constant RecTypeSXDB (line 96) | RecTypeSXDB                 recordType = 198
  constant RecTypeSXFDB (line 97) | RecTypeSXFDB                recordType = 199
  constant RecTypeSXDBB (line 98) | RecTypeSXDBB                recordType = 200
  constant RecTypeSXNum (line 99) | RecTypeSXNum                recordType = 201
  constant RecTypeSxBool (line 100) | RecTypeSxBool               recordType = 202
  constant RecTypeSxErr (line 101) | RecTypeSxErr                recordType = 203
  constant RecTypeSXInt (line 102) | RecTypeSXInt                recordType = 204
  constant RecTypeSXString (line 103) | RecTypeSXString             recordType = 205
  constant RecTypeSXDtr (line 104) | RecTypeSXDtr                recordType = 206
  constant RecTypeSxNil (line 105) | RecTypeSxNil                recordType = 207
  constant RecTypeSXTbl (line 106) | RecTypeSXTbl                recordType = 208
  constant RecTypeSXTBRGIITM (line 107) | RecTypeSXTBRGIITM           recordType = 209
  constant RecTypeSxTbpg (line 108) | RecTypeSxTbpg               recordType = 210
  constant RecTypeObProj (line 109) | RecTypeObProj               recordType = 211
  constant RecTypeSXStreamID (line 110) | RecTypeSXStreamID           recordType = 213
  constant RecTypeDBCell (line 111) | RecTypeDBCell               recordType = 215
  constant RecTypeSXRng (line 112) | RecTypeSXRng                recordType = 216
  constant RecTypeSxIsxoper (line 113) | RecTypeSxIsxoper            recordType = 217
  constant RecTypeBookBool (line 114) | RecTypeBookBool             recordType = 218
  constant RecTypeDbOrParamQry (line 115) | RecTypeDbOrParamQry         recordType = 220
  constant RecTypeScenarioProtect (line 116) | RecTypeScenarioProtect      recordType = 221
  constant RecTypeOleObjectSize (line 117) | RecTypeOleObjectSize        recordType = 222
  constant RecTypeXF (line 118) | RecTypeXF                   recordType = 224
  constant RecTypeInterfaceHdr (line 119) | RecTypeInterfaceHdr         recordType = 225
  constant RecTypeInterfaceEnd (line 120) | RecTypeInterfaceEnd         recordType = 226
  constant RecTypeSXVS (line 121) | RecTypeSXVS                 recordType = 227
  constant RecTypeMergeCells (line 122) | RecTypeMergeCells           recordType = 229
  constant RecTypeBkHim (line 123) | RecTypeBkHim                recordType = 233
  constant RecTypeMsoDrawingGroup (line 124) | RecTypeMsoDrawingGroup      recordType = 235
  constant RecTypeMsoDrawing (line 125) | RecTypeMsoDrawing           recordType = 236
  constant RecTypeMsoDrawingSelection (line 126) | RecTypeMsoDrawingSelection  recordType = 237
  constant RecTypePhoneticInfo (line 127) | RecTypePhoneticInfo         recordType = 239
  constant RecTypeSxRule (line 128) | RecTypeSxRule               recordType = 240
  constant RecTypeSXEx (line 129) | RecTypeSXEx                 recordType = 241
  constant RecTypeSxFilt (line 130) | RecTypeSxFilt               recordType = 242
  constant RecTypeSxDXF (line 131) | RecTypeSxDXF                recordType = 244
  constant RecTypeSxItm (line 132) | RecTypeSxItm                recordType = 245
  constant RecTypeSxName (line 133) | RecTypeSxName               recordType = 246
  constant RecTypeSxSelect (line 134) | RecTypeSxSelect             recordType = 247
  constant RecTypeSXPair (line 135) | RecTypeSXPair               recordType = 248
  constant RecTypeSxFmla (line 136) | RecTypeSxFmla               recordType = 249
  constant RecTypeSxFormat (line 137) | RecTypeSxFormat             recordType = 251
  constant RecTypeSST (line 138) | RecTypeSST                  recordType = 252
  constant RecTypeLabelSst (line 139) | RecTypeLabelSst             recordType = 253
  constant RecTypeExtSST (line 140) | RecTypeExtSST               recordType = 255
  constant RecTypeSXVDEx (line 141) | RecTypeSXVDEx               recordType = 256
  constant RecTypeSXFormula (line 142) | RecTypeSXFormula            recordType = 259
  constant RecTypeSXDBEx (line 143) | RecTypeSXDBEx               recordType = 290
  constant RecTypeRRDInsDel (line 144) | RecTypeRRDInsDel            recordType = 311
  constant RecTypeRRDHead (line 145) | RecTypeRRDHead              recordType = 312
  constant RecTypeRRDChgCell (line 146) | RecTypeRRDChgCell           recordType = 315
  constant RecTypeRRTabID (line 147) | RecTypeRRTabID              recordType = 317
  constant RecTypeRRDRenSheet (line 148) | RecTypeRRDRenSheet          recordType = 318
  constant RecTypeRRSort (line 149) | RecTypeRRSort               recordType = 319
  constant RecTypeRRDMove (line 150) | RecTypeRRDMove              recordType = 320
  constant RecTypeRRFormat (line 151) | RecTypeRRFormat             recordType = 330
  constant RecTypeRRAutoFmt (line 152) | RecTypeRRAutoFmt            recordType = 331
  constant RecTypeRRInsertSh (line 153) | RecTypeRRInsertSh           recordType = 333
  constant RecTypeRRDMoveBegin (line 154) | RecTypeRRDMoveBegin         recordType = 334
  constant RecTypeRRDMoveEnd (line 155) | RecTypeRRDMoveEnd           recordType = 335
  constant RecTypeRRDInsDelBegin (line 156) | RecTypeRRDInsDelBegin       recordType = 336
  constant RecTypeRRDInsDelEnd (line 157) | RecTypeRRDInsDelEnd         recordType = 337
  constant RecTypeRRDConflict (line 158) | RecTypeRRDConflict          recordType = 338
  constant RecTypeRRDDefName (line 159) | RecTypeRRDDefName           recordType = 339
  constant RecTypeRRDRstEtxp (line 160) | RecTypeRRDRstEtxp           recordType = 340
  constant RecTypeLRng (line 161) | RecTypeLRng                 recordType = 351
  constant RecTypeUsesELFs (line 162) | RecTypeUsesELFs             recordType = 352
  constant RecTypeDSF (line 163) | RecTypeDSF                  recordType = 353
  constant RecTypeCUsr (line 164) | RecTypeCUsr                 recordType = 401
  constant RecTypeCbUsr (line 165) | RecTypeCbUsr                recordType = 402
  constant RecTypeUsrInfo (line 166) | RecTypeUsrInfo              recordType = 403
  constant RecTypeUsrExcl (line 167) | RecTypeUsrExcl              recordType = 404
  constant RecTypeFileLock (line 168) | RecTypeFileLock             recordType = 405
  constant RecTypeRRDInfo (line 169) | RecTypeRRDInfo              recordType = 406
  constant RecTypeBCUsrs (line 170) | RecTypeBCUsrs               recordType = 407
  constant RecTypeUsrChk (line 171) | RecTypeUsrChk               recordType = 408
  constant RecTypeUserBView (line 172) | RecTypeUserBView            recordType = 425
  constant RecTypeUserSViewBegin (line 173) | RecTypeUserSViewBegin       recordType = 426
  constant RecTypeUserSViewBeginChart (line 174) | RecTypeUserSViewBeginChart  recordType = 426
  constant RecTypeUserSViewEnd (line 175) | RecTypeUserSViewEnd         recordType = 427
  constant RecTypeRRDUserView (line 176) | RecTypeRRDUserView          recordType = 428
  constant RecTypeQsi (line 177) | RecTypeQsi                  recordType = 429
  constant RecTypeSupBook (line 178) | RecTypeSupBook              recordType = 430
  constant RecTypeProt4Rev (line 179) | RecTypeProt4Rev             recordType = 431
  constant RecTypeCondFmt (line 180) | RecTypeCondFmt              recordType = 432
  constant RecTypeCF (line 181) | RecTypeCF                   recordType = 433
  constant RecTypeDVal (line 182) | RecTypeDVal                 recordType = 434
  constant RecTypeDConBin (line 183) | RecTypeDConBin              recordType = 437
  constant RecTypeTxO (line 184) | RecTypeTxO                  recordType = 438
  constant RecTypeRefreshAll (line 185) | RecTypeRefreshAll           recordType = 439
  constant RecTypeHLink (line 186) | RecTypeHLink                recordType = 440
  constant RecTypeLel (line 187) | RecTypeLel                  recordType = 441
  constant RecTypeCodeName (line 188) | RecTypeCodeName             recordType = 442
  constant RecTypeSXFDBType (line 189) | RecTypeSXFDBType            recordType = 443
  constant RecTypeProt4RevPass (line 190) | RecTypeProt4RevPass         recordType = 444
  constant RecTypeObNoMacros (line 191) | RecTypeObNoMacros           recordType = 445
  constant RecTypeDv (line 192) | RecTypeDv                   recordType = 446
  constant RecTypeExcel9File (line 193) | RecTypeExcel9File           recordType = 448
  constant RecTypeRecalcID (line 194) | RecTypeRecalcID             recordType = 449
  constant RecTypeEntExU2 (line 195) | RecTypeEntExU2              recordType = 450
  constant RecTypeDimensions (line 196) | RecTypeDimensions           recordType = 512
  constant RecTypeBlank (line 197) | RecTypeBlank                recordType = 513
  constant RecTypeNumber (line 198) | RecTypeNumber               recordType = 515
  constant RecTypeLabel (line 199) | RecTypeLabel                recordType = 516
  constant RecTypeBoolErr (line 200) | RecTypeBoolErr              recordType = 517
  constant RecTypeString (line 201) | RecTypeString               recordType = 519
  constant RecTypeRow (line 202) | RecTypeRow                  recordType = 520
  constant RecTypeIndex (line 203) | RecTypeIndex                recordType = 523
  constant RecTypeArray (line 204) | RecTypeArray                recordType = 545
  constant RecTypeDefaultRowHeight (line 205) | RecTypeDefaultRowHeight     recordType = 549
  constant RecTypeTable (line 206) | RecTypeTable                recordType = 566
  constant RecTypeWindow2 (line 207) | RecTypeWindow2              recordType = 574
  constant RecTypeRK (line 208) | RecTypeRK                   recordType = 638
  constant RecTypeStyle (line 209) | RecTypeStyle                recordType = 659
  constant RecTypeBigName (line 210) | RecTypeBigName              recordType = 1048
  constant RecTypeFormat (line 211) | RecTypeFormat               recordType = 1054
  constant RecTypeContinueBigName (line 212) | RecTypeContinueBigName      recordType = 1084
  constant RecTypeShrFmla (line 213) | RecTypeShrFmla              recordType = 1212
  constant RecTypeHLinkTooltip (line 214) | RecTypeHLinkTooltip         recordType = 2048
  constant RecTypeWebPub (line 215) | RecTypeWebPub               recordType = 2049
  constant RecTypeQsiSXTag (line 216) | RecTypeQsiSXTag             recordType = 2050
  constant RecTypeDBQueryExt (line 217) | RecTypeDBQueryExt           recordType = 2051
  constant RecTypeExtString (line 218) | RecTypeExtString            recordType = 2052
  constant RecTypeTxtQry (line 219) | RecTypeTxtQry               recordType = 2053
  constant RecTypeQsir (line 220) | RecTypeQsir                 recordType = 2054
  constant RecTypeQsif (line 221) | RecTypeQsif                 recordType = 2055
  constant RecTypeRRDTQSIF (line 222) | RecTypeRRDTQSIF             recordType = 2056
  constant RecTypeBOF (line 223) | RecTypeBOF                  recordType = 2057
  constant RecTypeOleDbConn (line 224) | RecTypeOleDbConn            recordType = 2058
  constant RecTypeWOpt (line 225) | RecTypeWOpt                 recordType = 2059
  constant RecTypeSXViewEx (line 226) | RecTypeSXViewEx             recordType = 2060
  constant RecTypeSXTH (line 227) | RecTypeSXTH                 recordType = 2061
  constant RecTypeSXPIEx (line 228) | RecTypeSXPIEx               recordType = 2062
  constant RecTypeSXVDTEx (line 229) | RecTypeSXVDTEx              recordType = 2063
  constant RecTypeSXViewEx9 (line 230) | RecTypeSXViewEx9            recordType = 2064
  constant RecTypeContinueFrt (line 231) | RecTypeContinueFrt          recordType = 2066
  constant RecTypeRealTimeData (line 232) | RecTypeRealTimeData         recordType = 2067
  constant RecTypeChartFrtInfo (line 233) | RecTypeChartFrtInfo         recordType = 2128
  constant RecTypeFrtWrapper (line 234) | RecTypeFrtWrapper           recordType = 2129
  constant RecTypeStartBlock (line 235) | RecTypeStartBlock           recordType = 2130
  constant RecTypeEndBlock (line 236) | RecTypeEndBlock             recordType = 2131
  constant RecTypeStartObject (line 237) | RecTypeStartObject          recordType = 2132
  constant RecTypeEndObject (line 238) | RecTypeEndObject            recordType = 2133
  constant RecTypeCatLab (line 239) | RecTypeCatLab               recordType = 2134
  constant RecTypeYMult (line 240) | RecTypeYMult                recordType = 2135
  constant RecTypeSXViewLink (line 241) | RecTypeSXViewLink           recordType = 2136
  constant RecTypePivotChartBits (line 242) | RecTypePivotChartBits       recordType = 2137
  constant RecTypeFrtFontList (line 243) | RecTypeFrtFontList          recordType = 2138
  constant RecTypeSheetExt (line 244) | RecTypeSheetExt             recordType = 2146
  constant RecTypeBookExt (line 245) | RecTypeBookExt              recordType = 2147
  constant RecTypeSXAddl (line 246) | RecTypeSXAddl               recordType = 2148
  constant RecTypeCrErr (line 247) | RecTypeCrErr                recordType = 2149
  constant RecTypeHFPicture (line 248) | RecTypeHFPicture            recordType = 2150
  constant RecTypeFeatHdr (line 249) | RecTypeFeatHdr              recordType = 2151
  constant RecTypeFeat (line 250) | RecTypeFeat                 recordType = 2152
  constant RecTypeDataLabExt (line 251) | RecTypeDataLabExt           recordType = 2154
  constant RecTypeDataLabExtContents (line 252) | RecTypeDataLabExtContents   recordType = 2155
  constant RecTypeCellWatch (line 253) | RecTypeCellWatch            recordType = 2156
  constant RecTypeFeatHdr11 (line 254) | RecTypeFeatHdr11            recordType = 2161
  constant RecTypeFeature11 (line 255) | RecTypeFeature11            recordType = 2162
  constant RecTypeDropDownObjIds (line 256) | RecTypeDropDownObjIds       recordType = 2164
  constant RecTypeContinueFrt11 (line 257) | RecTypeContinueFrt11        recordType = 2165
  constant RecTypeDConn (line 258) | RecTypeDConn                recordType = 2166
  constant RecTypeList12 (line 259) | RecTypeList12               recordType = 2167
  constant RecTypeFeature12 (line 260) | RecTypeFeature12            recordType = 2168
  constant RecTypeCondFmt12 (line 261) | RecTypeCondFmt12            recordType = 2169
  constant RecTypeCF12 (line 262) | RecTypeCF12                 recordType = 2170
  constant RecTypeCFEx (line 263) | RecTypeCFEx                 recordType = 2171
  constant RecTypeXFCRC (line 264) | RecTypeXFCRC                recordType = 2172
  constant RecTypeXFExt (line 265) | RecTypeXFExt                recordType = 2173
  constant RecTypeAutoFilter12 (line 266) | RecTypeAutoFilter12         recordType = 2174
  constant RecTypeContinueFrt12 (line 267) | RecTypeContinueFrt12        recordType = 2175
  constant RecTypeMDTInfo (line 268) | RecTypeMDTInfo              recordType = 2180
  constant RecTypeMDXStr (line 269) | RecTypeMDXStr               recordType = 2181
  constant RecTypeMDXTuple (line 270) | RecTypeMDXTuple             recordType = 2182
  constant RecTypeMDXSet (line 271) | RecTypeMDXSet               recordType = 2183
  constant RecTypeMDXProp (line 272) | RecTypeMDXProp              recordType = 2184
  constant RecTypeMDXKPI (line 273) | RecTypeMDXKPI               recordType = 2185
  constant RecTypeMDB (line 274) | RecTypeMDB                  recordType = 2186
  constant RecTypePLV (line 275) | RecTypePLV                  recordType = 2187
  constant RecTypeCompat12 (line 276) | RecTypeCompat12             recordType = 2188
  constant RecTypeDXF (line 277) | RecTypeDXF                  recordType = 2189
  constant RecTypeTableStyles (line 278) | RecTypeTableStyles          recordType = 2190
  constant RecTypeTableStyle (line 279) | RecTypeTableStyle           recordType = 2191
  constant RecTypeTableStyleElement (line 280) | RecTypeTableStyleElement    recordType = 2192
  constant RecTypeStyleExt (line 281) | RecTypeStyleExt             recordType = 2194
  constant RecTypeNamePublish (line 282) | RecTypeNamePublish          recordType = 2195
  constant RecTypeNameCmt (line 283) | RecTypeNameCmt              recordType = 2196
  constant RecTypeSortData (line 284) | RecTypeSortData             recordType = 2197
  constant RecTypeTheme (line 285) | RecTypeTheme                recordType = 2198
  constant RecTypeGUIDTypeLib (line 286) | RecTypeGUIDTypeLib          recordType = 2199
  constant RecTypeFnGrp12 (line 287) | RecTypeFnGrp12              recordType = 2200
  constant RecTypeNameFnGrp12 (line 288) | RecTypeNameFnGrp12          recordType = 2201
  constant RecTypeMTRSettings (line 289) | RecTypeMTRSettings          recordType = 2202
  constant RecTypeCompressPictures (line 290) | RecTypeCompressPictures     recordType = 2203
  constant RecTypeHeaderFooter (line 291) | RecTypeHeaderFooter         recordType = 2204
  constant RecTypeCrtLayout12 (line 292) | RecTypeCrtLayout12          recordType = 2205
  constant RecTypeCrtMlFrt (line 293) | RecTypeCrtMlFrt             recordType = 2206
  constant RecTypeCrtMlFrtContinue (line 294) | RecTypeCrtMlFrtContinue     recordType = 2207
  constant RecTypeForceFullCalculation (line 295) | RecTypeForceFullCalculation recordType = 2211
  constant RecTypeShapePropsStream (line 296) | RecTypeShapePropsStream     recordType = 2212
  constant RecTypeTextPropsStream (line 297) | RecTypeTextPropsStream      recordType = 2213
  constant RecTypeRichTextStream (line 298) | RecTypeRichTextStream       recordType = 2214
  constant RecTypeCrtLayout12A (line 299) | RecTypeCrtLayout12A         recordType = 2215
  constant RecTypeUnits (line 300) | RecTypeUnits                recordType = 4097
  constant RecTypeChart (line 301) | RecTypeChart                recordType = 4098
  constant RecTypeSeries (line 302) | RecTypeSeries               recordType = 4099
  constant RecTypeDataFormat (line 303) | RecTypeDataFormat           recordType = 4102
  constant RecTypeLineFormat (line 304) | RecTypeLineFormat           recordType = 4103
  constant RecTypeMarkerFormat (line 305) | RecTypeMarkerFormat         recordType = 4105
  constant RecTypeAreaFormat (line 306) | RecTypeAreaFormat           recordType = 4106
  constant RecTypePieFormat (line 307) | RecTypePieFormat            recordType = 4107
  constant RecTypeAttachedLabel (line 308) | RecTypeAttachedLabel        recordType = 4108
  constant RecTypeSeriesText (line 309) | RecTypeSeriesText           recordType = 4109
  constant RecTypeChartFormat (line 310) | RecTypeChartFormat          recordType = 4116
  constant RecTypeLegend (line 311) | RecTypeLegend               recordType = 4117
  constant RecTypeSeriesList (line 312) | RecTypeSeriesList           recordType = 4118
  constant RecTypeBar (line 313) | RecTypeBar                  recordType = 4119
  constant RecTypeLine (line 314) | RecTypeLine                 recordType = 4120
  constant RecTypePie (line 315) | RecTypePie                  recordType = 4121
  constant RecTypeArea (line 316) | RecTypeArea                 recordType = 4122
  constant RecTypeScatter (line 317) | RecTypeScatter              recordType = 4123
  constant RecTypeCrtLine (line 318) | RecTypeCrtLine              recordType = 4124
  constant RecTypeAxis (line 319) | RecTypeAxis                 recordType = 4125
  constant RecTypeTick (line 320) | RecTypeTick                 recordType = 4126
  constant RecTypeValueRange (line 321) | RecTypeValueRange           recordType = 4127
  constant RecTypeCatSerRange (line 322) | RecTypeCatSerRange          recordType = 4128
  constant RecTypeAxisLine (line 323) | RecTypeAxisLine             recordType = 4129
  constant RecTypeCrtLink (line 324) | RecTypeCrtLink              recordType = 4130
  constant RecTypeDefaultText (line 325) | RecTypeDefaultText          recordType = 4132
  constant RecTypeText (line 326) | RecTypeText                 recordType = 4133
  constant RecTypeFontX (line 327) | RecTypeFontX                recordType = 4134
  constant RecTypeObjectLink (line 328) | RecTypeObjectLink           recordType = 4135
  constant RecTypeFrame (line 329) | RecTypeFrame                recordType = 4146
  constant RecTypeBegin (line 330) | RecTypeBegin                recordType = 4147
  constant RecTypeEnd (line 331) | RecTypeEnd                  recordType = 4148
  constant RecTypePlotArea (line 332) | RecTypePlotArea             recordType = 4149
  constant RecTypeChart3d (line 333) | RecTypeChart3d              recordType = 4154
  constant RecTypePicF (line 334) | RecTypePicF                 recordType = 4156
  constant RecTypeDropBar (line 335) | RecTypeDropBar              recordType = 4157
  constant RecTypeRadar (line 336) | RecTypeRadar                recordType = 4158
  constant RecTypeSurf (line 337) | RecTypeSurf                 recordType = 4159
  constant RecTypeRadarArea (line 338) | RecTypeRadarArea            recordType = 4160
  constant RecTypeAxisParent (line 339) | RecTypeAxisParent           recordType = 4161
  constant RecTypeLegendException (line 340) | RecTypeLegendException      recordType = 4163
  constant RecTypeShtProps (line 341) | RecTypeShtProps             recordType = 4164
  constant RecTypeSerToCrt (line 342) | RecTypeSerToCrt             recordType = 4165
  constant RecTypeAxesUsed (line 343) | RecTypeAxesUsed             recordType = 4166
  constant RecTypeSBaseRef (line 344) | RecTypeSBaseRef             recordType = 4168
  constant RecTypeSerParent (line 345) | RecTypeSerParent            recordType = 4170
  constant RecTypeSerAuxTrend (line 346) | RecTypeSerAuxTrend          recordType = 4171
  constant RecTypeIFmtRecord (line 347) | RecTypeIFmtRecord           recordType = 4174
  constant RecTypePos (line 348) | RecTypePos                  recordType = 4175
  constant RecTypeAlRuns (line 349) | RecTypeAlRuns               recordType = 4176
  constant RecTypeBRAI (line 350) | RecTypeBRAI                 recordType = 4177
  constant RecTypeSerAuxErrBar (line 351) | RecTypeSerAuxErrBar         recordType = 4187
  constant RecTypeClrtClient (line 352) | RecTypeClrtClient           recordType = 4188
  constant RecTypeSerFmt (line 353) | RecTypeSerFmt               recordType = 4189
  constant RecTypeChart3DBarShape (line 354) | RecTypeChart3DBarShape      recordType = 4191
  constant RecTypeFbi (line 355) | RecTypeFbi                  recordType = 4192
  constant RecTypeBopPop (line 356) | RecTypeBopPop               recordType = 4193
  constant RecTypeAxcExt (line 357) | RecTypeAxcExt               recordType = 4194
  constant RecTypeDat (line 358) | RecTypeDat                  recordType = 4195
  constant RecTypePlotGrowth (line 359) | RecTypePlotGrowth           recordType = 4196
  constant RecTypeSIIndex (line 360) | RecTypeSIIndex              recordType = 4197
  constant RecTypeGelFrame (line 361) | RecTypeGelFrame             recordType = 4198
  constant RecTypeBopPopCustom (line 362) | RecTypeBopPopCustom         recordType = 4199
  constant RecTypeFbi2 (line 363) | RecTypeFbi2                 recordType = 4200

FILE: xls/sheets.go
  method List (line 15) | func (b *WorkBook) List() ([]string, error) {
  method ListHidden (line 26) | func (b *WorkBook) ListHidden() ([]string, error) {
  method Get (line 37) | func (b *WorkBook) Get(sheetName string) (grate.Collection, error) {
  method parseSheet (line 47) | func (b *WorkBook) parseSheet(s *boundSheet, ss int) (*commonxl.Sheet, e...

FILE: xls/simple_test.go
  function loadTestData (line 24) | func loadTestData(fn string, ff *commonxl.Formatter) (*commonxl.Sheet, e...
  function TestBasic (line 45) | func TestBasic(t *testing.T) {

FILE: xls/strings.go
  function decodeShortXLUnicodeString (line 12) | func decodeShortXLUnicodeString(raw []byte) (string, int, error) {
  function decodeXLUnicodeString (line 38) | func decodeXLUnicodeString(raw []byte) (string, int, error) {
  function decodeXLUnicodeRichExtendedString (line 64) | func decodeXLUnicodeRichExtendedString(r io.Reader) (string, error) {
  function parseSST (line 137) | func parseSST(recs []*rec) ([]string, error) {

FILE: xls/structs.go
  type header (line 8) | type header struct
  type rec (line 17) | type rec struct
  type boundSheet (line 23) | type boundSheet struct
  type shRow (line 31) | type shRow struct
  type shRef8 (line 40) | type shRef8 struct
  type shMulRK (line 46) | type shMulRK struct
  type RkRec (line 52) | type RkRec struct
  type shRK (line 57) | type shRK struct
  type RKNumber (line 64) | type RKNumber
    method IsInteger (line 66) | func (r RKNumber) IsInteger() bool {
    method Int (line 78) | func (r RKNumber) Int() int {
    method Float64 (line 89) | func (r RKNumber) Float64() float64 {
    method String (line 102) | func (r RKNumber) String() string {

FILE: xls/xls.go
  type WorkBook (line 26) | type WorkBook struct
    method IsProtected (line 48) | func (b *WorkBook) IsProtected() bool {
    method loadFromStream (line 79) | func (b *WorkBook) loadFromStream(raw []byte) error {
    method loadFromStreamWithDecryptor (line 83) | func (b *WorkBook) loadFromStreamWithDecryptor(raw []byte, dec crypto....
    method Close (line 162) | func (b *WorkBook) Close() error {
    method loadFromStream2 (line 175) | func (b *WorkBook) loadFromStream2(raw []byte, isDecrypted bool) error {
    method nextRecord (line 346) | func (b *WorkBook) nextRecord(raw []byte) (*rec, int, error) {
  function Open (line 52) | func Open(filename string) (grate.Source, error) {

FILE: xlsx/comp_test.go
  function TestAllFiles (line 10) | func TestAllFiles(t *testing.T) {

FILE: xlsx/sheets.go
  type Sheet (line 16) | type Sheet struct
    method parseSheet (line 29) | func (s *Sheet) parseSheet() error {

FILE: xlsx/simple_test.go
  function loadTestData (line 23) | func loadTestData(fn string, ff *commonxl.Formatter) (*commonxl.Sheet, e...
  function TestBasic (line 44) | func TestBasic(t *testing.T) {

FILE: xlsx/types.go
  type CellType (line 9) | type CellType
  constant BlankCellType (line 13) | BlankCellType         CellType = ""
  constant BooleanCellType (line 14) | BooleanCellType       CellType = "b"
  constant DateCellType (line 15) | DateCellType          CellType = "d"
  constant ErrorCellType (line 16) | ErrorCellType         CellType = "e"
  constant NumberCellType (line 17) | NumberCellType        CellType = "n"
  constant SharedStringCellType (line 18) | SharedStringCellType  CellType = "s"
  constant FormulaStringCellType (line 19) | FormulaStringCellType CellType = "str"
  constant InlineStringCellType (line 20) | InlineStringCellType  CellType = "inlineStr"
  type staticCellType (line 23) | type staticCellType
    method String (line 39) | func (s staticCellType) String() string {
  constant staticBlank (line 26) | staticBlank staticCellType = 0
  constant continueColumnMerged (line 29) | continueColumnMerged staticCellType = '→'
  constant endColumnMerged (line 31) | endColumnMerged staticCellType = '⇥'
  constant continueRowMerged (line 34) | continueRowMerged staticCellType = '↓'
  constant endRowMerged (line 36) | endRowMerged staticCellType = '⤓'
  function col2int (line 48) | func col2int(col string) int {
  function refToIndexes (line 57) | func refToIndexes(r string) (column, row int) {
  function getAttrs (line 82) | func getAttrs(attrs []xml.Attr, keys ...string) []string {

FILE: xlsx/workbook.go
  method parseRels (line 15) | func (d *Document) parseRels(dec *xml.Decoder, basedir string) error {
  method parseWorkbook (line 59) | func (d *Document) parseWorkbook(dec *xml.Decoder) error {
  method parseStyles (line 104) | func (d *Document) parseStyles(dec *xml.Decoder) error {
  method parseSharedStrings (line 184) | func (d *Document) parseSharedStrings(dec *xml.Decoder) error {

FILE: xlsx/xlsx.go
  type Document (line 20) | type Document struct
    method Close (line 34) | func (d *Document) Close() error {
    method openXML (line 135) | func (d *Document) openXML(name string) (*xml.Decoder, io.Closer, erro...
    method List (line 152) | func (d *Document) List() ([]string, error) {
    method Get (line 160) | func (d *Document) Get(sheetName string) (grate.Collection, error) {
  function Open (line 44) | func Open(filename string) (grate.Source, error) {
Condensed preview — 40 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (201K chars).
[
  {
    "path": ".github/workflows/go.yml",
    "chars": 473,
    "preview": "name: Go\n\non:\n  push:\n    branches: [ main ]\n  pull_request:\n    branches: [ main ]\n\njobs:\n\n  build:\n    runs-on: ubuntu"
  },
  {
    "path": ".gitignore",
    "chars": 46,
    "preview": "cmd/grate2tsv/results\ntestdata\n\n*.pprof\n*.pdf\n"
  },
  {
    "path": "LICENSE",
    "chars": 1066,
    "preview": "MIT License\n\nCopyright (c) 2021 Jeremy Jay\n\nPermission is hereby granted, free of charge, to any person obtaining a copy"
  },
  {
    "path": "README.md",
    "chars": 1617,
    "preview": "# grate\n\nA Go native tabular data extraction package. Currently supports `.xls`, `.xlsx`, `.csv`, `.tsv` formats.\n\n# Why"
  },
  {
    "path": "cmd/grate2tsv/main.go",
    "chars": 5763,
    "preview": "// Command grate2tsv is a highly parallel tabular data extraction tool. It's\n// probably not necessary in your situation"
  },
  {
    "path": "cmd/grater/main.go",
    "chars": 1177,
    "preview": "// 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\"string"
  },
  {
    "path": "commonxl/cell.go",
    "chars": 8665,
    "preview": "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 ty"
  },
  {
    "path": "commonxl/dates.go",
    "chars": 1961,
    "preview": "package commonxl\n\nimport (\n\t\"strings\"\n\t\"time\"\n)\n\n// ConvertToDate converts a floating-point value using the\n// Excel dat"
  },
  {
    "path": "commonxl/fmt.go",
    "chars": 6754,
    "preview": "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 st"
  },
  {
    "path": "commonxl/fmt_test.go",
    "chars": 3597,
    "preview": "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 comma"
  },
  {
    "path": "commonxl/formats.go",
    "chars": 8984,
    "preview": "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 E"
  },
  {
    "path": "commonxl/frac_test.go",
    "chars": 1502,
    "preview": "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 = ["
  },
  {
    "path": "commonxl/numbers.go",
    "chars": 1727,
    "preview": "package commonxl\n\nimport (\n\t\"math\"\n)\n\n// DecimalToWholeFraction converts a floating point value into a whole\n// number a"
  },
  {
    "path": "commonxl/sheet.go",
    "chars": 5583,
    "preview": "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 f"
  },
  {
    "path": "errs.go",
    "chars": 1167,
    "preview": "package grate\n\nimport \"errors\"\n\nvar (\n\t// configure at build time by adding go build arguments:\n\t//   -ldflags=\"-X githu"
  },
  {
    "path": "go.mod",
    "chars": 40,
    "preview": "module github.com/pbnjay/grate\n\ngo 1.16\n"
  },
  {
    "path": "grate.go",
    "chars": 3008,
    "preview": "// Package grate opens tabular data files (such as spreadsheets and delimited plaintext files)\n// and allows programmati"
  },
  {
    "path": "simple/csv.go",
    "chars": 1344,
    "preview": "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)"
  },
  {
    "path": "simple/simple.go",
    "chars": 2737,
    "preview": "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"
  },
  {
    "path": "simple/tsv.go",
    "chars": 1138,
    "preview": "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, Open"
  },
  {
    "path": "xls/cfb/cfb.go",
    "chars": 12043,
    "preview": "// Package cfb implements the Microsoft Compound File Binary File Format.\npackage cfb\n\n// https://docs.microsoft.com/en-"
  },
  {
    "path": "xls/cfb/interface.go",
    "chars": 1053,
    "preview": "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) ("
  },
  {
    "path": "xls/cfb/simple_test.go",
    "chars": 2445,
    "preview": "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"
  },
  {
    "path": "xls/cfb/slicereader.go",
    "chars": 2222,
    "preview": "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 transparen"
  },
  {
    "path": "xls/comp_test.go",
    "chars": 626,
    "preview": "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"
  },
  {
    "path": "xls/crypto/crypto.go",
    "chars": 3923,
    "preview": "// Package crypto implements excel encryption algorithms from the\n// MS-OFFCRYPTO design specs. Currently only standard/"
  },
  {
    "path": "xls/crypto/rc4.go",
    "chars": 3066,
    "preview": "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{"
  },
  {
    "path": "xls/hyperlinks.go",
    "chars": 3780,
    "preview": "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 []byt"
  },
  {
    "path": "xls/records.go",
    "chars": 40945,
    "preview": "package xls\n\nimport \"fmt\"\n\ntype recordType uint16\n\n// Record types defined by the XLS specification document, section 2."
  },
  {
    "path": "xls/sheets.go",
    "chars": 12969,
    "preview": "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."
  },
  {
    "path": "xls/simple_test.go",
    "chars": 1861,
    "preview": "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 testFilePa"
  },
  {
    "path": "xls/strings.go",
    "chars": 5352,
    "preview": "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 decodeShortXLU"
  },
  {
    "path": "xls/structs.go",
    "chars": 2604,
    "preview": "package xls\n\nimport (\n\t\"fmt\"\n\t\"math\"\n)\n\ntype header struct {\n\tVersion  uint16 // An unsigned integer that specifies the "
  },
  {
    "path": "xls/xls.go",
    "chars": 9355,
    "preview": "// Package xls implements the Microsoft Excel Binary File Format (.xls) Structure.\n// More specifically, it contains jus"
  },
  {
    "path": "xlsx/comp_test.go",
    "chars": 628,
    "preview": "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 := filepat"
  },
  {
    "path": "xlsx/sheets.go",
    "chars": 5417,
    "preview": "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/pbnja"
  },
  {
    "path": "xlsx/simple_test.go",
    "chars": 1808,
    "preview": "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 testFileP"
  },
  {
    "path": "xlsx/types.go",
    "chars": 1919,
    "preview": "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 s"
  },
  {
    "path": "xlsx/workbook.go",
    "chars": 5266,
    "preview": "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/pbnja"
  },
  {
    "path": "xlsx/xlsx.go",
    "chars": 3435,
    "preview": "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"
  }
]

About this extraction

This page contains the full source code of the pbnjay/grate GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 40 files (174.9 KB), approximately 59.3k tokens, and a symbol index with 577 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!