diff --git a/naturalnumber/naturalnumber.go b/naturalnumber/naturalnumber.go new file mode 100644 index 0000000..bd680fa --- /dev/null +++ b/naturalnumber/naturalnumber.go @@ -0,0 +1,64 @@ +package rfc8259naturalnumber + +import ( + "fmt" + + "sourcecode.social/reiver/go-opt" +) + +// NaturalNumber represents the numbers: +// ..., -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7,.... +// +// I.e., the set of positive-integers, negative-integers, and zero. +// +// From IETF RFC-8259 NaturalNumber represents the following: +// +// [ minus ] int +// +// This is part of the definition of: +// +// number = [ minus ] int [ frac ] [ exp ] +// +// Since a NaturalNumber is a common usage of a JSON number, NaturalNumber exists. +type NaturalNumber struct { + opt.Optional[string] +} + +func Nothing() NaturalNumber { + return NaturalNumber{opt.Nothing[string]()} +} + +func NegativeOne() NaturalNumber { + return Something("-1") +} + +func Zero() NaturalNumber { + return Something("0") +} + +func One() NaturalNumber { + return Something("1") +} + +func Something(value string) NaturalNumber { + return NaturalNumber{opt.Something(value)} +} + +func (receiver NaturalNumber) GoString() string { + switch receiver { + case Nothing(): + return "rfc8259naturalnumber.Nothing()" + case NegativeOne(): + return "rfc8259naturalnumber.NegativeOne()" + case Zero(): + return "rfc8259naturalnumber.Zero()" + case One(): + return "rfc8259naturalnumber.One()" + default: + value, found := receiver.Get() + if !found { + return fmt.Sprintf("--INTERNAL-ERROR--") + } + return fmt.Sprintf("rfc8259naturalnumber.Something(%#v)", value) + } +} diff --git a/naturalnumber/parse.go b/naturalnumber/parse.go new file mode 100644 index 0000000..a2dae02 --- /dev/null +++ b/naturalnumber/parse.go @@ -0,0 +1,91 @@ +package rfc8259naturalnumber + +import ( + "io" + + "sourcecode.social/reiver/go-opt" + + "sourcecode.social/reiver/go-rfc8259/errors" + "sourcecode.social/reiver/go-rfc8259/wholenumber" +) + +// Parse tries to parse a JSON natural-number literal. +// If it succeeds, then it return nil, and sets ‘dst’ to the parsed value. +// If it failed, it returns an error. +// +// Example usage: +// +// var rs io.RuneScaner +// +// // ... +// +// var value rfc8259naturalnumber.NaturalNumber +// err := rfc8259naturalnumber.Parse(rs, &value) +// +// if nil != err { +// return err +// } +// +// fmt.Printf("value = %#v\n", value) +func Parse(runescanner io.RuneScanner, dst *NaturalNumber) error { + if nil == runescanner { + return rfc8259errors.ErrNilRuneScanner + } + if nil == dst { + return rfc8259errors.ErrNilDestination + } + + var r rune + { + var err error + + r, _, err = runescanner.ReadRune() + if nil != err { + if io.EOF == err { + return rfc8259errors.ErrUnexpectedEndOfFile + } + + return rfc8259errors.ErrProblemReadingRune(err) + } + } + + var minusPart opt.Optional[rune] + var intPart rfc8259wholenumber.WholeNumber + + switch r { + case '-': + minusPart = opt.Something(r) + default: + if err := runescanner.UnreadRune(); nil != err { + return rfc8259errors.ErrProblemUnreadingRune(err, r) + } + } + + err := rfc8259wholenumber.Parse(runescanner, &intPart) + if nil != err { + return err + } + + var value string + { + var buffer [256]byte + var p []byte = buffer[0:0] + + minusPart.WhenSomething(func(value rune){ + p = append(p, string(value)...) + }) + + intPart.WhenSomething(func(value string){ + p = append(p, value...) + }) + + if len(p) < 1 { + + } + + value = string(p) + } + + *dst = Something(value) + return nil +} diff --git a/naturalnumber/parse_test.go b/naturalnumber/parse_test.go new file mode 100644 index 0000000..06d1125 --- /dev/null +++ b/naturalnumber/parse_test.go @@ -0,0 +1,290 @@ +package rfc8259naturalnumber_test + +import ( + "testing" + + "bytes" + "io" + + "sourcecode.social/reiver/go-utf8" + + "sourcecode.social/reiver/go-rfc8259/naturalnumber" +) + +func TestParse_success(t *testing.T) { + + tests := []struct{ + Value []byte + Expected rfc8259naturalnumber.NaturalNumber + }{ + { + Value: []byte("0"), + Expected: rfc8259naturalnumber.Zero(), + }, + + + + { + Value: []byte("1"), + Expected: rfc8259naturalnumber.One(), + }, + { + Value: []byte("-1"), + Expected: rfc8259naturalnumber.NegativeOne(), + }, + + + + { + Value: []byte("2"), + Expected: rfc8259naturalnumber.Something("2"), + }, + { + Value: []byte("-2"), + Expected: rfc8259naturalnumber.Something("-2"), + }, + + + + { + Value: []byte("3"), + Expected: rfc8259naturalnumber.Something("3"), + }, + { + Value: []byte("-3"), + Expected: rfc8259naturalnumber.Something("-3"), + }, + + + + { + Value: []byte("13"), + Expected: rfc8259naturalnumber.Something("13"), + }, + { + Value: []byte("-13"), + Expected: rfc8259naturalnumber.Something("-13"), + }, + + + + { + Value: []byte("13256278887989457651018865901401704640"), + Expected: rfc8259naturalnumber.Something("13256278887989457651018865901401704640"), + }, + { + Value: []byte("-13256278887989457651018865901401704640"), + Expected: rfc8259naturalnumber.Something("-13256278887989457651018865901401704640"), + }, + + + { + Value: []byte("123.45"), + Expected: rfc8259naturalnumber.Something("123"), + }, + { + Value: []byte("123 "), + Expected: rfc8259naturalnumber.Something("123"), + }, + { + Value: []byte("123,"), + Expected: rfc8259naturalnumber.Something("123"), + }, + { + Value: []byte("123e"), + Expected: rfc8259naturalnumber.Something("123"), + }, + { + Value: []byte("123E"), + Expected: rfc8259naturalnumber.Something("123"), + }, + + + + { + Value: []byte("-123.45"), + Expected: rfc8259naturalnumber.Something("-123"), + }, + { + Value: []byte("-123 "), + Expected: rfc8259naturalnumber.Something("-123"), + }, + { + Value: []byte("-123,"), + Expected: rfc8259naturalnumber.Something("-123"), + }, + { + Value: []byte("-123e"), + Expected: rfc8259naturalnumber.Something("-123"), + }, + { + Value: []byte("-123E"), + Expected: rfc8259naturalnumber.Something("-123"), + }, + } + + for testNumber, test := range tests { + + var reader io.Reader = bytes.NewReader(test.Value) + var runescanner io.RuneScanner = utf8.NewRuneScanner(reader) + + var actual rfc8259naturalnumber.NaturalNumber + err := rfc8259naturalnumber.Parse(runescanner, &actual) + + if nil != err { + t.Errorf("For test #%d, did not expect an error but actually got one.", testNumber) + t.Logf("ERROR: (%T) %s", err, err) + t.Logf("VALUE: %q", test.Value) + t.Logf("VALUE: %#v", test.Value) + continue + } + + { + expected := test.Expected + + if expected != actual { + t.Errorf("For test #%d, the actual value is not what was expected." , testNumber) + t.Logf("EXPECTED: %#v", expected) + t.Logf("ACTUAL: %#v", actual) + t.Logf("VALUE: %q", test.Value) + t.Logf("VALUE: %#v", test.Value) + continue + } + } + } +} + +func TestParse_failure(t *testing.T) { + + tests := []struct{ + Value []byte + ExpectedError string + }{ + { + Value: []byte(nil), + ExpectedError: "rfc8259: unexpected end-of-file", + }, + { + Value: []byte(""), + ExpectedError: "rfc8259: unexpected end-of-file", + }, + { + Value: []byte("-"), + ExpectedError: "rfc8259: unexpected end-of-file", + }, + + + + { + Value: []byte("\t"), + ExpectedError: `rfc8259: JSON parser encountered a problem — when trying to parse a whole-number, expected the first character to be '0', '1', '2', '3', '4', '5', '6', '7', '8', ot '9', but actually was '\t' (U+0009)`, + }, + { + Value: []byte("\n"), + ExpectedError: `rfc8259: JSON parser encountered a problem — when trying to parse a whole-number, expected the first character to be '0', '1', '2', '3', '4', '5', '6', '7', '8', ot '9', but actually was '\n' (U+000A)`, + }, + { + Value: []byte("\r"), + ExpectedError: `rfc8259: JSON parser encountered a problem — when trying to parse a whole-number, expected the first character to be '0', '1', '2', '3', '4', '5', '6', '7', '8', ot '9', but actually was '\r' (U+000D)`, + }, + { + Value: []byte(" "), + ExpectedError: `rfc8259: JSON parser encountered a problem — when trying to parse a whole-number, expected the first character to be '0', '1', '2', '3', '4', '5', '6', '7', '8', ot '9', but actually was ' ' (U+0020)`, + }, + { + Value: []byte("\""), + ExpectedError: `rfc8259: JSON parser encountered a problem — when trying to parse a whole-number, expected the first character to be '0', '1', '2', '3', '4', '5', '6', '7', '8', ot '9', but actually was '"' (U+0022)`, + }, + { + Value: []byte("f"), + ExpectedError: `rfc8259: JSON parser encountered a problem — when trying to parse a whole-number, expected the first character to be '0', '1', '2', '3', '4', '5', '6', '7', '8', ot '9', but actually was 'f' (U+0066)`, + }, + { + Value: []byte("n"), + ExpectedError: `rfc8259: JSON parser encountered a problem — when trying to parse a whole-number, expected the first character to be '0', '1', '2', '3', '4', '5', '6', '7', '8', ot '9', but actually was 'n' (U+006E)`, + }, + { + Value: []byte("t"), + ExpectedError: `rfc8259: JSON parser encountered a problem — when trying to parse a whole-number, expected the first character to be '0', '1', '2', '3', '4', '5', '6', '7', '8', ot '9', but actually was 't' (U+0074)`, + }, + + + + { + Value: []byte("\"name\""), + ExpectedError: `rfc8259: JSON parser encountered a problem — when trying to parse a whole-number, expected the first character to be '0', '1', '2', '3', '4', '5', '6', '7', '8', ot '9', but actually was '"' (U+0022)`, + }, + + + + { + Value: []byte("apple"), + ExpectedError: `rfc8259: JSON parser encountered a problem — when trying to parse a whole-number, expected the first character to be '0', '1', '2', '3', '4', '5', '6', '7', '8', ot '9', but actually was 'a' (U+0061)`, + }, + { + Value: []byte("banana"), + ExpectedError: `rfc8259: JSON parser encountered a problem — when trying to parse a whole-number, expected the first character to be '0', '1', '2', '3', '4', '5', '6', '7', '8', ot '9', but actually was 'b' (U+0062)`, + }, + { + Value: []byte("cherry"), + ExpectedError: `rfc8259: JSON parser encountered a problem — when trying to parse a whole-number, expected the first character to be '0', '1', '2', '3', '4', '5', '6', '7', '8', ot '9', but actually was 'c' (U+0063)`, + }, + + + + { + Value: []byte("ONCE TWICE THRICE FOURCE"), + ExpectedError: `rfc8259: JSON parser encountered a problem — when trying to parse a whole-number, expected the first character to be '0', '1', '2', '3', '4', '5', '6', '7', '8', ot '9', but actually was 'O' (U+004F)`, + }, + + + + { + Value: []byte("😈"), + ExpectedError: `rfc8259: JSON parser encountered a problem — when trying to parse a whole-number, expected the first character to be '0', '1', '2', '3', '4', '5', '6', '7', '8', ot '9', but actually was '😈' (U+1F608)`, + }, + } + + for testNumber, test := range tests { + + var reader io.Reader = bytes.NewReader(test.Value) + var runescanner io.RuneScanner = utf8.NewRuneScanner(reader) + + var actual rfc8259naturalnumber.NaturalNumber + err := rfc8259naturalnumber.Parse(runescanner, &actual) + + if nil == err { + t.Errorf("For test #%d, expected an error but did not actually get one.", testNumber) + t.Logf("VALUE: %q", test.Value) + t.Logf("VALUE: %#v", test.Value) + continue + } + + { + expected := rfc8259naturalnumber.Nothing() + + if expected != actual { + t.Errorf("For test #%d, the actual value is not what was expected." , testNumber) + t.Logf("EXPECTED: %#v", expected) + t.Logf("ACTUAL: %#v", actual) + t.Logf("VALUE: %q", test.Value) + t.Logf("VALUE: %#v", test.Value) + continue + } + } + + { + expected := test.ExpectedError + actual := err.Error() + + if expected != actual { + t.Errorf("For test #%d, the actual value is not what was expected." , testNumber) + t.Logf("EXPECTED: %#v", expected) + t.Logf("ACTUAL: %#v", actual) + t.Logf("VALUE: %q", test.Value) + t.Logf("VALUE: %#v", test.Value) + continue + } + } + } +}