Skip to content

Commit ab5f98c

Browse files
committed
Add date-time data types and validators
1 parent 21c62a6 commit ab5f98c

File tree

4 files changed

+731
-0
lines changed

4 files changed

+731
-0
lines changed
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
package com.relogiclabs.json.schema.time;
2+
3+
import com.relogiclabs.json.schema.exception.InvalidDateTimeException;
4+
import lombok.Getter;
5+
import lombok.RequiredArgsConstructor;
6+
7+
import java.time.LocalDate;
8+
import java.util.Arrays;
9+
import java.util.HashMap;
10+
import java.util.Map;
11+
12+
import static com.relogiclabs.json.schema.internal.util.StringHelper.concat;
13+
import static com.relogiclabs.json.schema.message.ErrorCode.DCNF01;
14+
import static com.relogiclabs.json.schema.message.ErrorCode.DDAY03;
15+
import static com.relogiclabs.json.schema.message.ErrorCode.DDAY04;
16+
import static com.relogiclabs.json.schema.message.ErrorCode.DERA02;
17+
import static com.relogiclabs.json.schema.message.ErrorCode.DHUR03;
18+
import static com.relogiclabs.json.schema.message.ErrorCode.DHUR04;
19+
import static com.relogiclabs.json.schema.message.ErrorCode.DHUR05;
20+
import static com.relogiclabs.json.schema.message.ErrorCode.DHUR06;
21+
import static com.relogiclabs.json.schema.message.ErrorCode.DINV01;
22+
import static com.relogiclabs.json.schema.message.ErrorCode.DMIN03;
23+
import static com.relogiclabs.json.schema.message.ErrorCode.DMON05;
24+
import static com.relogiclabs.json.schema.message.ErrorCode.DSEC03;
25+
import static com.relogiclabs.json.schema.message.ErrorCode.DTAP02;
26+
import static com.relogiclabs.json.schema.message.ErrorCode.DUTC04;
27+
import static com.relogiclabs.json.schema.message.ErrorCode.DUTC05;
28+
import static com.relogiclabs.json.schema.message.ErrorCode.DWKD03;
29+
import static com.relogiclabs.json.schema.message.ErrorCode.DYAR03;
30+
import static java.time.DayOfWeek.FRIDAY;
31+
import static java.time.DayOfWeek.MONDAY;
32+
import static java.time.DayOfWeek.SATURDAY;
33+
import static java.time.DayOfWeek.SUNDAY;
34+
import static java.time.DayOfWeek.THURSDAY;
35+
import static java.time.DayOfWeek.TUESDAY;
36+
import static java.time.DayOfWeek.WEDNESDAY;
37+
import static org.apache.commons.lang3.StringUtils.removeEnd;
38+
39+
@RequiredArgsConstructor
40+
public class DateTimeContext {
41+
private static final int PIVOT_YEAR = 50;
42+
private static final int[] DAYS_IN_MONTH = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
43+
private static final Map<String, Integer> MONTHS = new HashMap<>();
44+
private static final Map<String, Integer> WEEKDAYS = new HashMap<>();
45+
46+
47+
static {
48+
addMonth("january", "jan", 1);
49+
addMonth("february", "feb", 2);
50+
addMonth("march", "mar", 3);
51+
addMonth("april", "apr", 4);
52+
addMonth("may", "may", 5);
53+
addMonth("june", "jun", 6);
54+
addMonth("july", "jul", 7);
55+
addMonth("august", "aug", 8);
56+
addMonth("september", "sep", 9);
57+
addMonth("october", "oct", 10);
58+
addMonth("november", "nov", 11);
59+
addMonth("december", "dec", 12);
60+
61+
addWeekday("sunday", "sun", SUNDAY.getValue());
62+
addWeekday("monday", "mon", MONDAY.getValue());
63+
addWeekday("tuesday", "tue", TUESDAY.getValue());
64+
addWeekday("wednesday", "wed", WEDNESDAY.getValue());
65+
addWeekday("thursday", "thu", THURSDAY.getValue());
66+
addWeekday("friday", "fri", FRIDAY.getValue());
67+
addWeekday("saturday", "sat", SATURDAY.getValue());
68+
}
69+
70+
private static void addMonth(String key1, String key2, int value) {
71+
MONTHS.put(key1, value);
72+
MONTHS.put(key2, value);
73+
}
74+
75+
private static void addWeekday(String key1, String key2, int value) {
76+
WEEKDAYS.put(key1, value);
77+
WEEKDAYS.put(key2, value);
78+
}
79+
80+
private static final int UNSET = -100;
81+
82+
private int era = UNSET;
83+
private int year = UNSET;
84+
private int month = UNSET;
85+
private int weekday = UNSET;
86+
private int day = UNSET;
87+
private int amPm = UNSET;
88+
private int hour = UNSET;
89+
private int minute = UNSET;
90+
private int second = UNSET;
91+
private int fraction = UNSET;
92+
private int utcOffsetHour = UNSET;
93+
private int utcOffsetMinute = UNSET;
94+
95+
@Getter
96+
public final DateTimeType type;
97+
98+
public void setEra(String era) {
99+
var eraNum = switch(era.toUpperCase()) {
100+
case "BC" -> 1;
101+
case "AD" -> 2;
102+
default -> throw new InvalidDateTimeException(DERA02,
103+
concat("Invalid ", type, " era input"));
104+
};
105+
this.era = checkField(this.era, eraNum);
106+
}
107+
108+
public void setYear(int year, int digitNum) {
109+
if(year < 1 || year > 9999) throw new InvalidDateTimeException(DYAR03,
110+
concat("Invalid ", type, " year out of range"));
111+
year = digitNum <= 2 ? toFourDigitYear(year) : year;
112+
this.year = checkField(this.year, year);
113+
}
114+
115+
public void setMonth(String month) {
116+
var monthNum = MONTHS.get(month.toLowerCase());
117+
this.month = checkField(this.month, monthNum);
118+
}
119+
120+
public void setMonth(int month) {
121+
if(month < 1 || month > 12) throw new InvalidDateTimeException(DMON05,
122+
concat("Invalid ", type, " month out of range"));
123+
this.month = checkField(this.month, month);
124+
}
125+
126+
public void setWeekday(String weekday) {
127+
var dayOfWeek = WEEKDAYS.get(weekday.toLowerCase());
128+
this.weekday = checkField(this.weekday, dayOfWeek);
129+
}
130+
131+
public void setDay(int day) {
132+
if(day < 1 || day > 31) throw new InvalidDateTimeException(DDAY04,
133+
concat("Invalid ", type, " day out of range"));
134+
this.day = checkField(this.day, day);
135+
}
136+
137+
public void setAmPm(String amPm) {
138+
var amPmNum = switch(amPm.toLowerCase()) {
139+
case "am" -> 1;
140+
case "pm" -> 2;
141+
default -> throw new InvalidDateTimeException(DTAP02,
142+
concat("Invalid ", type, " hour AM/PM input"));
143+
};
144+
if(hour != UNSET && (hour < 1 || hour > 12))
145+
throw new InvalidDateTimeException(DHUR03,
146+
concat("Invalid ", type, " hour AM/PM out of range"));
147+
this.amPm = checkField(this.amPm, amPmNum);
148+
}
149+
150+
public void setHour(int hour) {
151+
if(amPm != UNSET && (this.hour < 1 || this.hour > 12))
152+
throw new InvalidDateTimeException(DHUR04,
153+
concat("Invalid ", type, " hour AM/PM out of range"));
154+
if(hour < 0 || hour > 23)
155+
throw new InvalidDateTimeException(DHUR06,
156+
concat("Invalid ", type, " hour out of range"));
157+
this.hour = checkField(this.hour, hour);
158+
}
159+
160+
public void setMinute(int minute) {
161+
if(minute < 0 || minute > 59) throw new InvalidDateTimeException(DMIN03,
162+
concat("Invalid ", type, " minute out of range"));
163+
this.minute = checkField(this.minute, minute);
164+
}
165+
166+
public void setSecond(int second) {
167+
if(second < 0 || second > 59) throw new InvalidDateTimeException(DSEC03,
168+
concat("Invalid ", type, " second out of range"));
169+
this.second = checkField(this.second, second);
170+
}
171+
172+
public void setFraction(int fraction) {
173+
this.fraction = checkField(this.fraction, fraction);
174+
}
175+
176+
public void setUtcOffset(int hour, int minute) {
177+
if(hour < -12 || hour > 12) throw new InvalidDateTimeException(DUTC04,
178+
concat("Invalid ", type, " UTC offset hour out of range"));
179+
if(minute < 0 || minute > 59) throw new InvalidDateTimeException(DUTC05,
180+
concat("Invalid ", type, " UTC offset minute out of range"));
181+
utcOffsetHour = checkField(utcOffsetHour, hour);
182+
utcOffsetMinute = checkField(utcOffsetMinute, minute);
183+
}
184+
185+
private int checkField(int current, int newValue) {
186+
if(current != UNSET && current != newValue)
187+
throw new InvalidDateTimeException(DCNF01,
188+
concat("Conflicting ", type, " segments input"));
189+
return newValue;
190+
}
191+
192+
private static boolean isAllSet(int... values) {
193+
return Arrays.stream(values).allMatch(v -> v != UNSET);
194+
}
195+
196+
public void validate() {
197+
try {
198+
LocalDate date;
199+
if(isAllSet(year, month, day)) {
200+
DAYS_IN_MONTH[2] = isLeapYear(year)? 29 : 28;
201+
if(day < 1 || day > DAYS_IN_MONTH[month])
202+
throw new InvalidDateTimeException(DDAY03,
203+
concat("Invalid ", type, " day out of range"));
204+
date = LocalDate.of(year, month, day);
205+
if(weekday != UNSET && date.getDayOfWeek().getValue() != weekday)
206+
throw new InvalidDateTimeException(DWKD03, concat("Invalid ",
207+
type, " weekday input"));
208+
}
209+
if(isAllSet(year, month)) date = LocalDate.of(year, month, 1);
210+
if(isAllSet(year)) date = LocalDate.of(year, 1, 1);
211+
} catch(InvalidDateTimeException e) {
212+
throw e;
213+
} catch(Exception e) {
214+
throw new InvalidDateTimeException(DINV01,
215+
concat("Invalid ", type, " year, month or day out of range", e));
216+
}
217+
if(isAllSet(hour, amPm)) convertTo24Hour();
218+
if(hour != UNSET && (hour < 0 || hour > 23))
219+
throw new InvalidDateTimeException(DHUR05, concat("Invalid ",
220+
type, " hour out of range"));
221+
}
222+
223+
private void convertTo24Hour() {
224+
if(amPm == 2 && hour != 12) hour += 12;
225+
else if(amPm == 1 && hour == 12) hour = 0;
226+
}
227+
228+
private static boolean isLeapYear(int year) {
229+
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
230+
}
231+
232+
private static int toFourDigitYear(int year) {
233+
var century = LocalDate.now().getYear() / 100 * 100;
234+
return year < PIVOT_YEAR ? century + year : century - 100 + year;
235+
}
236+
237+
@Override
238+
public String toString() {
239+
var builder = new StringBuilder("{");
240+
if(era != UNSET) append(builder, "Era: ", era);
241+
if(year != UNSET) append(builder, "Year: ", year);
242+
if(month != UNSET) append(builder, "Month: ", month);
243+
if(weekday != UNSET) append(builder, "Weekday: ", weekday);
244+
if(day != UNSET) append(builder, "Day: ", day);
245+
if(amPm != UNSET) append(builder, "AM/PM: ", amPm);
246+
if(hour != UNSET) append(builder, "Hour: ", hour);
247+
if(minute != UNSET) append(builder, "Minute: ", minute);
248+
if(second != UNSET) append(builder, "Second: ", second);
249+
if(fraction != UNSET) append(builder, "Fraction: ", fraction);
250+
if(utcOffsetHour != UNSET) append(builder, "UTC Offset Hour: ", utcOffsetHour);
251+
if(utcOffsetMinute != UNSET) append(builder, "UTC Offset Minute: ", utcOffsetMinute);
252+
return removeEnd(builder.toString(), ", ") + "}";
253+
}
254+
255+
private void append(StringBuilder builder, String label, int value) {
256+
builder.append(label).append(value).append(", ");
257+
}
258+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.relogiclabs.json.schema.time;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Getter;
5+
6+
@Getter
7+
@AllArgsConstructor
8+
public enum DateTimeType {
9+
DATE_TYPE("date"),
10+
TIME_TYPE("time");
11+
12+
private final String name;
13+
14+
@Override
15+
public String toString() {
16+
return name;
17+
}
18+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package com.relogiclabs.json.schema.time;
2+
3+
import com.relogiclabs.json.schema.exception.InvalidDateTimeException;
4+
import com.relogiclabs.json.schema.internal.antlr.DateTimeLexer;
5+
import com.relogiclabs.json.schema.internal.util.LexerErrorListener;
6+
import com.relogiclabs.json.schema.util.DebugUtils;
7+
import org.antlr.v4.runtime.CharStreams;
8+
import org.antlr.v4.runtime.Token;
9+
10+
import java.util.HashMap;
11+
import java.util.List;
12+
import java.util.Map;
13+
14+
import static com.relogiclabs.json.schema.internal.util.StringHelper.concat;
15+
import static com.relogiclabs.json.schema.message.ErrorCode.DINV02;
16+
17+
public class DateTimeValidator {
18+
public static final String ISO_8601_DATE = "YYYY-MM-DD";
19+
public static final String ISO_8601_TIME = "YYYY-MM-DD'T'hh:mm:ss.fffZZ";
20+
21+
private static final Map<String, SegmentProcessor> PROCESSORS;
22+
private final DateTimeLexer dateTimeLexer;
23+
private final List<Token> lexerTokens;
24+
25+
static {
26+
PROCESSORS = new HashMap<>(50);
27+
PROCESSORS.put("TEXT", SegmentProcessor.Text);
28+
PROCESSORS.put("SYMBOL", SegmentProcessor.Symbol);
29+
PROCESSORS.put("WHITESPACE", SegmentProcessor.Whitespace);
30+
PROCESSORS.put("ERA", SegmentProcessor.Era);
31+
PROCESSORS.put("YEAR_NUM4", SegmentProcessor.YearNum4);
32+
PROCESSORS.put("YEAR_NUM2", SegmentProcessor.YearNum2);
33+
PROCESSORS.put("MONTH_NAME", SegmentProcessor.MonthName);
34+
PROCESSORS.put("MONTH_SHORT_NAME", SegmentProcessor.MonthShortName);
35+
PROCESSORS.put("MONTH_NUM2", SegmentProcessor.MonthNum2);
36+
PROCESSORS.put("MONTH_NUM", SegmentProcessor.MonthNum);
37+
PROCESSORS.put("WEEKDAY_NAME", SegmentProcessor.WeekdayName);
38+
PROCESSORS.put("WEEKDAY_SHORT_NAME", SegmentProcessor.WeekdayShortName);
39+
PROCESSORS.put("DAY_NUM2", SegmentProcessor.DayNum2);
40+
PROCESSORS.put("DAY_NUM", SegmentProcessor.DayNum);
41+
PROCESSORS.put("AM_PM", SegmentProcessor.AmPm);
42+
PROCESSORS.put("HOUR_NUM2", SegmentProcessor.HourNum2);
43+
PROCESSORS.put("HOUR_NUM", SegmentProcessor.HourNum);
44+
PROCESSORS.put("MINUTE_NUM2", SegmentProcessor.MinuteNum2);
45+
PROCESSORS.put("MINUTE_NUM", SegmentProcessor.MinuteNum);
46+
PROCESSORS.put("SECOND_NUM2", SegmentProcessor.SecondNum2);
47+
PROCESSORS.put("SECOND_NUM", SegmentProcessor.SecondNum);
48+
PROCESSORS.put("FRACTION_NUM", SegmentProcessor.FractionNum);
49+
PROCESSORS.put("FRACTION_NUM01", SegmentProcessor.FractionNum01);
50+
PROCESSORS.put("FRACTION_NUM02", SegmentProcessor.FractionNum02);
51+
PROCESSORS.put("FRACTION_NUM03", SegmentProcessor.FractionNum03);
52+
PROCESSORS.put("FRACTION_NUM04", SegmentProcessor.FractionNum04);
53+
PROCESSORS.put("FRACTION_NUM05", SegmentProcessor.FractionNum05);
54+
PROCESSORS.put("FRACTION_NUM06", SegmentProcessor.FractionNum06);
55+
PROCESSORS.put("UTC_OFFSET_HOUR", SegmentProcessor.UtcOffsetHour);
56+
PROCESSORS.put("UTC_OFFSET_TIME1", SegmentProcessor.UtcOffsetTime1);
57+
PROCESSORS.put("UTC_OFFSET_TIME2", SegmentProcessor.UtcOffsetTime2);
58+
}
59+
60+
@SuppressWarnings("unchecked")
61+
public DateTimeValidator(String pattern) {
62+
dateTimeLexer = new DateTimeLexer(CharStreams.fromString(pattern));
63+
dateTimeLexer.removeErrorListeners();
64+
dateTimeLexer.addErrorListener(LexerErrorListener.DATE_TIME);
65+
lexerTokens = (List<Token>) dateTimeLexer.getAllTokens();
66+
}
67+
68+
private void Validate(String input, DateTimeContext context) {
69+
for(var token : lexerTokens) {
70+
var processor = PROCESSORS.get(dateTimeLexer.getVocabulary().getSymbolicName(token.getType()));
71+
input = processor.process(input, token, context);
72+
}
73+
if(input.length() != 0) throw new InvalidDateTimeException(DINV02,
74+
concat("Invalid ", context.getType(), " input format"));
75+
76+
context.validate();
77+
DebugUtils.print(context);
78+
}
79+
80+
public void ValidateDate(String input) {
81+
Validate(input, new DateTimeContext(DateTimeType.DATE_TYPE));
82+
}
83+
84+
public void ValidateTime(String input) {
85+
Validate(input, new DateTimeContext(DateTimeType.TIME_TYPE));
86+
}
87+
88+
public boolean IsValidDate(String input) {
89+
try {
90+
ValidateDate(input);
91+
return true;
92+
} catch(InvalidDateTimeException e) {
93+
DebugUtils.print(e);
94+
return false;
95+
}
96+
}
97+
98+
public boolean IsValidTime(String input) {
99+
try {
100+
ValidateTime(input);
101+
return true;
102+
} catch(InvalidDateTimeException e) {
103+
DebugUtils.print(e);
104+
return false;
105+
}
106+
}
107+
}

0 commit comments

Comments
 (0)