Skip to content
Snippets Groups Projects
Commit 307ef6b4 authored by Ivan Frade's avatar Ivan Frade Committed by Matthias Sohn
Browse files

GitTimeParser: A date parser using the java.time API

Replacement of GitDateParser that uses java.time classes instead of
the obsolete Date. Updating GitDateParser would have been a mess of
deprecation and methods with confusing names, so I opted for writing a
parallel class with the new types.

Some differences:

* The new DateTimeFormatter is thread-safe, so we don't need the
LocalThread cache

* No code seems to use other locale than the default, we don't need to
cache per locale either

Change-Id: If24610a055a47702fb5b7be2fc35a7c722480ee3
parent 2e5fb1fc
No related branches found
No related tags found
No related merge requests found
/*
* Copyright (C) 2012, Christian Halstrick and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
* https://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
package org.eclipse.jgit.util;
import static org.junit.Assert.assertThrows;
import java.text.ParseException;
import org.eclipse.jgit.junit.MockSystemReader;
import org.junit.After;
import org.junit.Before;
import org.junit.experimental.theories.DataPoints;
import org.junit.experimental.theories.Theories;
import org.junit.experimental.theories.Theory;
import org.junit.runner.RunWith;
/**
* Tests which assert that unparseable Strings lead to ParseExceptions
*/
@RunWith(Theories.class)
public class GitTimeParserBadlyFormattedTest {
private String dateStr;
@Before
public void setUp() {
MockSystemReader mockSystemReader = new MockSystemReader();
SystemReader.setInstance(mockSystemReader);
}
@After
public void tearDown() {
SystemReader.setInstance(null);
}
public GitTimeParserBadlyFormattedTest(String dateStr) {
this.dateStr = dateStr;
}
@DataPoints
public static String[] getDataPoints() {
return new String[] { "", "1970", "3000.3000.3000", "3 yesterday ago",
"now yesterday ago", "yesterdays", "3.day. 2.week.ago",
"day ago", "Gra Feb 21 15:35:00 2007 +0100",
"Sun Feb 21 15:35:00 2007 +0100",
"Wed Feb 21 15:35:00 Grand +0100" };
}
@Theory
public void badlyFormattedWithoutRef() {
assertThrows(
"The expected ParseException while parsing '" + dateStr
+ "' did not occur.",
ParseException.class, () -> GitTimeParser.parse(dateStr));
}
}
/*
* Copyright (C) 2024, Christian Halstrick and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
* https://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
package org.eclipse.jgit.util;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.text.ParseException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.Period;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoField;
import java.time.temporal.TemporalAccessor;
import org.eclipse.jgit.junit.MockSystemReader;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
public class GitTimeParserTest {
MockSystemReader mockSystemReader;
@Before
public void setUp() {
mockSystemReader = new MockSystemReader();
SystemReader.setInstance(mockSystemReader);
}
@After
public void tearDown() {
SystemReader.setInstance(null);
}
@Test
public void yesterday() throws ParseException {
LocalDateTime parse = GitTimeParser.parse("yesterday");
LocalDateTime now = SystemReader.getInstance().civilNow();
assertEquals(Period.between(parse.toLocalDate(), now.toLocalDate()),
Period.ofDays(1));
}
@Test
public void never() throws ParseException {
LocalDateTime parse = GitTimeParser.parse("never");
assertEquals(LocalDateTime.MAX, parse);
}
@Test
public void now_pointInTime() throws ParseException {
LocalDateTime aTime = asLocalDateTime("2007-02-21 15:35:00 +0100");
LocalDateTime parsedNow = GitTimeParser.parse("now", aTime);
assertEquals(aTime, parsedNow);
}
@Test
public void now_systemTime() throws ParseException {
LocalDateTime firstNow = GitTimeParser.parse("now");
assertEquals(SystemReader.getInstance().civilNow(), firstNow);
mockSystemReader.tick(10);
LocalDateTime secondNow = GitTimeParser.parse("now");
assertTrue(secondNow.isAfter(firstNow));
}
@Test
public void weeksAgo() throws ParseException {
LocalDateTime aTime = asLocalDateTime("2007-02-21 15:35:00 +0100");
LocalDateTime parse = GitTimeParser.parse("2 weeks ago", aTime);
assertEquals(asLocalDateTime("2007-02-07 15:35:00 +0100"), parse);
}
@Test
public void daysAndWeeksAgo() throws ParseException {
LocalDateTime aTime = asLocalDateTime("2007-02-21 15:35:00 +0100");
LocalDateTime twoWeeksAgoActual = GitTimeParser.parse("2 weeks ago",
aTime);
LocalDateTime twoWeeksAgoExpected = asLocalDateTime(
"2007-02-07 15:35:00 +0100");
assertEquals(twoWeeksAgoExpected, twoWeeksAgoActual);
LocalDateTime combinedWhitespace = GitTimeParser
.parse("3 days 2 weeks ago", aTime);
LocalDateTime combinedWhitespaceExpected = asLocalDateTime(
"2007-02-04 15:35:00 +0100");
assertEquals(combinedWhitespaceExpected, combinedWhitespace);
LocalDateTime combinedDots = GitTimeParser.parse("3.day.2.week.ago",
aTime);
LocalDateTime combinedDotsExpected = asLocalDateTime(
"2007-02-04 15:35:00 +0100");
assertEquals(combinedDotsExpected, combinedDots);
}
@Test
public void hoursAgo() throws ParseException {
LocalDateTime aTime = asLocalDateTime("2007-02-21 17:35:00 +0100");
LocalDateTime twoHoursAgoActual = GitTimeParser.parse("2 hours ago",
aTime);
LocalDateTime twoHoursAgoExpected = asLocalDateTime(
"2007-02-21 15:35:00 +0100");
assertEquals(twoHoursAgoExpected, twoHoursAgoActual);
}
@Test
public void hoursAgo_acrossDay() throws ParseException {
LocalDateTime aTime = asLocalDateTime("2007-02-21 00:35:00 +0100");
LocalDateTime twoHoursAgoActual = GitTimeParser.parse("2 hours ago",
aTime);
LocalDateTime twoHoursAgoExpected = asLocalDateTime(
"2007-02-20 22:35:00 +0100");
assertEquals(twoHoursAgoExpected, twoHoursAgoActual);
}
@Test
public void minutesHoursAgoCombined() throws ParseException {
LocalDateTime aTime = asLocalDateTime("2007-02-04 15:35:00 +0100");
LocalDateTime combinedWhitespace = GitTimeParser
.parse("3 hours 2 minutes ago", aTime);
LocalDateTime combinedWhitespaceExpected = asLocalDateTime(
"2007-02-04 12:33:00 +0100");
assertEquals(combinedWhitespaceExpected, combinedWhitespace);
LocalDateTime combinedDots = GitTimeParser
.parse("3.hours.2.minutes.ago", aTime);
LocalDateTime combinedDotsExpected = asLocalDateTime(
"2007-02-04 12:33:00 +0100");
assertEquals(combinedDotsExpected, combinedDots);
}
@Test
public void minutesAgo() throws ParseException {
LocalDateTime aTime = asLocalDateTime("2007-02-21 17:35:10 +0100");
LocalDateTime twoMinutesAgo = GitTimeParser.parse("2 minutes ago",
aTime);
LocalDateTime twoMinutesAgoExpected = asLocalDateTime(
"2007-02-21 17:33:10 +0100");
assertEquals(twoMinutesAgoExpected, twoMinutesAgo);
}
@Test
public void minutesAgo_acrossDay() throws ParseException {
LocalDateTime aTime = asLocalDateTime("2007-02-21 00:35:10 +0100");
LocalDateTime minutesAgoActual = GitTimeParser.parse("40 minutes ago",
aTime);
LocalDateTime minutesAgoExpected = asLocalDateTime(
"2007-02-20 23:55:10 +0100");
assertEquals(minutesAgoExpected, minutesAgoActual);
}
@Test
public void iso() throws ParseException {
String dateStr = "2007-02-21 15:35:00 +0100";
LocalDateTime actual = GitTimeParser.parse(dateStr);
LocalDateTime expected = asLocalDateTime(dateStr);
assertEquals(expected, actual);
}
@Test
public void rfc() throws ParseException {
String dateStr = "Wed, 21 Feb 2007 15:35:00 +0100";
LocalDateTime actual = GitTimeParser.parse(dateStr);
LocalDateTime expected = asLocalDateTime(dateStr,
"EEE, dd MMM yyyy HH:mm:ss Z");
assertEquals(expected, actual);
}
@Test
public void shortFmt() throws ParseException {
assertParsing("2007-02-21", "yyyy-MM-dd");
}
@Test
public void shortWithDots() throws ParseException {
assertParsing("2007.02.21", "yyyy.MM.dd");
}
@Test
public void shortWithSlash() throws ParseException {
assertParsing("02/21/2007", "MM/dd/yyyy");
}
@Test
public void shortWithDotsReverse() throws ParseException {
assertParsing("21.02.2007", "dd.MM.yyyy");
}
@Test
public void defaultFmt() throws ParseException {
assertParsing("Wed Feb 21 15:35:00 2007 +0100",
"EEE MMM dd HH:mm:ss yyyy Z");
}
@Test
public void local() throws ParseException {
assertParsing("Wed Feb 21 15:35:00 2007", "EEE MMM dd HH:mm:ss yyyy");
}
private static void assertParsing(String dateStr, String format)
throws ParseException {
LocalDateTime actual = GitTimeParser.parse(dateStr);
LocalDateTime expected = asLocalDateTime(dateStr, format);
assertEquals(expected, actual);
}
private static LocalDateTime asLocalDateTime(String dateStr) {
return asLocalDateTime(dateStr, "yyyy-MM-dd HH:mm:ss Z");
}
private static LocalDateTime asLocalDateTime(String dateStr,
String pattern) {
DateTimeFormatter fmt = DateTimeFormatter.ofPattern(pattern);
TemporalAccessor ta = fmt
.withZone(SystemReader.getInstance().getTimeZoneId())
.withLocale(SystemReader.getInstance().getLocale())
.parse(dateStr);
return ta.isSupported(ChronoField.HOUR_OF_DAY) ? LocalDateTime.from(ta)
: LocalDate.from(ta).atStartOfDay();
}
}
......@@ -28,7 +28,10 @@
* used. One example is the parsing of the config parameter gc.pruneexpire. The
* parser can handle only subset of what native gits approxidate parser
* understands.
*
* @deprecated Use {@link GitTimeParser} instead.
*/
@Deprecated(since = "7.1")
public class GitDateParser {
/**
* The Date representing never. Though this is a concrete value, most
......
/*
* Copyright (C) 2024 Christian Halstrick and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
* https://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
package org.eclipse.jgit.util;
import java.text.MessageFormat;
import java.text.ParseException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoField;
import java.time.temporal.TemporalAccessor;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jgit.internal.JGitText;
/**
* Parses strings with time and date specifications into
* {@link java.time.Instant}.
*
* When git needs to parse strings specified by the user this parser can be
* used. One example is the parsing of the config parameter gc.pruneexpire. The
* parser can handle only subset of what native gits approxidate parser
* understands.
*
* @since 7.1
*/
public class GitTimeParser {
private static final Map<ParseableSimpleDateFormat, DateTimeFormatter> formatCache = new HashMap<>();
// An enum of all those formats which this parser can parse with the help of
// a DateTimeFormatter. There are other formats (e.g. the relative formats
// like "yesterday" or "1 week ago") which this parser can parse but which
// are not listed here because they are parsed without the help of a
// DateTimeFormatter.
enum ParseableSimpleDateFormat {
ISO("yyyy-MM-dd HH:mm:ss Z"), // //$NON-NLS-1$
RFC("EEE, dd MMM yyyy HH:mm:ss Z"), // //$NON-NLS-1$
SHORT("yyyy-MM-dd"), // //$NON-NLS-1$
SHORT_WITH_DOTS_REVERSE("dd.MM.yyyy"), // //$NON-NLS-1$
SHORT_WITH_DOTS("yyyy.MM.dd"), // //$NON-NLS-1$
SHORT_WITH_SLASH("MM/dd/yyyy"), // //$NON-NLS-1$
DEFAULT("EEE MMM dd HH:mm:ss yyyy Z"), // //$NON-NLS-1$
LOCAL("EEE MMM dd HH:mm:ss yyyy"); //$NON-NLS-1$
private final String formatStr;
ParseableSimpleDateFormat(String formatStr) {
this.formatStr = formatStr;
}
}
/**
* Parses a string into a {@link java.time.LocalDateTime} using the default
* locale. Since this parser also supports relative formats (e.g.
* "yesterday") the caller can specify the reference date. These types of
* strings can be parsed:
* <ul>
* <li>"never"</li>
* <li>"now"</li>
* <li>"yesterday"</li>
* <li>"(x) years|months|weeks|days|hours|minutes|seconds ago"<br>
* Multiple specs can be combined like in "2 weeks 3 days ago". Instead of '
* ' one can use '.' to separate the words</li>
* <li>"yyyy-MM-dd HH:mm:ss Z" (ISO)</li>
* <li>"EEE, dd MMM yyyy HH:mm:ss Z" (RFC)</li>
* <li>"yyyy-MM-dd"</li>
* <li>"yyyy.MM.dd"</li>
* <li>"MM/dd/yyyy",</li>
* <li>"dd.MM.yyyy"</li>
* <li>"EEE MMM dd HH:mm:ss yyyy Z" (DEFAULT)</li>
* <li>"EEE MMM dd HH:mm:ss yyyy" (LOCAL)</li>
* </ul>
*
* @param dateStr
* the string to be parsed
* @return the parsed {@link java.time.LocalDateTime}
* @throws java.text.ParseException
* if the given dateStr was not recognized
*/
public static LocalDateTime parse(String dateStr) throws ParseException {
return parse(dateStr, SystemReader.getInstance().civilNow());
}
// Only tests seem to use this method
static LocalDateTime parse(String dateStr, LocalDateTime now)
throws ParseException {
dateStr = dateStr.trim();
LocalDateTime ret;
if ("never".equalsIgnoreCase(dateStr)) //$NON-NLS-1$
return LocalDateTime.MAX;
ret = parse_relative(dateStr, now);
if (ret != null)
return ret;
for (ParseableSimpleDateFormat f : ParseableSimpleDateFormat.values()) {
try {
return parse_simple(dateStr, f);
} catch (DateTimeParseException e) {
// simply proceed with the next parser
}
}
ParseableSimpleDateFormat[] values = ParseableSimpleDateFormat.values();
StringBuilder allFormats = new StringBuilder("\"") //$NON-NLS-1$
.append(values[0].formatStr);
for (int i = 1; i < values.length; i++)
allFormats.append("\", \"").append(values[i].formatStr); //$NON-NLS-1$
allFormats.append("\""); //$NON-NLS-1$
throw new ParseException(
MessageFormat.format(JGitText.get().cannotParseDate, dateStr,
allFormats.toString()),
0);
}
// tries to parse a string with the formats supported by DateTimeFormatter
private static LocalDateTime parse_simple(String dateStr,
ParseableSimpleDateFormat f) throws DateTimeParseException {
DateTimeFormatter dateFormat = formatCache.computeIfAbsent(f,
format -> DateTimeFormatter.ofPattern(f.formatStr)
.withLocale(SystemReader.getInstance().getLocale()));
TemporalAccessor parsed = dateFormat.parse(dateStr);
return parsed.isSupported(ChronoField.HOUR_OF_DAY)
? LocalDateTime.from(parsed)
: LocalDate.from(parsed).atStartOfDay();
}
// tries to parse a string with a relative time specification
@SuppressWarnings("nls")
private static LocalDateTime parse_relative(String dateStr,
LocalDateTime now) {
// check for the static words "yesterday" or "now"
if ("now".equals(dateStr)) {
return now;
}
if ("yesterday".equals(dateStr)) {
return now.minusDays(1);
}
// parse constructs like "3 days ago", "5.week.2.day.ago"
String[] parts = dateStr.split("\\.| ");
int partsLength = parts.length;
// check we have an odd number of parts (at least 3) and that the last
// part is "ago"
if (partsLength < 3 || (partsLength & 1) == 0
|| !"ago".equals(parts[parts.length - 1]))
return null;
int number;
for (int i = 0; i < parts.length - 2; i += 2) {
try {
number = Integer.parseInt(parts[i]);
} catch (NumberFormatException e) {
return null;
}
if (parts[i + 1] == null) {
return null;
}
switch (parts[i + 1]) {
case "year":
case "years":
now = now.minusYears(number);
break;
case "month":
case "months":
now = now.minusMonths(number);
break;
case "week":
case "weeks":
now = now.minusWeeks(number);
break;
case "day":
case "days":
now = now.minusDays(number);
break;
case "hour":
case "hours":
now = now.minusHours(number);
break;
case "minute":
case "minutes":
now = now.minusMinutes(number);
break;
case "second":
case "seconds":
now = now.minusSeconds(number);
break;
default:
return null;
}
}
return now;
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment