Skip to content

Commit

Permalink
Added support for parsing rf2616 and iso1806 date formats in json.
Browse files Browse the repository at this point in the history
  • Loading branch information
avrecko committed Mar 11, 2023
1 parent 0ef9a2f commit 3f17042
Show file tree
Hide file tree
Showing 4 changed files with 312 additions and 5 deletions.
21 changes: 20 additions & 1 deletion src/one/nio/serial/DateSerializer.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

package one.nio.serial;

import one.nio.util.DateParser;

import java.io.IOException;
import java.util.Date;

Expand Down Expand Up @@ -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
Expand Down
201 changes: 201 additions & 0 deletions src/one/nio/util/DateParser.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
26 changes: 22 additions & 4 deletions test/one/nio/serial/JsonTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
}
}
69 changes: 69 additions & 0 deletions test/one/nio/util/DateParserTest.java
Original file line number Diff line number Diff line change
@@ -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"));
}
}

0 comments on commit 3f17042

Please sign in to comment.