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 +} |