Skip to content

Commit

Permalink
fix for #298
Browse files Browse the repository at this point in the history
1) change rendering - now temporal accessors are rendered via pattern without conversion to zoned date time, so Instant is safe now
2) add more patterns support
  • Loading branch information
msangel committed Apr 7, 2024
1 parent b5aac74 commit 3ec9252
Show file tree
Hide file tree
Showing 7 changed files with 148 additions and 61 deletions.
25 changes: 21 additions & 4 deletions src/main/java/liqp/LValue.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoField;
import java.time.temporal.Temporal;
import java.time.temporal.TemporalAccessor;
import java.util.ArrayList;
import java.util.Arrays;
Expand Down Expand Up @@ -194,7 +195,23 @@ public static Object[] temporalAsArray(ZonedDateTime time) {
return new Object[]{sec, min, hour, day, month, year, wday, yday, isdst, zone};
}

public static ZonedDateTime asTemporal(Object value, TemplateContext context) {
/**
* This one keeps an original temporal type as is, while `asRubyDate` converts it to ZonedDateTime.
*/
public static TemporalAccessor asTemporal(Object value, TemplateContext context) {
ZonedDateTime time = ZonedDateTime.now();
if (value instanceof TemporalAccessor) {
return (TemporalAccessor) value;
} else if (CustomDateFormatRegistry.isCustomDateType(value)) {
time = CustomDateFormatRegistry.getFromCustomType(value);
}
return time;
}

/**
* Ruby have a single date type, and its equivalent is ZonedDateTime.
*/
public static ZonedDateTime asRubyDate(Object value, TemplateContext context) {
ZonedDateTime time = ZonedDateTime.now();
if (value instanceof TemporalAccessor) {
time = getZonedDateTimeFromTemporalAccessor((TemporalAccessor) value, context.getParser().defaultTimeZone);
Expand Down Expand Up @@ -306,7 +323,7 @@ public String asString(Object value, TemplateContext context) {
}

if (isTemporal(value)) {
ZonedDateTime time = asTemporal(value, context);
ZonedDateTime time = asRubyDate(value, context);
return rubyDateTimeFormat.format(time);
}

Expand Down Expand Up @@ -339,7 +356,7 @@ public Object asAppendableObject(Object value, TemplateContext context) {
}

if (isTemporal(value)) {
ZonedDateTime time = asTemporal(value, context);
ZonedDateTime time = asRubyDate(value, context);
return rubyDateTimeFormat.format(time);
}

Expand Down Expand Up @@ -380,7 +397,7 @@ public boolean isArray(Object value) {
* @return true iff `value` is a whole number (Integer or Long).
*/
public boolean isInteger(Object value) {
return value != null && (value instanceof Long || value instanceof Integer);
return (value instanceof Long || value instanceof Integer);
}

/**
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/liqp/TemplateParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ public enum ErrorMode {
public final boolean showExceptionsFromInclude;
public final TemplateParser.EvaluateMode evaluateMode;
public final Locale locale;
/**
* Never null, if empty - system default timezone is used.
*/
public final ZoneId defaultTimeZone;
private final RenderTransformer renderTransformer;
private final Consumer<Map<String, Object>> environmentMapConfigurator;
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/liqp/filters/Date.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import java.time.Instant;
import java.time.ZonedDateTime;
import java.time.temporal.TemporalAccessor;
import java.util.Locale;

import static liqp.filters.date.Parser.datePatterns;
Expand All @@ -35,7 +36,7 @@ public Object apply(Object value, TemplateContext context, Object... params) {
value = asArray(value, context)[0];
}
try {
final ZonedDateTime compatibleDate;
final TemporalAccessor compatibleDate;
String valAsString = super.asString(value, context);
if ("now".equals(valAsString) || "today".equals(valAsString)) {
compatibleDate = ZonedDateTime.now();
Expand Down
144 changes: 99 additions & 45 deletions src/main/java/liqp/filters/date/Parser.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package liqp.filters.date;

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.temporal.TemporalAccessor;
Expand All @@ -12,13 +10,8 @@
import java.util.List;
import java.util.Locale;

import static java.time.temporal.ChronoField.DAY_OF_MONTH;
import static java.time.temporal.ChronoField.HOUR_OF_DAY;
import static java.time.temporal.ChronoField.MINUTE_OF_HOUR;
import static java.time.temporal.ChronoField.MONTH_OF_YEAR;
import static java.time.temporal.ChronoField.NANO_OF_SECOND;
import static java.time.temporal.ChronoField.SECOND_OF_MINUTE;
import static java.time.temporal.ChronoField.YEAR;
import static java.time.temporal.ChronoField.*;
import static java.time.temporal.ChronoField.INSTANT_SECONDS;

public class Parser {

Expand All @@ -37,28 +30,74 @@ public class Parser {
public static List<String> datePatterns = new ArrayList<>();

static {

datePatterns.add("EEE MMM dd hh:mm:ss yyyy");
datePatterns.add("EEE MMM dd hh:mm yyyy");
datePatterns.add("yyyy-MM-dd");
datePatterns.add("dd-MM-yyyy");

// this is section without `T`, change here and do same change in section below with `T`
datePatterns.add("yyyy-MM-dd HH:mm");
datePatterns.add("yyyy-MM-dd HH:mm X");
datePatterns.add("yyyy-MM-dd HH:mm Z");
datePatterns.add("yyyy-MM-dd HH:mm z");
datePatterns.add("yyyy-MM-dd HH:mm'Z'");

datePatterns.add("yyyy-MM-dd HH:mm:ss");
datePatterns.add("yyyy-MM-dd'T'HH:mm:ss");
datePatterns.add("yyyy-MM-dd HH:mm:ss Z");
datePatterns.add("yyyy-MM-dd'T'HH:mm:ss Z");
datePatterns.add("yyyy-MM-dd HH:mm:ss X");
datePatterns.add("yyyy-MM-dd'T'HH:mm:ss X");
datePatterns.add("yyyy-MM-dd HH:mm:ss Z");
datePatterns.add("yyyy-MM-dd HH:mm:ss z");
datePatterns.add("yyyy-MM-dd'T'HH:mm:ss z");
datePatterns.add("EEE MMM dd hh:mm:ss yyyy");
datePatterns.add("yyyy-MM-dd HH:mm:ss'Z'");

datePatterns.add("yyyy-MM-dd HH:mm");
datePatterns.add("yyyy-MM-dd HH:mm:ss.SSS");
datePatterns.add("yyyy-MM-dd HH:mm:ss.SSS X");
datePatterns.add("yyyy-MM-dd HH:mm:ss.SSS Z");
datePatterns.add("yyyy-MM-dd HH:mm:ss.SSS z");
datePatterns.add("yyyy-MM-dd HH:mm:ss.SSS'Z'");

datePatterns.add("yyyy-MM-dd HH:mm:ss.SSSSSS");
datePatterns.add("yyyy-MM-dd HH:mm:ss.SSSSSS X");
datePatterns.add("yyyy-MM-dd HH:mm:ss.SSSSSS Z");
datePatterns.add("yyyy-MM-dd HH:mm:ss.SSSSSS z");
datePatterns.add("yyyy-MM-dd HH:mm:ss.SSSSSS'Z'");

datePatterns.add("yyyy-MM-dd HH:mm:ss.SSSSSSSSS");
datePatterns.add("yyyy-MM-dd HH:mm:ss.SSSSSSSSS X");
datePatterns.add("yyyy-MM-dd HH:mm:ss.SSSSSSSSS Z");
datePatterns.add("yyyy-MM-dd HH:mm:ss.SSSSSSSSS z");
datePatterns.add("yyyy-MM-dd HH:mm:ss.SSSSSSSSS'Z'");

// this is section with `T`
datePatterns.add("yyyy-MM-dd'T'HH:mm");
datePatterns.add("yyyy-MM-dd HH:mm Z");
datePatterns.add("yyyy-MM-dd'T'HH:mm Z");
datePatterns.add("yyyy-MM-dd HH:mm X");
datePatterns.add("yyyy-MM-dd'T'HH:mm X");
datePatterns.add("yyyy-MM-dd HH:mm z");
datePatterns.add("yyyy-MM-dd'T'HH:mm Z");
datePatterns.add("yyyy-MM-dd'T'HH:mm z");
datePatterns.add("EEE MMM dd hh:mm yyyy");
datePatterns.add("yyyy-MM-dd'T'HH:mm'Z'");

datePatterns.add("yyyy-MM-dd'T'HH:mm:ss");
datePatterns.add("yyyy-MM-dd'T'HH:mm:ss X");
datePatterns.add("yyyy-MM-dd'T'HH:mm:ss Z");
datePatterns.add("yyyy-MM-dd'T'HH:mm:ss z");
datePatterns.add("yyyy-MM-dd'T'HH:mm:ss'Z'");

datePatterns.add("yyyy-MM-dd'T'HH:mm:ss.SSS");
datePatterns.add("yyyy-MM-dd'T'HH:mm:ss.SSS X");
datePatterns.add("yyyy-MM-dd'T'HH:mm:ss.SSS Z");
datePatterns.add("yyyy-MM-dd'T'HH:mm:ss.SSS z");
datePatterns.add("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");

datePatterns.add("yyyy-MM-dd'T'HH:mm:ss.SSSSSS");
datePatterns.add("yyyy-MM-dd'T'HH:mm:ss.SSSSSS X");
datePatterns.add("yyyy-MM-dd'T'HH:mm:ss.SSSSSS Z");
datePatterns.add("yyyy-MM-dd'T'HH:mm:ss.SSSSSS z");
datePatterns.add("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'");

datePatterns.add("yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSS");
datePatterns.add("yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSS X");
datePatterns.add("yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSS Z");
datePatterns.add("yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSS z");
datePatterns.add("yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSS'Z'");

datePatterns.add("yyyy-MM-dd");
datePatterns.add("dd-MM-yyyy");
}

public static ZonedDateTime parse(String str, Locale locale, ZoneId defaultZone) {
Expand Down Expand Up @@ -86,31 +125,46 @@ public static ZonedDateTime parse(String str, Locale locale, ZoneId defaultZone)
* Follow ruby rules: if some datetime part is missing,
* the default is taken from `now` with default zone
*/
public static ZonedDateTime getZonedDateTimeFromTemporalAccessor(TemporalAccessor temporalAccessor, ZoneId defaultZone) {
if (temporalAccessor instanceof ZonedDateTime) {
return (ZonedDateTime) temporalAccessor;
public static ZonedDateTime getZonedDateTimeFromTemporalAccessor(TemporalAccessor temporal, ZoneId defaultZone) {
if (temporal == null) {
return ZonedDateTime.now(defaultZone);
}
LocalDateTime now = LocalDateTime.now();
TemporalField[] copyThese = new TemporalField[]{
YEAR,
MONTH_OF_YEAR,
DAY_OF_MONTH,
HOUR_OF_DAY,
MINUTE_OF_HOUR,
SECOND_OF_MINUTE,
NANO_OF_SECOND
};
for (TemporalField tf: copyThese) {
if (temporalAccessor.isSupported(tf)) {
now = now.with(tf, temporalAccessor.get(tf));
}
if (temporal instanceof ZonedDateTime) {
return (ZonedDateTime) temporal;
}
if (temporal instanceof Instant) {
return ZonedDateTime.ofInstant((Instant) temporal, defaultZone);
}

ZoneId zoneId = temporalAccessor.query(TemporalQueries.zone());
ZoneId zoneId = temporal.query(TemporalQueries.zone());
if (zoneId == null) {
zoneId = defaultZone;
}
LocalDate date = temporal.query(TemporalQueries.localDate());
LocalTime time = temporal.query(TemporalQueries.localTime());

return now.atZone(zoneId);
if (date == null) {
date = LocalDate.now(defaultZone);
}
if (time == null) {
time = LocalTime.now(defaultZone);
}
return ZonedDateTime.of(date, time, defaultZone);
} else {
LocalDateTime now = LocalDateTime.now(zoneId);
TemporalField[] copyThese = new TemporalField[]{
YEAR,
MONTH_OF_YEAR,
DAY_OF_MONTH,
HOUR_OF_DAY,
MINUTE_OF_HOUR,
SECOND_OF_MINUTE,
NANO_OF_SECOND
};
for (TemporalField tf: copyThese) {
if (temporal.isSupported(tf)) {
now = now.with(tf, temporal.get(tf));
}
}
return now.atZone(zoneId);
}
}
}
10 changes: 3 additions & 7 deletions src/main/java/liqp/nodes/BlockNode.java
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
package liqp.nodes;

import static liqp.LValue.BREAK;
import static liqp.LValue.CONTINUE;
import static liqp.LValue.asTemporal;
import static liqp.LValue.isTemporal;
import static liqp.LValue.rubyDateTimeFormat;

import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;

import liqp.RenderTransformer.ObjectAppender;
import liqp.TemplateContext;

import static liqp.LValue.*;

public class BlockNode implements LNode {

private List<LNode> children;
Expand Down Expand Up @@ -67,7 +63,7 @@ public Object render(TemplateContext context) {

private Object postprocess(Object value, TemplateContext context) {
if (isTemporal(value)) {
ZonedDateTime time = asTemporal(value, context);
ZonedDateTime time = asRubyDate(value, context);
return rubyDateTimeFormat.format(time);
} else {
return value;
Expand Down
16 changes: 12 additions & 4 deletions src/main/java/liqp/nodes/ComparingExpressionNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,18 @@ public abstract class ComparingExpressionNode extends LValue implements LNode {
protected final LNode rhs;
private final boolean relative;

public ComparingExpressionNode(LNode lhs, LNode rhs, boolean realtive) {
/**
*
* @param lhs - left-hand side
* @param rhs - right-hand side
* @param relative - expressions are two kinds:
* relative(>, >=, <, <=)
* and equality (==, <>, !=) and rules for comparing them different.
*/
public ComparingExpressionNode(LNode lhs, LNode rhs, boolean relative) {
this.lhs = lhs;
this.rhs = rhs;
this.relative = realtive;
this.relative = relative;
}

@Override
Expand All @@ -41,10 +49,10 @@ public Object render(TemplateContext context) {
Object a = lhs.render(context);
Object b = rhs.render(context);
if (isTemporal(a)) {
a = asTemporal(a, context);
a = asRubyDate(a, context);
}
if (isTemporal(b)) {
b = asTemporal(b, context);
b = asRubyDate(b, context);
}

if (a instanceof Number) {
Expand Down
8 changes: 8 additions & 0 deletions src/test/java/liqp/filters/DateTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -225,4 +225,12 @@ public void test240() {
assertEquals("10-13", TemplateParser.DEFAULT.parse("{{ \"2022-10-13\" | date: \"%m-%e\" }}").render());
assertEquals("10-13", TemplateParser.DEFAULT.parse("{{ \"13-10-2022\" | date: \"%m-%e\" }}").render());
}

@Test
public void test298InstantWhenEpochBeginAtUTC() {
Instant instant = Instant.ofEpochSecond(0);
TemplateParser parser = new TemplateParser.Builder().withDefaultTimeZone(ZoneOffset.UTC).build();
String res = parser.parse("{{ val }}").render("val", instant);
assertEquals("1970-01-01 00:00:00 Z", res);
}
}

0 comments on commit 3ec9252

Please sign in to comment.