diff options
Diffstat (limited to 'mintages')
-rw-r--r-- | mintages/errors.go | 46 | ||||
-rw-r--r-- | mintages/parser.go | 124 |
2 files changed, 170 insertions, 0 deletions
diff --git a/mintages/errors.go b/mintages/errors.go new file mode 100644 index 0000000..a456c1a --- /dev/null +++ b/mintages/errors.go @@ -0,0 +1,46 @@ +package mintages + +import "fmt" + +type location struct { + file string + linenr int +} + +func (loc location) String() string { + return fmt.Sprintf("%s: %d", loc.file, loc.linenr) +} + +type BadTokenError struct { + location + token string +} + +func (e BadTokenError) Error() string { + return fmt.Sprintf("%s: unknown token ‘%s’", e.location, e.linenr, e.token) +} + +type ArgCountMismatchError struct { + location + token string + expected, got int +} + +func (e ArgCountMismatchError) Error() string { + var suffix string + if e.expected != 1 { + suffix = "s" + } + return fmt.Sprintf("%s: ‘%s’ token expects %d argument%s but got %d", + e.location, e.token, e.expected, suffix, e.got) +} + +type SyntaxError struct { + location + expected, got string +} + +func (e SyntaxError) Error() string { + return fmt.Sprintf("%s: syntax error: expected %s but got %s", + e.location, e.expected, e.got) +} diff --git a/mintages/parser.go b/mintages/parser.go new file mode 100644 index 0000000..5c83713 --- /dev/null +++ b/mintages/parser.go @@ -0,0 +1,124 @@ +package mintages + +import ( + "bufio" + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + "time" + "unicode" +) + +type coinset [8]int + +type Data struct { + StartYear int + Circ, Bu, Proof []coinset +} + +func ForCountry(code string) (Data, error) { + path := filepath.Join("data", "mintages", code) + f, err := os.Open(path) + if err != nil { + return Data{}, err + } + defer f.Close() + scanner := bufio.NewScanner(f) + + var ( + data Data // Our data struct + slice *[]coinset // Where to append mintages + ) + + for linenr := 1; scanner.Scan(); linenr++ { + line := scanner.Text() + tokens := strings.FieldsFunc(strings.TrimSpace(line), unicode.IsSpace) + + switch { + case len(tokens) == 0: + continue + case tokens[0] == "BEGIN": + if len(tokens) != 2 { + return Data{}, ArgCountMismatchError{ + token: tokens[0], + expected: 1, + got: len(tokens) - 1, + location: location{path, linenr}, + } + } + + arg := tokens[1] + + switch arg { + case "CIRC": + slice = &data.Circ + case "BU": + slice = &data.Bu + case "PROOF": + slice = &data.Proof + default: + if !isNumeric(arg, false) { + return Data{}, SyntaxError{ + expected: "‘CIRC’, ‘BU’, ‘PROOF’, or a year", + got: arg, + location: location{path, linenr}, + } + } + data.StartYear, _ = strconv.Atoi(arg) + } + case isNumeric(tokens[0], true): + numcoins := len(coinset{}) + tokcnt := len(tokens) + + if tokcnt != numcoins { + return Data{}, SyntaxError{ + expected: fmt.Sprintf("%d mintage entries", numcoins), + got: fmt.Sprintf("%d entries", tokcnt), + location: location{path, linenr}, + } + } + + var row coinset + for i, tok := range tokens { + row[i], _ = strconv.Atoi(strings.ReplaceAll(tok, ".", "")) + } + *slice = append(*slice, row) + default: + return Data{}, BadTokenError{ + token: tokens[0], + location: location{path, linenr}, + } + } + } + + /* Pad rows of ‘unknown’ mintages at the end of each set of mintages + for each year that we haven’t filled in info for. This avoids + things accidentally breaking if the new year comes and we forget + to add extra rows. */ + for _, ms := range [...]*[]coinset{&data.Circ, &data.Bu, &data.Proof} { + finalYear := len(*ms) + data.StartYear - 1 + missing := time.Now().Year() - finalYear + for i := 0; i < missing; i++ { + *ms = append(*ms, coinset{-1, -1, -1, -1, -1, -1, -1, -1}) + } + } + + return data, nil +} + +func isNumeric(s string, dot bool) bool { + for _, ch := range s { + switch ch { + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + case '.': + if !dot { + return false + } + default: + return false + } + } + return true +} |