Skip to content

Commit

Permalink
Add timeZone parameter to date filter (#530)
Browse files Browse the repository at this point in the history
Co-authored-by: Victor Grigoriu <[email protected]>
  • Loading branch information
vgrigoriu and Victor Grigoriu authored Jul 31, 2020
1 parent bf76894 commit 821e229
Show file tree
Hide file tree
Showing 3 changed files with 185 additions and 35 deletions.
27 changes: 21 additions & 6 deletions docs/src/orchid/resources/wiki/filter/date.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
# `date`
The `date` filter is used to format an existing `java.util.Date` object. The filter will construct a
`java.text.SimpleDateFormat` using the provided pattern and then use this newly created
`SimpleDateFormat` to format the provided `Date` or `java.lang.Number` object.
The `date` filter formats a date in a variety of formats. It can handle old-school `java.util.Date`,
Java 8 `java.time` constructs like `OffsetDateTime` and timestamps in milliseconds from the epoch.
The filter will construct a `java.text.SimpleDateFormat` or `java.time.format.DateTimeFormatter` using the provided
pattern and then use this newly created format to format the provided date object. If you don't provide a pattern,
either `DateTimeFormatter.ISO_DATE_TIME` or `yyyy-MM-dd'T'HH:mm:ssZ` will be used.

```twig
{{ user.birthday | date("yyyy-MM-dd") }}
```

The alternative way to use this filter is to use it on a string but then provide two arguments:
first is the desired pattern for the output and the second is the existing format used to parse the
An alternative way to use this filter is to use it on a string but then provide two arguments:
the first is the desired pattern for the output, and the second is the existing format used to parse the
input string into a `java.util.Date` object.
```twig
{{ "July 24, 2001" | date("yyyy-MM-dd", existingFormat="MMMM dd, yyyy") }}
Expand All @@ -18,6 +20,19 @@ The above example will output the following:
2001-07-24
```

## Time zones

If the provided date has time zone info (e.g. `OffsetDateTime`) then it will be used. If the provided date has no
time zone info, by default the system time zone will be used. If you need to use a specific
time zone then you can pass in a `timeZone` parameter any string that's understood by `ZoneId` / `ZoneInfo`:
```twig
{# the timeZone parameter will be ignored #}
{{ someOffsetDateTime | date("yyyy-MM-dd'T'HH:mm:ssX", timeZone="UTC") }}
{# the provided time zone will override the system default #}
{{ someInstant | date("yyyy-MM-dd'T'HH:mm:ssX", timeZone="Pacific/Funafuti") }}
```

## Arguments
- format
- existingFormat
- existingFormat
- timeZone
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,23 @@
*/
package com.mitchellbosecke.pebble.extension.core;

import static java.lang.String.format;

import com.mitchellbosecke.pebble.error.PebbleException;
import com.mitchellbosecke.pebble.extension.Filter;
import com.mitchellbosecke.pebble.extension.escaper.SafeString;
import com.mitchellbosecke.pebble.template.EvaluationContext;
import com.mitchellbosecke.pebble.template.PebbleTemplate;

import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.DateTimeException;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAccessor;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.time.temporal.TemporalQueries;
import java.util.*;

import static java.lang.String.format;

public class DateFilter implements Filter {

Expand All @@ -34,6 +33,7 @@ public class DateFilter implements Filter {
public DateFilter() {
this.argumentNames.add("format");
this.argumentNames.add("existingFormat");
this.argumentNames.add("timeZone");
}

@Override
Expand All @@ -49,16 +49,18 @@ public Object apply(Object input, Map<String, Object> args, PebbleTemplate self,
}
final Locale locale = context.getLocale();
final String format = (String) args.get("format");
final String timeZone = (String) args.get("timeZone");

if (TemporalAccessor.class.isAssignableFrom(input.getClass())) {
return this.applyTemporal((TemporalAccessor) input, self, locale, lineNumber, format);
return this.applyTemporal((TemporalAccessor) input, self, locale, lineNumber, format, timeZone);
}
return this
.applyDate(input, self, locale, lineNumber, format, (String) args.get("existingFormat"));
return this.applyDate(
input, self, locale, lineNumber,
format, (String) args.get("existingFormat"), timeZone);
}

private Object applyDate(Object dateOrString, final PebbleTemplate self, final Locale locale,
int lineNumber, final String format, final String existingFormatString)
int lineNumber, final String format, final String existingFormatString, final String timeZone)
throws PebbleException {
Date date;
DateFormat existingFormat;
Expand All @@ -82,22 +84,46 @@ private Object applyDate(Object dateOrString, final PebbleTemplate self, final L
dateOrString));
}
}
intendedFormat = new SimpleDateFormat(format, locale);
intendedFormat = new SimpleDateFormat(format == null ? "yyyy-MM-dd'T'HH:mm:ssZ" : format, locale);
if (timeZone != null) {
intendedFormat.setTimeZone(TimeZone.getTimeZone(timeZone));
}
return new SafeString(intendedFormat.format(date));
}

