From 2c726e551d90d20bcd2d78545c369864cc9038e5 Mon Sep 17 00:00:00 2001 From: Thomas Voss Date: Fri, 6 Jun 2025 01:32:41 +0200 Subject: Use CSV’s for mintages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mintage/parser.go | 368 +++++++++++++++----------------------------------- 1 file changed, 112 insertions(+), 256 deletions(-) (limited to 'src/mintage/parser.go') diff --git a/src/mintage/parser.go b/src/mintage/parser.go index 364b6e8..daeb05d 100644 --- a/src/mintage/parser.go +++ b/src/mintage/parser.go @@ -1,297 +1,153 @@ package mintage import ( - "bufio" - "fmt" + "encoding/csv" "io" + "os" + "path/filepath" "strconv" - "strings" - "time" ) -type SyntaxError struct { - expected, got string - file string - linenr int -} - -func (e SyntaxError) Error() string { - return fmt.Sprintf("%s:%d: syntax error: expected %s but got %s", - e.file, e.linenr, e.expected, e.got) -} - -type Data struct { - Standard []SRow - Commemorative []CRow -} +func Parse(country string) ([3]Data, error) { + if data, ok := cache[country]; ok { + return data, nil + } -type SRow struct { - Year int - Mintmark string - Mintages [typeCount][denoms]int -} + var ( + data [3]Data + err error + path = filepath.Join("data", "mintages", country) + ) -type CRow struct { - Year int - Mintmark string - Name string - Mintage [typeCount]int + data[TypeCirc].Standard, err = parseS(path + "-s-circ.csv") + if err != nil { + return data, err + } + data[TypeNifc].Standard, err = parseS(path + "-s-nifc.csv") + if err != nil { + return data, err + } + data[TypeProof].Standard, err = parseS(path + "-s-proof.csv") + if err != nil { + return data, err + } + data[TypeCirc].Commemorative, err = parseC(path + "-c-circ.csv") + if err != nil { + return data, err + } + data[TypeNifc].Commemorative, err = parseC(path + "-c-nifc.csv") + if err != nil { + return data, err + } + data[TypeProof].Commemorative, err = parseC(path + "-c-proof.csv") + if err == nil { + cache[country] = data + } + return data, err } -const ( - TypeCirc = iota - TypeNIFC - TypeProof - typeCount -) +func parseS(path string) ([]SRow, error) { + rows := make([]SRow, 0, 69) /* TODO: Compute number of rows */ -const ( - Unknown = -iota - 1 - Invalid -) + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer f.Close() -const ( - denoms = 8 - ws = " \t" -) + r := csv.NewReader(f) + r.Comment = '#' + r.FieldsPerRecord = 10 + r.ReuseRecord = true -func Parse(r io.Reader, file string) (Data, error) { - yearsSince := time.Now().Year() - 1999 + 1 - data := Data{ - Standard: make([]SRow, 0, yearsSince), - Commemorative: make([]CRow, 0, yearsSince), + /* Skip header */ + if _, err := r.Read(); err != nil { + return nil, err } - scanner := bufio.NewScanner(r) - for linenr := 1; scanner.Scan(); linenr++ { - line := strings.Trim(scanner.Text(), ws) - if isBlankOrComment(line) { - continue + for { + record, err := r.Read() + if err == io.EOF { + break } - - if len(line) < 4 || !isNumeric(line[:4], false) { - return Data{}, SyntaxError{ - expected: "4-digit year", - got: line, - linenr: linenr, - file: file, - } + if err != nil { + return nil, err } - var ( - commem bool - mintmark string - ) - year, _ := strconv.Atoi(line[:4]) - line = line[4:] - - if len(line) != 0 { - if strings.ContainsRune(ws, rune(line[0])) { - commem = true - goto out - } - if line[0] != '-' { - return Data{}, SyntaxError{ - expected: "end-of-line or mintmark", - got: line, - linenr: linenr, - file: file, - } - } + data := SRow{Mintmark: record[1]} - if line = line[1:]; len(line) == 0 { - return Data{}, SyntaxError{ - expected: "mintmark", - got: "end-of-line", - linenr: linenr, - file: file, - } - } + data.Year, err = strconv.Atoi(record[0]) + if err != nil { + return nil, err + } - switch i := strings.IndexAny(line, ws); i { - case 0: - return Data{}, SyntaxError{ - expected: "mintmark", - got: "whitespace", - linenr: linenr, - file: file, + for i, s := range record[2:] { + if s == "" { + data.Mintages[i] = Unknown + } else { + data.Mintages[i], err = strconv.Atoi(s) + if err != nil { + data.Mintages[i] = Invalid } - case -1: - mintmark = line - default: - mintmark, line = line[:i], line[i:] - commem = true } } - out: - - if !commem { - row := SRow{ - Year: year, - Mintmark: mintmark, - } - for i := range row.Mintages { - line = "" - for isBlankOrComment(line) { - if !scanner.Scan() { - return Data{}, SyntaxError{ - expected: "mintage row", - got: "end-of-file", - linenr: linenr, - file: file, - } - } - line = strings.Trim(scanner.Text(), ws) - linenr++ - } - - tokens := strings.FieldsFunc(line, func(r rune) bool { - return strings.ContainsRune(ws, r) - }) - if tokcnt := len(tokens); tokcnt != denoms { - word := "entries" - if tokcnt == 1 { - word = "entry" - } - return Data{}, SyntaxError{ - expected: fmt.Sprintf("%d mintage entries", denoms), - got: fmt.Sprintf("%d %s", tokcnt, word), - linenr: linenr, - file: file, - } - } - - for j, tok := range tokens { - if tok != "?" && !isNumeric(tok, true) { - return Data{}, SyntaxError{ - expected: "numeric mintage figure or ‘?’", - got: tok, - linenr: linenr, - file: file, - } - } - if tok == "?" { - row.Mintages[i][j] = Unknown - } else { - row.Mintages[i][j] = atoiWithDots(tok) - } - } - } + rows = append(rows, data) + } - data.Standard = append(data.Standard, row) - } else { - row := CRow{ - Year: year, - Mintmark: mintmark, - } - line = strings.TrimLeft(line, ws) - if line[0] != '"' { - return Data{}, SyntaxError{ - expected: "string", - got: line, - linenr: linenr, - file: file, - } - } + return rows, nil +} - line = line[1:] - switch i := strings.IndexByte(line, '"'); i { - case -1: - return Data{}, SyntaxError{ - expected: "closing quote", - got: "end-of-line", - linenr: linenr, - file: file, - } - case 0: - return Data{}, SyntaxError{ - expected: "commemorated event", - got: "empty string", - linenr: linenr, - file: file, - } - default: - row.Name, line = line[:i], line[i+1:] - } +func parseC(path string) ([]CRow, error) { + rows := make([]CRow, 0, 69) /* TODO: Compute number of rows */ - if len(line) != 0 { - return Data{}, SyntaxError{ - expected: "end-of-line", - got: line, - linenr: linenr, - file: file, - } - } + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer f.Close() - for isBlankOrComment(line) { - if !scanner.Scan() { - return Data{}, SyntaxError{ - expected: "mintage row", - got: "end-of-file", - linenr: linenr, - file: file, - } - } - line = strings.Trim(scanner.Text(), ws) - linenr++ - } + r := csv.NewReader(f) + r.Comment = '#' + r.FieldsPerRecord = 4 + r.ReuseRecord = true - tokens := strings.FieldsFunc(line, func(r rune) bool { - return strings.ContainsRune(ws, r) - }) - if tokcnt := len(tokens); tokcnt != typeCount { - word := "entries" - if tokcnt == 1 { - word = "entry" - } - return Data{}, SyntaxError{ - expected: fmt.Sprintf("%d mintage entries", typeCount), - got: fmt.Sprintf("%d %s", tokcnt, word), - linenr: linenr, - file: file, - } - } + /* Skip header */ + if _, err := r.Read(); err != nil { + return nil, err + } - for i, tok := range tokens { - if tok == "?" { - row.Mintage[i] = Unknown - } else { - row.Mintage[i] = atoiWithDots(tok) - } - } + for { + record, err := r.Read() + if err == io.EOF { + break + } + if err != nil { + return nil, err + } - data.Commemorative = append(data.Commemorative, row) + data := CRow{ + Name: record[1], + Mintmark: record[2], } - } - return data, nil -} + data.Year, err = strconv.Atoi(record[0]) + if err != nil { + return nil, err + } -func isNumeric(s string, dot bool) bool { - for _, ch := range s { - switch ch { - case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': - default: - if ch != '.' || !dot { - return false + s := record[3] + if s == "" { + data.Mintage = Unknown + } else { + data.Mintage, err = strconv.Atoi(s) + if err != nil { + data.Mintage = Invalid } } - } - return true -} -func atoiWithDots(s string) int { - n := 0 - for _, ch := range s { - if ch == '.' { - continue - } - n = n*10 + int(ch) - '0' + rows = append(rows, data) } - return n -} -func isBlankOrComment(s string) bool { - return len(s) == 0 || s[0] == '#' + return rows, nil } -- cgit v1.2.3