package iso8601 import ( "fmt" "testing" "time" ) func TestParse(t *testing.T) { for _, input := range []string{ // Fast path (20 bytes) "1987-12-16T23:45:12Z", "2006-01-02T15:04:05Z", "2000-02-29T23:59:59Z", // leap year "2020-02-29T23:59:59Z", // leap year "0000-01-01T00:00:00Z", "9999-12-31T23:59:59Z", // Fast path (24 bytes) "1987-12-16T23:45:12.123Z", "2006-01-02T15:04:05.123Z", "2000-02-29T23:59:59.123Z", "2020-02-29T23:59:59.123Z", "0000-01-01T00:00:00.000Z", "9999-12-31T23:59:59.999Z", // Fast path (30 bytes) "1987-12-16T23:45:12.123456789Z", "2006-01-02T15:04:05.123456789Z", "2000-02-29T23:59:59.123456789Z", "2020-02-29T23:59:59.123456789Z", "0000-01-01T00:00:00.000000000Z", "9999-12-31T23:59:59.999999999Z", // Slow path "2006-01-02T15:04:05.1Z", "2006-01-02T15:04:05.12Z", "2006-01-02T15:04:05.1234Z", "2006-01-02T15:04:05.12345Z", "2006-01-02T15:04:05.123456Z", "2006-01-02T15:04:05.1234567Z", "2006-01-02T15:04:05.12345678Z", "2021-10-16T07:55:07+10:00", "2021-10-16T07:55:07.1+10:00", "2021-10-16T07:55:07.12+10:00", "2021-10-16T07:55:07.123+10:00", "2021-10-16T07:55:07.1234+10:00", "2021-10-16T07:55:07.12345+10:00", "2021-10-16T07:55:07.123456+10:00", "2021-10-16T07:55:07.1234567+10:00", "2021-10-16T07:55:07.12345678+10:00", "2021-10-16T07:55:07.123456789+10:00", "2021-10-16T07:55:07-10:00", "2021-10-16T07:55:07.1-10:00", "2021-10-16T07:55:07.12-10:00", "2021-10-16T07:55:07.123-10:00", "2021-10-16T07:55:07.1234-10:00", "2021-10-16T07:55:07.12345-10:00", "2021-10-16T07:55:07.123456-10:00", "2021-10-16T07:55:07.1234567-10:00", "2021-10-16T07:55:07.12345678-10:00", "2021-10-16T07:55:07.123456789-10:00", } { t.Run(input, func(t *testing.T) { expect, err := time.Parse(time.RFC3339Nano, input) if err != nil { t.Fatal(err) } actual, err := Parse(input) if err != nil { t.Error(err) } else if !actual.Equal(expect) { t.Errorf("unexpected time: %v vs expected %v", actual, expect) } else if actual.Location().String() != expect.Location().String() { t.Errorf("unexpected timezone: %v vs expected %v", actual.Location().String(), expect.Location().String()) } }) } // Check ~4M YYYY-MM-DD dates in 20 byte form. for year := 0; year <= 9999; year++ { for month := 0; month <= 13; month++ { for day := 0; day <= 32; day++ { input := fmt.Sprintf("%04d-%02d-%02dT12:34:56Z", year, month, day) expect, expectErr := time.Parse(time.RFC3339Nano, input) actual, actualErr := Parse(input) if (expectErr != nil) != (actualErr != nil) { t.Errorf("unexpected error for %v: %v vs. %v expected", input, actualErr, expectErr) } else if !actual.Equal(expect) { t.Errorf("unexpected time for %v: %v vs. %v expected", input, actual, expect) } } } } // Check ~4M YYYY-MM-DD dates in 24 byte form. for year := 0; year <= 9999; year++ { for month := 0; month <= 13; month++ { for day := 0; day <= 32; day++ { input := fmt.Sprintf("%04d-%02d-%02dT12:34:56.789Z", year, month, day) expect, expectErr := time.Parse(time.RFC3339Nano, input) actual, actualErr := Parse(input) if (expectErr != nil) != (actualErr != nil) { t.Errorf("unexpected error for %v: %v vs. %v expected", input, actualErr, expectErr) } else if !actual.Equal(expect) { t.Errorf("unexpected time for %v: %v vs. %v expected", input, actual, expect) } } } } // Check ~4M YYYY-MM-DD dates in 30 byte form. for year := 0; year <= 9999; year++ { for month := 0; month <= 13; month++ { for day := 0; day <= 32; day++ { input := fmt.Sprintf("%04d-%02d-%02dT12:34:56.123456789Z", year, month, day) expect, expectErr := time.Parse(time.RFC3339Nano, input) actual, actualErr := Parse(input) if (expectErr != nil) != (actualErr != nil) { t.Errorf("unexpected error for %v: %v vs. %v expected", input, actualErr, expectErr) } else if !actual.Equal(expect) { t.Errorf("unexpected time for %v: %v vs. %v expected", input, actual, expect) } } } } // Check all ~1M HH:MM:SS times in 20 byte form. for hour := 0; hour < 100; hour++ { for minute := 0; minute < 100; minute++ { for second := 0; second < 100; second++ { input := fmt.Sprintf("2000-01-01T%02d:%02d:%02dZ", hour, minute, second) expect, expectErr := time.Parse(time.RFC3339Nano, input) actual, actualErr := Parse(input) if (expectErr != nil) != (actualErr != nil) { t.Errorf("unexpected error for %v: %v vs. %v expected", input, actualErr, expectErr) } else if !actual.Equal(expect) { t.Errorf("unexpected time for %v: %v vs. %v expected", input, actual, expect) } } } } // Check ~1M HH:MM:SS.MMM times in 24 byte form. for hour := 0; hour < 100; hour++ { for minute := 0; minute < 100; minute++ { for second := 0; second < 100; second++ { input := fmt.Sprintf("2000-01-01T%02d:%02d:%02d.123Z", hour, minute, second) expect, expectErr := time.Parse(time.RFC3339Nano, input) actual, actualErr := Parse(input) if (expectErr != nil) != (actualErr != nil) { t.Errorf("unexpected error for %v: %v vs. %v expected", input, actualErr, expectErr) } else if !actual.Equal(expect) { t.Errorf("unexpected time for %v: %v vs. %v expected", input, actual, expect) } } } } // Check ~1M HH:MM:SS.MMM times in 30 byte form. for hour := 0; hour < 100; hour++ { for minute := 0; minute < 100; minute++ { for second := 0; second < 100; second++ { input := fmt.Sprintf("2000-01-01T%02d:%02d:%02d.123456789Z", hour, minute, second) expect, expectErr := time.Parse(time.RFC3339Nano, input) actual, actualErr := Parse(input) if (expectErr != nil) != (actualErr != nil) { t.Errorf("unexpected error for %v: %v vs. %v expected", input, actualErr, expectErr) } else if !actual.Equal(expect) { t.Errorf("unexpected time for %v: %v vs. %v expected", input, actual, expect) } } } } // Check milliseconds. for millis := 1; millis < 1000; millis <<= 1 { input := fmt.Sprintf("2000-01-01T00:00:00.%03dZ", millis) expect, expectErr := time.Parse(time.RFC3339Nano, input) actual, actualErr := Parse(input) if (expectErr != nil) != (actualErr != nil) { t.Errorf("unexpected error for %v: %v vs. %v expected", input, actualErr, expectErr) } else if !actual.Equal(expect) { t.Errorf("unexpected time for %v: %v vs. %v expected", input, actual, expect) } } // Check nanoseconds. for nanos := 1; nanos < 1e9; nanos <<= 1 { input := fmt.Sprintf("2000-01-01T00:00:00.%09dZ", nanos) expect, expectErr := time.Parse(time.RFC3339Nano, input) actual, actualErr := Parse(input) if (expectErr != nil) != (actualErr != nil) { t.Errorf("unexpected error for %v: %v vs. %v expected", input, actualErr, expectErr) } else if !actual.Equal(expect) { t.Errorf("unexpected time for %v: %v vs. %v expected", input, actual, expect) } } // Check with trailing zeroes omitted. for n := 1; n < 1e9; n <<= 1 { input := fmt.Sprintf("2000-01-01T00:00:00.%dZ", n) expect, expectErr := time.Parse(time.RFC3339Nano, input) actual, actualErr := Parse(input) if (expectErr != nil) != (actualErr != nil) { t.Errorf("unexpected error for %v: %v vs. %v expected", input, actualErr, expectErr) } else if !actual.Equal(expect) { t.Errorf("unexpected time for %v: %v vs. %v expected", input, actual, expect) } } } func TestParseInvalid(t *testing.T) { for _, input := range []string{ // 20 bytes "XXXXXXXXXXXXXXXXXXXX", "00000000000000000000", "1900-02-29T00:00:00Z", // 28 days in month (not a leap year) "2021-02-29T00:00:00Z", // 28 days in month (not a leap year) "2021-02-30T00:00:00Z", // 28 days in month "2021-02-31T00:00:00Z", // 28 days in month "2021-04-31T00:00:00Z", // 30 days in month "2021-06-31T00:00:00Z", // 30 days in month "2021-09-31T00:00:00Z", // 30 days in month "2021-11-31T00:00:00Z", // 30 days in month "XXXX-13-01T00:00:00Z", // invalid year "2000-13-01T00:00:00Z", // invalid month (1) "2000-00-01T00:00:00Z", // invalid month (2) "2000-XX-01T00:00:00Z", // invalid month (3) "2000-12-32T00:00:00Z", // invalid day (1) "2000-12-00T00:00:00Z", // invalid day (2) "2000-12-XXT00:00:00Z", // invalid day (3) "2000-12-31T24:00:00Z", // invalid hour (1) "2000-12-31TXX:00:00Z", // invalid hour (2) "2000-12-31T23:60:00Z", // invalid minute (1) "2000-12-31T23:XX:00Z", // invalid minute (2) "2000-12-31T23:59:60Z", // invalid second (1) "2000-12-31T23:59:XXZ", // invalid second (2) "1999-01-01 23:45:00Z", // missing T separator "1999 01-01T23:45:00Z", // missing date separator (1) "1999-01 01T23:45:00Z", // missing date separator (2) "1999-01-01T23 45:00Z", // missing time separator (1) "1999-01-01T23:45 00Z", // missing time separator (2) "1999-01-01T23:45:00 ", // missing timezone "1999-01-01t23:45:00Z", // lowercase T "1999-01-01T23:45:00z", // lowercase Z "X999-01-01T23:45:00Z", // X in various positions "1X99-01-01T23:45:00Z", "19X9-01-01T23:45:00Z", "199X-01-01T23:45:00Z", "1999X01-01T23:45:00Z", "1999-X1-01T23:45:00Z", "1999-0X-01T23:45:00Z", "1999-01X01T23:45:00Z", "1999-01-X1T23:45:00Z", "1999-01-0XT23:45:00Z", "1999-01-01X23:45:00Z", "1999-01-01TX3:45:00Z", "1999-01-01T2X:45:00Z", "1999-01-01T23X45:00Z", "1999-01-01T23:X5:00Z", "1999-01-01T23:4X:00Z", "1999-01-01T23:45X00Z", "1999-01-01T23:45:X0Z", "1999-01-01T23:45:0XZ", "1999-01-01T23:45:00X", // 24 bytes "XXXXXXXXXXXXXXXXXXXXXXXX", "000000000000000000000000", "1900-02-29T00:00:00.123Z", // 28 days in month (not a leap year) "2021-02-29T00:00:00.123Z", // 28 days in month (not a leap year) "2021-02-30T00:00:00.123Z", // 28 days in month "2021-02-31T00:00:00.123Z", // 28 days in month "2021-04-31T00:00:00.123Z", // 30 days in month "2021-06-31T00:00:00.123Z", // 30 days in month "2021-09-31T00:00:00.123Z", // 30 days in month "2021-11-31T00:00:00.123Z", // 30 days in month "XXXX-13-01T00:00:00.123Z", // invalid year "2000-13-01T00:00:00.123Z", // invalid month (1) "2000-00-01T00:00:00.123Z", // invalid month (2) "2000-XX-01T00:00:00.123Z", // invalid month (3) "2000-12-32T00:00:00.123Z", // invalid day (1) "2000-12-00T00:00:00.123Z", // invalid day (2) "2000-12-XXT00:00:00.123Z", // invalid day (3) "2000-12-31T24:00:00.123Z", // invalid hour (1) "2000-12-31TXX:00:00.123Z", // invalid hour (2) "2000-12-31T23:60:00.123Z", // invalid minute (1) "2000-12-31T23:XX:00.123Z", // invalid minute (2) "2000-12-31T23:59:60.123Z", // invalid second (1) "2000-12-31T23:59:XX.123Z", // invalid second (2) "2000-12-31T23:59:59.XXXZ", // invalid millis "1999-01-01 23:45:00.123Z", // missing T separator "1999 01-01T23:45:00.123Z", // missing date separator (1) "1999-01 01T23:45:00.123Z", // missing date separator (2) "1999-01-01T23 45:00.123Z", // missing time separator (1) "1999-01-01T23:45 00.123Z", // missing time separator (2) "1999-01-01T23:45:00 123Z", // missing time separator (3) "1999-01-01T23:45:00.123 ", // missing timezone "1999-01-01t23:45:00.123Z", // lowercase T "1999-01-01T23:45:00.123z", // lowercase Z "X999-01-01T23:45:00.123Z", // X in various positions "1X99-01-01T23:45:00.123Z", "19X9-01-01T23:45:00.123Z", "199X-01-01T23:45:00.123Z", "1999X01-01T23:45:00.123Z", "1999-X1-01T23:45:00.123Z", "1999-0X-01T23:45:00.123Z", "1999-01X01T23:45:00.123Z", "1999-01-X1T23:45:00.123Z", "1999-01-0XT23:45:00.123Z", "1999-01-01X23:45:00.123Z", "1999-01-01TX3:45:00.123Z", "1999-01-01T2X:45:00.123Z", "1999-01-01T23X45:00.123Z", "1999-01-01T23:X5:00.123Z", "1999-01-01T23:4X:00.123Z", "1999-01-01T23:45X00.123Z", "1999-01-01T23:45:X0.123Z", "1999-01-01T23:45:0X.123Z", "1999-01-01T23:45:00X123Z", "1999-01-01T23:45:00.X23Z", "1999-01-01T23:45:00.1X3Z", "1999-01-01T23:45:00.12XZ", "1999-01-01T23:45:00.123X", // 30 bytes "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", "000000000000000000000000000000", "1900-02-29T00:00:00.123456789Z", // 28 days in month (not a leap year) "2021-02-29T00:00:00.123456789Z", // 28 days in month (not a leap year) "2021-02-30T00:00:00.123456789Z", // 28 days in month "2021-02-31T00:00:00.123456789Z", // 28 days in month "2021-04-31T00:00:00.123456789Z", // 30 days in month "2021-06-31T00:00:00.123456789Z", // 30 days in month "2021-09-31T00:00:00.123456789Z", // 30 days in month "2021-11-31T00:00:00.123456789Z", // 30 days in month "XXXX-13-01T00:00:00.123456789Z", // invalid year "2000-13-01T00:00:00.123456789Z", // invalid month (1) "2000-00-01T00:00:00.123456789Z", // invalid month (2) "2000-XX-01T00:00:00.123456789Z", // invalid month (3) "2000-12-32T00:00:00.123456789Z", // invalid day (1) "2000-12-00T00:00:00.123456789Z", // invalid day (2) "2000-12-XXT00:00:00.123456789Z", // invalid day (3) "2000-12-31T24:00:00.123456789Z", // invalid hour (1) "2000-12-31TXX:00:00.123456789Z", // invalid hour (2) "2000-12-31T23:60:00.123456789Z", // invalid minute (1) "2000-12-31T23:XX:00.123456789Z", // invalid minute (2) "2000-12-31T23:59:60.123456789Z", // invalid second (1) "2000-12-31T23:59:XX.123456789Z", // invalid second (2) "2000-12-31T23:59:59.XXXXXXXXXZ", // invalid nanos "1999-01-01 23:45:00.123456789Z", // missing T separator "1999 01-01T23:45:00.123456789Z", // missing date separator (1) "1999-01 01T23:45:00.123456789Z", // missing date separator (2) "1999-01-01T23 45:00.123456789Z", // missing time separator (1) "1999-01-01T23:45 00.123456789Z", // missing time separator (2) "1999-01-01T23:45:00 123456789Z", // missing time separator (3) "1999-01-01T23:45:00.123456789 ", // missing timezone "1999-01-01t23:45:00.123456789Z", // lowercase T "1999-01-01T23:45:00.123456789z", // lowercase Z "X999-01-01T23:45:00.123456789Z", // X in various positions "1X99-01-01T23:45:00.123456789Z", "19X9-01-01T23:45:00.123456789Z", "199X-01-01T23:45:00.123456789Z", "1999X01-01T23:45:00.123456789Z", "1999-X1-01T23:45:00.123456789Z", "1999-0X-01T23:45:00.123456789Z", "1999-01X01T23:45:00.123456789Z", "1999-01-X1T23:45:00.123456789Z", "1999-01-0XT23:45:00.123456789Z", "1999-01-01X23:45:00.123456789Z", "1999-01-01TX3:45:00.123456789Z", "1999-01-01T2X:45:00.123456789Z", "1999-01-01T23X45:00.123456789Z", "1999-01-01T23:X5:00.123456789Z", "1999-01-01T23:4X:00.123456789Z", "1999-01-01T23:45X00.123456789Z", "1999-01-01T23:45:X0.123456789Z", "1999-01-01T23:45:0X.123456789Z", "1999-01-01T23:45:00X123456789Z", "1999-01-01T23:45:00.X23456789Z", "1999-01-01T23:45:00.1X3456789Z", "1999-01-01T23:45:00.12X456789Z", "1999-01-01T23:45:00.123X56789Z", "1999-01-01T23:45:00.1234X6789Z", "1999-01-01T23:45:00.12345X789Z", "1999-01-01T23:45:00.123456X89Z", "1999-01-01T23:45:00.1234567X9Z", "1999-01-01T23:45:00.12345678XZ", "1999-01-01T23:45:00.123456789X", "2000-01-01T00:00:00.Z", // missing number after decimal point } { t.Run(input, func(t *testing.T) { ts, err := time.Parse(time.RFC3339Nano, input) if err == nil { t.Fatalf("expected time.Parse('%s') error, got %v", input, ts) } ts, actualErr := Parse(input) if (err != nil) != (actualErr != nil) { t.Fatalf("expected Parse('%s') error %v, got %v", input, err, actualErr) } }) } } func BenchmarkParse(b *testing.B) { for i := 0; i < b.N; i++ { Parse("2006-01-02T15:04:05Z") } } func BenchmarkParseMilliseconds(b *testing.B) { for i := 0; i < b.N; i++ { Parse("2006-01-02T15:04:05.123Z") } } func BenchmarkParseMicroseconds(b *testing.B) { for i := 0; i < b.N; i++ { Parse("2006-01-02T15:04:05.123456Z") } } func BenchmarkParseNanoseconds(b *testing.B) { for i := 0; i < b.N; i++ { Parse("2006-01-02T15:04:05.123456789Z") } } func BenchmarkParseInvalid(b *testing.B) { for i := 0; i < b.N; i++ { Parse("2006-01-02T15:04:05.XZ") } }