private Object applyTemporal(final TemporalAccessor input, PebbleTemplate self,
final Locale locale,
int lineNumber, final String format) throws PebbleException {
final DateTimeFormatter formatter = format != null
int lineNumber, final String format, final String timeZone) throws PebbleException {
DateTimeFormatter formatter = format != null
? DateTimeFormatter.ofPattern(format, locale)
: DateTimeFormatter.ISO_DATE_TIME;

ZoneId zoneId = getZoneId(input, timeZone);
formatter = formatter.withZone(zoneId);

try {
return new SafeString(formatter.format(input));
} catch (DateTimeException dte) {
throw new PebbleException(dte, String.format("Could not parse the string '%s' into a date.",
input.toString()), lineNumber, self.getName());
throw new PebbleException(
dte,
String.format("Could not format instance '%s' of type %s into a date.", input.toString(), input.getClass()),
lineNumber,
self.getName());
}
}

private ZoneId getZoneId(TemporalAccessor input, String timeZone) {
// First try the time zone of the input.
ZoneId zoneId = input.query(TemporalQueries.zone());
if (zoneId == null && timeZone != null) {
// Fallback to time zone provided as filter argument.
zoneId = ZoneId.of(timeZone);
}
if (zoneId == null) {
// Fallback to system time zone.
zoneId = ZoneId.systemDefault();
}
return zoneId;
}

}
135 changes: 122 additions & 13 deletions pebble/src/test/java/com/mitchellbosecke/pebble/CoreFiltersTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,8 @@
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.time.*;
import java.util.*;

import static java.lang.Boolean.TRUE;
import static java.util.Collections.emptyList;
Expand Down Expand Up @@ -232,6 +221,126 @@ void testDateWithNumberInput() throws IOException {
assertEquals("02/07/2018", writer.toString());
}

@Test
void testDateWithDateAndExplicitTimeZone() throws IOException {
PebbleEngine pebble = new PebbleEngine.Builder().build();

PebbleTemplate template = pebble.getLiteralTemplate("{{ date | date(timeZone=\"Asia/Almaty\") }}");
Map<String, Object> context = new HashMap<>();
context.put("date", Date.from(Instant.ofEpochSecond(1595853935)));

Writer writer = new StringWriter();
template.evaluate(writer, context);

assertEquals("2020-07-27T18:45:35+0600", writer.toString());
}

@Test
void testDateWithDateAndFormatAndExplicitTimeZone() throws IOException {
PebbleEngine pebble = new PebbleEngine.Builder().build();

PebbleTemplate template = pebble.getLiteralTemplate("{{ date | date(\"yyyy-MM-dd'T'HH:mm:ssX\", timeZone=\"Asia/Almaty\") }}");
Map<String, Object> context = new HashMap<>();
context.put("date", Date.from(Instant.ofEpochSecond(1595853935)));

Writer writer = new StringWriter();
template.evaluate(writer, context);

assertEquals("2020-07-27T18:45:35+06", writer.toString());
}

