diff options
Diffstat (limited to 'lib/mintage/parser.go')
-rw-r--r-- | lib/mintage/parser.go | 318 |
1 files changed, 199 insertions, 119 deletions
diff --git a/lib/mintage/parser.go b/lib/mintage/parser.go index fb92e64..76f13e7 100644 --- a/lib/mintage/parser.go +++ b/lib/mintage/parser.go @@ -6,22 +6,7 @@ import ( "io" "strconv" "strings" - "unicode" -) - -type CoinType int - -const ( - TypeCirculated CoinType = iota - TypeNIFC - TypeProof - coinTypes -) - -const ( - _ = -iota - Unknown // Unknown mintage - Invalid // All mintages <= than this are invalid + "time" ) type SyntaxError struct { @@ -35,151 +20,248 @@ func (e SyntaxError) Error() string { e.file, e.linenr, e.expected, e.got) } -type Row struct { +type Data struct { + Standard []SRow + Commemorative []CRow +} + +type SRow struct { Year int Mintmark string - Cols [8]int + Mintages [typeCount][denoms]int } -type Set struct { - StartYear int - Tables [coinTypes][]Row +type CRow struct { + Year int + Mintmark string + Name string + Mintage [typeCount]int } -func (r Row) Label() string { - if r.Mintmark != "" { - return fmt.Sprintf("%d %s", r.Year, r.Mintmark) - } - return strconv.Itoa(r.Year) -} +const ( + TypeCirc = iota + TypeNIFC + TypeProof + typeCount +) -func Parse(reader io.Reader, file string) (Set, error) { - var ( - data Set - ctype CoinType = -1 - ) +const ( + Unknown = -iota - 1 + Invalid +) - scanner := bufio.NewScanner(reader) +const ( + denoms = 8 + ws = " \t" +) + +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), + } + + scanner := bufio.NewScanner(r) for linenr := 1; scanner.Scan(); linenr++ { - var mintmark struct { - s string - star bool + line := strings.Trim(scanner.Text(), ws) + if isBlankOrComment(line) { + continue } - line := scanner.Text() - tokens := strings.FieldsFunc(strings.TrimSpace(line), unicode.IsSpace) + if len(line) < 4 || !isNumeric(line[:4], false) { + return Data{}, SyntaxError{ + expected: "4-digit year", + got: line, + linenr: linenr, + file: file, + } + } - switch { - case len(tokens) == 0: - continue - case tokens[0] == "BEGIN": - if len(tokens)-1 != 1 { - return Set{}, SyntaxError{ - expected: "single argument to ‘BEGIN’", - got: fmt.Sprintf("%d arguments", len(tokens)-1), - file: file, + 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, } } - arg := tokens[1] + if line = line[1:]; len(line) == 0 { + return Data{}, SyntaxError{ + expected: "mintmark", + got: "end-of-line", + linenr: linenr, + file: file, + } + } - switch arg { - case "CIRC": - ctype = TypeCirculated - case "BU": - ctype = TypeNIFC - case "PROOF": - ctype = TypeProof + switch i := strings.IndexAny(line, ws); i { + case 0: + return Data{}, SyntaxError{ + expected: "mintmark", + got: "whitespace", + linenr: linenr, + file: file, + } + case -1: + mintmark = line default: - if !isNumeric(arg, false) { - return Set{}, SyntaxError{ - expected: "‘CIRC’, ‘BU’, ‘PROOF’, or a year", - got: arg, - file: file, + 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) } } - data.StartYear, _ = strconv.Atoi(arg) } - case isLabel(tokens[0]): - n := len(tokens[0]) - if n > 2 && tokens[0][n-2] == '*' { - mintmark.star = true - mintmark.s = tokens[0][:n-2] - } else { - mintmark.s = tokens[0][:n-1] + + data.Standard = append(data.Standard, row) + } else { + row := CRow{ + Year: year, + Mintmark: mintmark, } - tokens = tokens[1:] - if !isNumeric(tokens[0], true) && tokens[0] != "?" { - return Set{}, SyntaxError{ - expected: "mintage row after label", - got: tokens[0], - file: file, + line = strings.TrimLeft(line, ws) + if line[0] != '"' { + return Data{}, SyntaxError{ + expected: "string", + got: line, linenr: linenr, + file: file, } } - fallthrough - case isNumeric(tokens[0], true), tokens[0] == "?": - switch { - case ctype == -1: - return Set{}, SyntaxError{ - expected: "coin type declaration", - got: tokens[0], - file: file, + + 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 data.StartYear == 0: - return Set{}, SyntaxError{ - expected: "start year declaration", - got: tokens[0], + case 0: + return Data{}, SyntaxError{ + expected: "commemorated event", + got: "empty string", + linenr: linenr, file: file, + } + default: + row.Name, line = line[:i], line[i+1:] + } + + if len(line) != 0 { + return Data{}, SyntaxError{ + expected: "end-of-line", + got: line, linenr: linenr, + file: file, } } - numcoins := len(Row{}.Cols) - tokcnt := len(tokens) + 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++ + } - if tokcnt != numcoins { + 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 Set{}, SyntaxError{ - expected: fmt.Sprintf("%d mintage entries", numcoins), + return Data{}, SyntaxError{ + expected: fmt.Sprintf("%d mintage entries", typeCount), got: fmt.Sprintf("%d %s", tokcnt, word), - file: file, linenr: linenr, - } - } - - row := Row{Mintmark: mintmark.s} - if len(data.Tables[ctype]) == 0 { - row.Year = data.StartYear - } else { - rows := data.Tables[ctype] - row.Year = rows[len(rows)-1].Year - if row.Mintmark == "" || mintmark.star { - row.Year++ + file: file, } } for i, tok := range tokens { if tok == "?" { - row.Cols[i] = Unknown + row.Mintage[i] = Unknown } else { - row.Cols[i] = atoiWithDots(tok) + row.Mintage[i] = atoiWithDots(tok) } } - data.Tables[ctype] = append(data.Tables[ctype], row) - default: - return Set{}, SyntaxError{ - expected: "‘BEGIN’ directive or mintage row", - got: fmt.Sprintf("invalid token ‘%s’", tokens[0]), - file: file, - linenr: linenr, - } + + data.Commemorative = append(data.Commemorative, row) } } @@ -199,12 +281,6 @@ func isNumeric(s string, dot bool) bool { return true } -func isLabel(s string) bool { - n := len(s) - return (n > 2 && s[n-1] == ':' && s[n-2] == '*') || - (n > 1 && s[n-1] == ':') -} - func atoiWithDots(s string) int { n := 0 for _, ch := range s { @@ -215,3 +291,7 @@ func atoiWithDots(s string) int { } return n } + +func isBlankOrComment(s string) bool { + return len(s) == 0 || s[0] == '#' +} |