package mintages import ( "bufio" "fmt" "io" "strconv" "strings" "unicode" ) 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 Row struct { Label string Cols [8]int } type Data struct { StartYear int Circ, BU, Proof []Row } func Parse(reader io.Reader, file string) (Data, error) { var ( data Data // Our data struct slice *[]Row // Where to append mintages year int // The current year we are at ) scanner := bufio.NewScanner(reader) for linenr := 1; scanner.Scan(); linenr++ { var mintmark struct { s string star bool } line := scanner.Text() tokens := strings.FieldsFunc(strings.TrimSpace(line), unicode.IsSpace) switch { case len(tokens) == 0: continue case tokens[0] == "BEGIN": if len(tokens)-1 != 1 { return Data{}, SyntaxError{ expected: "single argument to ‘BEGIN’", got: fmt.Sprintf("%d arguments", len(tokens)-1), file: file, linenr: 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, file: file, linenr: linenr, } } data.StartYear, _ = strconv.Atoi(arg) } year = data.StartYear - 1 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] } tokens = tokens[1:] if !isNumeric(tokens[0], true) && tokens[0] != "?" { return Data{}, SyntaxError{ expected: "mintage row after label", got: tokens[0], file: file, linenr: linenr, } } fallthrough case isNumeric(tokens[0], true), tokens[0] == "?": switch { case slice == nil: return Data{}, SyntaxError{ expected: "coin type declaration", got: tokens[0], file: file, linenr: linenr, } case data.StartYear == 0: return Data{}, SyntaxError{ expected: "start year declaration", got: tokens[0], file: file, linenr: linenr, } } numcoins := len(Row{}.Cols) tokcnt := len(tokens) if tokcnt != numcoins { word := "entries" if tokcnt == 1 { word = "entry" } return Data{}, SyntaxError{ expected: fmt.Sprintf("%d mintage entries", numcoins), got: fmt.Sprintf("%d %s", tokcnt, word), file: file, linenr: linenr, } } var row Row switch { case mintmark.s == "": year++ row.Label = strconv.Itoa(year) case mintmark.star: year++ fallthrough default: row.Label = fmt.Sprintf("%d %s", year, mintmark.s) } for i, tok := range tokens { if tok == "?" { row.Cols[i] = -1 } else { row.Cols[i] = atoiWithDots(tok) } } *slice = append(*slice, row) default: return Data{}, SyntaxError{ expected: "‘BEGIN’ directive or mintage row", got: fmt.Sprintf("invalid token ‘%s’", tokens[0]), file: file, linenr: linenr, } } } 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 } func isLabel(s string) bool { n := len(s) switch { case len(s) > 2 && s[n-1] == ':' && s[n-2] == '*', len(s) > 1 && s[n-1] == ':': return true default: return false } } func atoiWithDots(s string) int { n := 0 for _, ch := range s { if ch == '.' { continue } n = n*10 + int(ch) - '0' } return n }