diff --git a/src/one/nio/serial/DateSerializer.java b/src/one/nio/serial/DateSerializer.java index 7ab04b9b..b8927e49 100755 --- a/src/one/nio/serial/DateSerializer.java +++ b/src/one/nio/serial/DateSerializer.java @@ -16,6 +16,8 @@ package one.nio.serial; +import one.nio.util.DateParser; + import java.io.IOException; import java.util.Date; @@ -54,7 +56,24 @@ public void toJson(Date obj, StringBuilder builder) { @Override public Date fromJson(JsonReader in) throws IOException { - return new Date(in.readLong()); + String numberOrParsableText = in.readString(); + numberOrParsableText = numberOrParsableText.trim(); + if (numberOrParsableText.length() == 0) { + return null; + } + char ch0 = numberOrParsableText.charAt(0); + boolean isNumber = ch0 == '-' || (ch0 >= '0' && ch0 <= '9'); + for (int i = 1; i < numberOrParsableText.length() && isNumber; i++) { + char ch1 = numberOrParsableText.charAt(i); + if (ch1 < '0' || ch1 > '9') { + isNumber = false; + } + } + if (isNumber) { + return new Date(Long.parseLong(numberOrParsableText)); + } else { + return new Date(DateParser.parse(numberOrParsableText)); + } } @Override diff --git a/src/one/nio/util/DateParser.java b/src/one/nio/util/DateParser.java new file mode 100644 index 00000000..ac3cfbd8 --- /dev/null +++ b/src/one/nio/util/DateParser.java @@ -0,0 +1,201 @@ +package one.nio.util; + +import java.time.ZoneOffset; +import java.time.ZonedDateTime; + +/** + * Parser for subset of rfc2616 (http) and subset of ISO 1806 date formats. + */ +public class DateParser { + + public static long parse(String input) { + if (input == null) { + throw new NullPointerException("Cannot provide null input to date parser."); + } + // simple detection for rfc2616 + if (input.endsWith("GMT")) { + return parseRfc2616(input); + } + return parseIso1806(input); + } + + private static long parseIso1806(String input) { + // expecting format + // DDDD-DD-DD'T'DD:DD:DD[.D*] + // DDDD-DD-DD'T'DD:DD:DD[.D*]Z + // DDDD-DD-DD'T'DD:DD:DD[.D*]+DD:DD + // DDDD-DD-DD'T'DD:DD:DD[.D*]-DD:DD + // DDDD-DD-DD'T'DD:DD:DD[.D*]+DDDD + // DDDD-DD-DD'T'DD:DD:DD[.D*]-DDDD + + if (input.charAt(10) != 'T') { + throw new IllegalArgumentException("Expecting T in position 10."); + } + + if (input.charAt(4) != '-' && input.charAt(7) != '-') { + throw new IllegalArgumentException("Expecting - at position 4 and 7."); + } + + if (input.charAt(13) != ':' && input.charAt(16) != ':') { + throw new IllegalArgumentException("Expecting : at position 13 and 16."); + } + + int year = parseNumber4(input, 0); + int month = parseNumber2(input, 5); + int day = parseNumber2(input, 8); + + int hour = parseNumber2(input, 11); + int minute = parseNumber2(input, 14); + int second = parseNumber2(input, 17); + + int extraPrecisionDigits = 0; + int nanoseconds = 0; + if (input.length() >= 20) { + if (input.charAt(19) == '.') { + for (int i = 20; i < input.length(); i++) { + final char ch = input.charAt(i); + if (ch >= '0' && ch <= '9') { + extraPrecisionDigits++; + } else { + break; + } + } + if (extraPrecisionDigits > 9) { + throw new IllegalArgumentException("Cannot parse more than 9 digits for subsecond time."); + } + + int multiplier = 100_000_000; + for (int i = 20; i < 20 + extraPrecisionDigits; i++) { + final char ch = input.charAt(i); + if (ch >= '0' && ch <= '9') { + nanoseconds += (ch - 48) * multiplier; + multiplier /= 10; + } + } + extraPrecisionDigits++; // incremented as used for offset in charAt next + } + int candidateForTimeZoneOffset = 19 + extraPrecisionDigits; + if (candidateForTimeZoneOffset < input.length()) { + // must have time zone + char chTz = input.charAt(candidateForTimeZoneOffset); + if (chTz == '+' || chTz == '-') { + // expecting format +DD:DD or -DD:DD or +DDDD -DDDD + int hours = parseNumber2(input, candidateForTimeZoneOffset + 1); + + int minutes; + if (input.charAt(candidateForTimeZoneOffset + 3) == ':') { + minutes = parseNumber2(input, candidateForTimeZoneOffset + 4); + } else { + minutes = parseNumber2(input, candidateForTimeZoneOffset + 3); + } + + ZoneOffset zoneOffset; + if (chTz == '-') { + zoneOffset = ZoneOffset.ofTotalSeconds(-hours * 60 * 60 - minutes * 60); + } else { + zoneOffset = ZoneOffset.ofTotalSeconds(hours * 60 * 60 + minutes * 60); + } + return ZonedDateTime.of(year, month, day, hour, minute, second, nanoseconds, zoneOffset).toInstant().toEpochMilli(); + } else if (chTz != 'Z') { + throw new IllegalArgumentException("Failed to parse timezone info."); + } + } + } + return ZonedDateTime.of(year, month, day, hour, minute, second, nanoseconds, ZoneOffset.UTC).toInstant().toEpochMilli(); + } + + private static long parseRfc2616(String input) { + // expecting format + // Sun, 06 Nov 1994 08:49:37 GMT + // CCC, DD CCC DDDD DD:DD:DD GMT + + if (input.charAt(3) != ',' || input.charAt(4) != ' ' || input.charAt(7) != ' ' || input.charAt(11) != ' ' + || input.charAt(16) != ' ' || input.charAt(25) != ' ') { + throw new IllegalArgumentException("Invalid or unsupported rfc2616 date expecting format 'CCC, DD CCC DDDD DD:DD:DD GMT'."); + } + + int day = parseNumber2(input, 5); + + char chM1 = input.charAt(8); + char chM2 = input.charAt(9); + char chM3 = input.charAt(10); + + // Apr, Aug + // Feb, + // Jan, Jun, Jul + // Mar, May + // Nov + // Oct + // Sep + // Dec + + int month = -1; + if (chM1 == 'J' && chM2 == 'a') { + month = 1; + } + if (chM1 == 'F' && chM2 == 'e') { + month = 2; + } + if (chM1 == 'M' && chM2 == 'a' && chM3 == 'r') { + month = 3; + } + if (chM1 == 'A' && chM2 == 'p') { + month = 4; + } + if (chM1 == 'M' && chM2 == 'a' && chM3 == 'y') { + month = 5; + } + if (chM1 == 'J' && chM2 == 'u' && chM3 == 'n') { + month = 6; + } + if (chM1 == 'J' && chM2 == 'u' && chM3 == 'l') { + month = 7; + } + if (chM1 == 'A' && chM2 == 'u') { + month = 8; + } + if (chM1 == 'S' && chM2 == 'e') { + month = 9; + } + if (chM1 == 'O' && chM2 == 'c') { + month = 10; + } + if (chM1 == 'N' && chM2 == 'o') { + month = 11; + } + if (chM1 == 'D' && chM2 == 'e') { + month = 12; + } + + if (month == -1) { + throw new IllegalArgumentException("Failed to parse month."); + } + + int year = parseNumber4(input, 12); + int hour = parseNumber2(input, 17); + int minute = parseNumber2(input, 20); + int second = parseNumber2(input, 23); + + return ZonedDateTime.of(year, month, day, hour, minute, second, 0, ZoneOffset.UTC).toInstant().toEpochMilli(); + } + + private static int parseNumber2(CharSequence offsetId, int pos) { + char ch1 = offsetId.charAt(pos); + char ch2 = offsetId.charAt(pos + 1); + if (ch1 < '0' || ch1 > '9' || ch2 < '0' || ch2 > '9') { + throw new IllegalArgumentException("non numeric characters found: " + offsetId); + } + return (ch1 - 48) * 10 + (ch2 - 48); + } + + private static int parseNumber4(CharSequence offsetId, int pos) { + char ch1 = offsetId.charAt(pos); + char ch2 = offsetId.charAt(pos + 1); + char ch3 = offsetId.charAt(pos + 2); + char ch4 = offsetId.charAt(pos + 3); + if (ch1 < '0' || ch1 > '9' || ch2 < '0' || ch2 > '9' || ch3 > '9' || ch3 < '0' || ch4 > '9' || ch4 < '0') { + throw new IllegalArgumentException("non numeric characters found: " + offsetId); + } + return (ch1 - 48) * 1000 + (ch2 - 48) * 100 + (ch3 - 48) * 10 + (ch4 - 48); + } +} diff --git a/test/one/nio/serial/JsonTest.java b/test/one/nio/serial/JsonTest.java index 4fed2247..a0ff8ad4 100755 --- a/test/one/nio/serial/JsonTest.java +++ b/test/one/nio/serial/JsonTest.java @@ -16,9 +16,13 @@ package one.nio.serial; +import org.junit.Assert; +import org.junit.Test; + import java.io.IOException; import java.io.Serializable; import java.util.Arrays; +import java.util.Date; import java.util.HashMap; public class JsonTest implements Serializable { @@ -28,17 +32,31 @@ public class JsonTest implements Serializable { put("someKey", "some \"Value\""); }}; - public static void main(String[] args) throws IOException { + @Test + public void basicTest() throws IOException { Object obj = Arrays.asList("abc", 1, 2.0, true, new JsonTest()); - System.out.println(Json.toJson(obj)); - + Assert.assertEquals("[\"abc\",1,2.0,true,{\"lng\":\"-9223372036854775808\",\"map\":{\"someKey\":\"some \\\"Value\\\"\"}}]", Json.toJson(obj)); TestObject object = new TestObject(); object.name = "Maxim"; - System.out.println(Json.toJson(object)); + Assert.assertEquals("{\"test_name\":\"Maxim\",\"date\":null}", Json.toJson(object)); + } + + @Test + public void testDateParsing() throws IOException, ClassNotFoundException { + Assert.assertEquals(0, Json.fromJson("{\"date\":\"0\"}", TestObject.class).date.getTime()); + Assert.assertEquals(123456789, Json.fromJson("{\"date\":123456789}", TestObject.class).date.getTime()); + Assert.assertEquals(-1678468117765L, Json.fromJson("{\"date\":\"-1678468117765\"}", TestObject.class).date.getTime()); + Assert.assertEquals(1678468117765L, Json.fromJson("{\"date\":\"1678468117765\"}", TestObject.class).date.getTime()); + Assert.assertEquals(1678561558935L, Json.fromJson("{\"date\":\"2023-03-11T19:05:58.935Z\"}", TestObject.class).date.getTime()); + Assert.assertEquals(1678561558935L, Json.fromJson("{\"date\":\"2023-03-11T20:05:58.935+01:00\"}", TestObject.class).date.getTime()); + Assert.assertEquals(1678561558000L, Json.fromJson("{\"date\":\"Sat, 11 Mar 2023 19:05:58 GMT\"}", TestObject.class).date.getTime()); + Assert.assertNull(Json.fromJson("{\"date\":\"\"}", TestObject.class).date); + Assert.assertNull(Json.fromJson("{\"date\":\" \"}", TestObject.class).date); } public static class TestObject implements Serializable { @JsonName("test_name") public String name; + public Date date; } } diff --git a/test/one/nio/util/DateParserTest.java b/test/one/nio/util/DateParserTest.java new file mode 100644 index 00000000..5368d08a --- /dev/null +++ b/test/one/nio/util/DateParserTest.java @@ -0,0 +1,69 @@ +package one.nio.util; + +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; + +public class DateParserTest { + + @Test + public void testParsing() { + // rfc2616 + Assert.assertEquals(757846177000L, DateParser.parse("Thu, 06 Jan 1994 08:49:37 GMT")); + Assert.assertEquals(760524577000L, DateParser.parse("Sun, 06 Feb 1994 08:49:37 GMT")); + Assert.assertEquals(762943777000L, DateParser.parse("Sun, 06 Mar 1994 08:49:37 GMT")); + Assert.assertEquals(765622177000L, DateParser.parse("Wed, 06 Apr 1994 08:49:37 GMT")); + Assert.assertEquals(768214177000L, DateParser.parse("Fri, 06 May 1994 08:49:37 GMT")); + Assert.assertEquals(770892577000L, DateParser.parse("Mon, 06 Jun 1994 08:49:37 GMT")); + Assert.assertEquals(773484577000L, DateParser.parse("Wed, 06 Jul 1994 08:49:37 GMT")); + Assert.assertEquals(776162977000L, DateParser.parse("Sat, 06 Aug 1994 08:49:37 GMT")); + Assert.assertEquals(778841377000L, DateParser.parse("Tue, 06 Sep 1994 08:49:37 GMT")); + Assert.assertEquals(781433377000L, DateParser.parse("Thu, 06 Oct 1994 08:49:37 GMT")); + Assert.assertEquals(784111777000L, DateParser.parse("Sun, 06 Nov 1994 08:49:37 GMT")); + Assert.assertEquals(786703777000L, DateParser.parse("Sun, 06 Dec 1994 08:49:37 GMT")); + + Assert.assertEquals(1677701552000L, DateParser.parse("Wed, 01 Mar 2023 20:12:32 GMT")); + Assert.assertEquals(1692785121000L, DateParser.parse("Wed, 23 Aug 2023 10:05:21 GMT")); + + // subset of iso1806 + Assert.assertEquals(1677701552000L, DateParser.parse("2023-03-01T20:12:32Z")); + Assert.assertEquals(1677701552000L, DateParser.parse("2023-03-01T21:12:32+01:00")); + Assert.assertEquals(1677699752000L, DateParser.parse("2023-03-01T21:12:32+01:30")); + Assert.assertEquals(1677710552000L, DateParser.parse("2023-03-01T21:12:32-01:30")); + Assert.assertEquals(1677701552000L, DateParser.parse("2023-03-01T21:12:32+0100")); + Assert.assertEquals(1677699752000L, DateParser.parse("2023-03-01T21:12:32+0130")); + Assert.assertEquals(1677710552000L, DateParser.parse("2023-03-01T21:12:32-0130")); + + // also support without Z, for extra compatibility + Assert.assertEquals(1677701552000L, DateParser.parse("2023-03-01T20:12:32")); + + // support millis + Assert.assertEquals(1692785121234L, DateParser.parse("2023-08-23T10:05:21.234")); + Assert.assertEquals(1692785121234L, DateParser.parse("2023-08-23T10:05:21.234Z")); + Assert.assertEquals(1692785121234L, DateParser.parse("2023-08-23T12:05:21.234+02:00")); + + // decided to support micros and nanos as well, they just get trimmed, for extra compatibility + Assert.assertEquals(1692785121234L, DateParser.parse("2023-08-23T10:05:21.234567Z")); + Assert.assertEquals(1692785121234L, DateParser.parse("2023-08-23T12:05:21.234567890+02:00")); + Assert.assertEquals(1660816887967L, DateParser.parse("2022-08-18T12:01:27.967875+0200")); + } + + @Test + @Ignore + public void nonSupportedFormats() { + // part of rfc2616 + // RFC 850, obsoleted by RFC 1036 + Assert.assertNotEquals(784111777000L, DateParser.parse("Sunday, 06-Nov-94 08:49:37 GMT")); + // ANSI C's asctime() format + Assert.assertNotEquals(784111777000L, DateParser.parse("Sun Nov 6 08:49:37 1994")); + + // other iso1806 and similar + Assert.assertNotEquals(1677701552000L, DateParser.parse("20230301T211232+01:00")); + Assert.assertNotEquals(1677701552000L, DateParser.parse("2023-03-01 20:12:32+00:00")); + Assert.assertNotEquals(1677701552000L, DateParser.parse("2023-03-01 21:12:32+01:00")); + Assert.assertNotEquals(1692785121234L, DateParser.parse("20230823T100521.234Z")); + Assert.assertNotEquals(1692785121234L, DateParser.parse("20230823T120521.234+02:00")); + Assert.assertNotEquals(1692785121234L, DateParser.parse("2023-08-23 10:05:21.234+00:00")); + Assert.assertNotEquals(1692785121234L, DateParser.parse("2023-08-23 12:05:21.234+02:00")); + } +} \ No newline at end of file