Initial commit of tab formatted table convert app
commit
bf0f4b9eda
@ -0,0 +1,2 @@
|
|||||||
|
/tft-convert
|
||||||
|
/.idea/
|
@ -0,0 +1,66 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Document struct {
|
||||||
|
Header []string
|
||||||
|
Rows [][]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d Document) MarshalJSON() ([]byte, error) {
|
||||||
|
headers := keys(d.Header)
|
||||||
|
var ret strings.Builder
|
||||||
|
ret.WriteRune('[')
|
||||||
|
for i, row := range d.Rows {
|
||||||
|
ret.WriteRune('{')
|
||||||
|
|
||||||
|
for j, col := range row {
|
||||||
|
obj, err := json.Marshal(map[string]string{headers[j]: col})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to marshal %s: %w", headers[j], err)
|
||||||
|
}
|
||||||
|
ret.WriteString(strings.Trim(string(obj), "{}"))
|
||||||
|
if j < len(row)-1 {
|
||||||
|
ret.WriteRune(',')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if i < len(d.Rows)-1 {
|
||||||
|
ret.WriteString("},")
|
||||||
|
} else {
|
||||||
|
ret.WriteRune('}')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ret.WriteRune(']')
|
||||||
|
return []byte(ret.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Document) setHeader(h []string) error {
|
||||||
|
d.Header = h
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Document) addRow(row []string) error {
|
||||||
|
d.Rows = append(d.Rows, row)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func keys(list []string) []string {
|
||||||
|
keys := make([]string, 0, len(list))
|
||||||
|
for _, h := range list {
|
||||||
|
heading := h
|
||||||
|
index := 0
|
||||||
|
for slices.Contains(keys, heading) {
|
||||||
|
index++
|
||||||
|
heading = fmt.Sprintf("%s_%d", h, index)
|
||||||
|
}
|
||||||
|
keys = append(keys, heading)
|
||||||
|
}
|
||||||
|
return keys
|
||||||
|
}
|
@ -0,0 +1,98 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/csv"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
FormatCSV = "csv"
|
||||||
|
FormatJSON = "json"
|
||||||
|
FormatStream = "stream"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UnknownFormatError struct{ Format string }
|
||||||
|
|
||||||
|
func (e UnknownFormatError) Error() string {
|
||||||
|
return "unknown format: '" + e.Format + "'"
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if err := run(); err != nil {
|
||||||
|
_, _ = os.Stderr.WriteString("ERROR: " + err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func run() error {
|
||||||
|
var in string
|
||||||
|
var out string
|
||||||
|
var format string
|
||||||
|
var width int
|
||||||
|
|
||||||
|
flag.StringVar(&in, "in", "", "input file")
|
||||||
|
flag.StringVar(&out, "out", "", "output file")
|
||||||
|
flag.StringVar(&format, "format", "csv", "output format (csv,json,stream)")
|
||||||
|
flag.IntVar(&width, "width", DefaultTabWidth, "spaces per tab")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
var input io.ReadCloser = os.Stdin
|
||||||
|
var output io.WriteCloser = os.Stdout
|
||||||
|
|
||||||
|
if in != "" {
|
||||||
|
var err error
|
||||||
|
if input, err = os.Open(in); err != nil {
|
||||||
|
return fmt.Errorf("failed to get input %s: %w", in, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if out != "" {
|
||||||
|
var err error
|
||||||
|
if output, err = os.Create(out); err != nil {
|
||||||
|
return fmt.Errorf("failed to get output %s: %w", out, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p := Parser{Width: width}
|
||||||
|
var err error
|
||||||
|
switch format {
|
||||||
|
case FormatCSV:
|
||||||
|
var once sync.Once
|
||||||
|
writer := csv.NewWriter(output)
|
||||||
|
err = p.IterSlice(input, func(headers []string, values []string) error {
|
||||||
|
var err error
|
||||||
|
once.Do(func() { err = writer.Write(headers) })
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return writer.Write(values)
|
||||||
|
})
|
||||||
|
writer.Flush()
|
||||||
|
|
||||||
|
case FormatStream:
|
||||||
|
enc := json.NewEncoder(output)
|
||||||
|
err = p.IterMap(input, func(values map[string]string) error {
|
||||||
|
return enc.Encode(values)
|
||||||
|
})
|
||||||
|
|
||||||
|
case FormatJSON:
|
||||||
|
var doc Document
|
||||||
|
doc, err = p.Parse(input)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse input: %w",
|
||||||
|
errors.Join(err, input.Close(), output.Close()))
|
||||||
|
}
|
||||||
|
err = json.NewEncoder(output).Encode(doc)
|
||||||
|
|
||||||
|
default:
|
||||||
|
err = UnknownFormatError{Format: format}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.Join(err, input.Close(), output.Close())
|
||||||
|
}
|
@ -0,0 +1,196 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const DefaultTabWidth = 8
|
||||||
|
|
||||||
|
type Token struct {
|
||||||
|
Name string
|
||||||
|
Pos int
|
||||||
|
Length int
|
||||||
|
}
|
||||||
|
|
||||||
|
type Parser struct {
|
||||||
|
Width int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Parser) IterMap(r io.Reader, onValue func(map[string]string) error) error {
|
||||||
|
return p.IterSlice(r, func(headers, row []string) error {
|
||||||
|
return onValue(zip(headers, row))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Parser) IterSlice(r io.Reader, onValue func([]string, []string) error) error {
|
||||||
|
var headers []string
|
||||||
|
return iter{
|
||||||
|
Reader: r,
|
||||||
|
Width: p.width(),
|
||||||
|
OnHeaders: func(h []string) error {
|
||||||
|
headers = h
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
OnRow: func(row []string) error {
|
||||||
|
return onValue(headers, row)
|
||||||
|
},
|
||||||
|
}.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Parser) Parse(r io.Reader) (Document, error) {
|
||||||
|
var doc Document
|
||||||
|
return doc, iter{
|
||||||
|
Reader: r,
|
||||||
|
OnHeaders: doc.setHeader,
|
||||||
|
OnRow: doc.addRow,
|
||||||
|
Width: p.width(),
|
||||||
|
}.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Parser) width() int {
|
||||||
|
if p.Width == 0 {
|
||||||
|
return DefaultTabWidth
|
||||||
|
}
|
||||||
|
return p.Width
|
||||||
|
}
|
||||||
|
|
||||||
|
type iter struct {
|
||||||
|
Reader io.Reader
|
||||||
|
Width int
|
||||||
|
OnHeaders func([]string) error
|
||||||
|
OnRow func([]string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i iter) Run() error {
|
||||||
|
s := bufio.NewScanner(i.Reader)
|
||||||
|
var tokens []Token
|
||||||
|
if s.Scan() {
|
||||||
|
tokens = i.tokens(s.Text())
|
||||||
|
headers := make([]string, len(tokens), len(tokens))
|
||||||
|
for i, header := range tokens {
|
||||||
|
headers[i] = header.Name
|
||||||
|
}
|
||||||
|
if err := i.OnHeaders(headers); err != nil {
|
||||||
|
return fmt.Errorf("failed to handle headers: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := s.Err(); err != nil {
|
||||||
|
return fmt.Errorf("failed to read header line: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for s.Scan() {
|
||||||
|
if err := i.OnRow(i.parse(tokens, s.Text())); err != nil {
|
||||||
|
return fmt.Errorf("failed to handle values: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := s.Err(); err != nil {
|
||||||
|
return fmt.Errorf("failed to read input: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i iter) tokens(line string) []Token {
|
||||||
|
index := 0
|
||||||
|
pos := 0
|
||||||
|
tokens := make([]Token, 0)
|
||||||
|
word := make([]rune, 0, len([]rune(line)))
|
||||||
|
for _, char := range line {
|
||||||
|
if char != '\t' {
|
||||||
|
if len(word) == 0 {
|
||||||
|
index = pos
|
||||||
|
}
|
||||||
|
word = append(word, char)
|
||||||
|
pos += 1
|
||||||
|
} else {
|
||||||
|
if len(word) == 0 {
|
||||||
|
pos += i.Width
|
||||||
|
if len(tokens) > 0 {
|
||||||
|
tokens[len(tokens)-1].Length += i.Width
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
padding := i.Width - (len(word) % i.Width)
|
||||||
|
pos += padding
|
||||||
|
tokens = append(tokens, Token{
|
||||||
|
Name: string(word),
|
||||||
|
Length: len(word) + padding,
|
||||||
|
Pos: index,
|
||||||
|
})
|
||||||
|
word = word[len(word):]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(word) != 0 {
|
||||||
|
tokens = append(tokens, Token{
|
||||||
|
Name: string(word),
|
||||||
|
Length: len(word),
|
||||||
|
Pos: index,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i iter) parse(headings []Token, l string) []string {
|
||||||
|
line := removeTabs(i.Width, l)
|
||||||
|
values := make([]string, len(headings), len(headings))
|
||||||
|
var value string
|
||||||
|
for j, heading := range headings {
|
||||||
|
if len(line) >= heading.Pos+heading.Length {
|
||||||
|
if j >= len(headings)-1 {
|
||||||
|
value = line[heading.Pos:]
|
||||||
|
} else {
|
||||||
|
value = line[heading.Pos : heading.Pos+heading.Length]
|
||||||
|
}
|
||||||
|
} else if len(line) >= heading.Pos {
|
||||||
|
value = line[heading.Pos:]
|
||||||
|
} else {
|
||||||
|
value = ""
|
||||||
|
}
|
||||||
|
values[j] = strings.TrimSpace(value)
|
||||||
|
}
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
|
||||||
|
func zip[K comparable, V any](keys []K, values []V) map[K]V {
|
||||||
|
ret := make(map[K]V, len(keys))
|
||||||
|
for i, key := range keys {
|
||||||
|
if i < len(values) {
|
||||||
|
ret[key] = values[i]
|
||||||
|
} else {
|
||||||
|
ret[key] = *new(V)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeTabs(width int, line string) string {
|
||||||
|
var ret strings.Builder
|
||||||
|
var word []rune
|
||||||
|
for _, char := range line {
|
||||||
|
if char != '\t' {
|
||||||
|
word = append(word, char)
|
||||||
|
} else {
|
||||||
|
if len(word) == 0 {
|
||||||
|
ret.WriteString(spaces(width))
|
||||||
|
} else {
|
||||||
|
ret.WriteString(string(word) + spaces(width-(len(word)%width)))
|
||||||
|
word = make([]rune, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ret.WriteString(string(word))
|
||||||
|
return ret.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func spaces(length int) string {
|
||||||
|
n := max(length, 0)
|
||||||
|
ret := make([]rune, n)
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
ret[i] = ' '
|
||||||
|
}
|
||||||
|
return string(ret)
|
||||||
|
}
|
Loading…
Reference in New Issue