@Test
void testDateWithTimestampAndExplicitTimeZone() throws IOException {
PebbleEngine pebble = new PebbleEngine.Builder().build();

PebbleTemplate template = pebble.getLiteralTemplate("{{ timestamp | date(timeZone=\"Asia/Almaty\") }}");
Map<String, Object> context = new HashMap<>();
context.put("timestamp", 1595853935000L);

Writer writer = new StringWriter();
template.evaluate(writer, context);

assertEquals("2020-07-27T18:45:35+0600", writer.toString());
}

@Test
void testDateWithOffsetDateTimeAndExplicitTimeZoneUsesTimeZoneOfInput() throws IOException {
PebbleEngine pebble = new PebbleEngine.Builder().build();

PebbleTemplate template = pebble.getLiteralTemplate("{{ offsetDateTime | date(timeZone=\"Asia/Almaty\") }}");
Map<String, Object> context = new HashMap<>();
context.put("offsetDateTime", OffsetDateTime.of(2020, 7, 27, 16, 12, 13, 0, ZoneOffset.ofHours(3)));

Writer writer = new StringWriter();
template.evaluate(writer, context);

assertEquals("2020-07-27T16:12:13+03:00", writer.toString());
}

@Test
void testDateWithOffsetDateTimeAndFormatAndExplicitTimeZoneUsesTimeZoneOfInput() throws IOException {
PebbleEngine pebble = new PebbleEngine.Builder().build();

PebbleTemplate template = pebble.getLiteralTemplate("{{ offsetDateTime | date(\"yyyy-MM-dd'T'HH:mm:ssX\", timeZone=\"Asia/Almaty\") }}");
Map<String, Object> context = new HashMap<>();
context.put("offsetDateTime", OffsetDateTime.of(2020, 7, 27, 16, 12, 13, 0, ZoneOffset.ofHours(3)));

Writer writer = new StringWriter();
template.evaluate(writer, context);

assertEquals("2020-07-27T16:12:13+03", writer.toString());
}

@Test
void testDateWithOffsetDateTimeAndFormatAndNoExplicitTimeZoneUsesTimeZoneOfInput() throws IOException {
PebbleEngine pebble = new PebbleEngine.Builder().build();

PebbleTemplate template = pebble.getLiteralTemplate("{{ offsetDateTime | date(\"yyyy-MM-dd'T'HH:mm:ssX\") }}");
Map<String, Object> context = new HashMap<>();
context.put("offsetDateTime", OffsetDateTime.of(2020, 7, 27, 16, 12, 13, 0, ZoneOffset.ofHours(5)));

Writer writer = new StringWriter();
template.evaluate(writer, context);

assertEquals("2020-07-27T16:12:13+05", writer.toString());
}

@Test
void testDateWithInstantAndExplicitTimeZone() throws IOException {
PebbleEngine pebble = new PebbleEngine.Builder().build();

PebbleTemplate template = pebble.getLiteralTemplate("{{ instant | date(timeZone=\"Asia/Almaty\") }}");
Map<String, Object> context = new HashMap<>();
context.put("instant", Instant.ofEpochSecond(1595853935));

Writer writer = new StringWriter();
template.evaluate(writer, context);

assertEquals("2020-07-27T18:45:35+06:00[Asia/Almaty]", writer.toString());
}

@Test
void testDateWithInstantAndNoExplicitTimeZoneUsesSystemTimeZone() throws IOException {
PebbleEngine pebble = new PebbleEngine.Builder().build();

TimeZone defaultTimeZone = TimeZone.getDefault();
try {
TimeZone.setDefault(TimeZone.getTimeZone("Pacific/Funafuti"));

PebbleTemplate template = pebble.getLiteralTemplate("{{ instant | date() }}");
Map<String, Object> context = new HashMap<>();
context.put("instant", Instant.ofEpochSecond(1595853935));

Writer writer = new StringWriter();
template.evaluate(writer, context);

assertEquals("2020-07-28T00:45:35+12:00[Pacific/Funafuti]", writer.toString());
}
finally {
TimeZone.setDefault(defaultTimeZone);
}
}

@Test
void testDateWithUnsupportedInput() throws IOException {
assertThrows(IllegalArgumentException.class, () -> {
Expand Down

0 comments on commit 821e229

Please sign in to comment.