diff --git a/README.md b/README.md index f52a9b6..88ea876 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # Mini-Parsers - An API for parsing discrete types +[![Maven Central](https://img.shields.io/maven-central/v/io.heretical/mini-parsers-core)](https://search.maven.org/search?q=g:io.heretical+mini-parsers) +[![javadoc](https://javadoc.io/badge2/io.heretical/mini-parsers-core/javadoc.svg?label=javadoc+mini-parsers-core)](https://javadoc.io/doc/io.heretical/mini-parsers-core) +[![javadoc](https://javadoc.io/badge2/io.heretical/mini-parsers-temporal/javadoc.svg?label=javadoc+mini-parsers-temporal)](https://javadoc.io/doc/io.heretical/mini-parsers-temporal) + ## Overview Mini-Parsers is a Java API for parsing short discrete text strings into native types where a single type may have @@ -11,29 +15,22 @@ validation, or normalizing data in a column during data cleansing and ETL. For example, the same instant in time (`java.time.Instant`) may have multiple formats. The two strings `1423526400000` and `2015-02-10T02:04:30+00:00` are equivalent if the first is interpreted as the milliseconds since the epoch. -Only absolute and duration text representation disambiguation is currently supported. But with plans for handling more -complex relative temporal representations like `10 days ago` or `last tuesday`. Also support for common units of measure -are being considered. +Absolute, duration, and relative adjuster text representation disambiguation is currently supported. + +But with plans for handling more complex relative temporal representations like `10 days ago` or `last tuesday`. Also +support for common units of measure are being considered. Final Releases are available on Maven Central: ```gradle -implementation 'io.heretical:mini-parsers-core:1.1.0' -implementation 'io.heretical:mini-parsers-temporal:1.1.0' +implementation 'io.heretical:mini-parsers-temporal:2.0.0' ``` ```xml - - io.heretical - mini-parsers-core - 1.1.0 - pom - - io.heretical mini-parsers-temporal - 1.1.0 + 2.0.0 pom ``` @@ -46,7 +43,7 @@ This library requires Java 8 and the parsing functionality is dependent on [Parb ## Usage -For the most comprehensive examples on usage, see the unit tests for each sub-project. +For the most comprehensive examples on usage, see the unit tests for each subproject. ### Temporal Parsers Syntax @@ -87,22 +84,22 @@ Where `number` is any natural integer (with commas). And `unit` is either the Unit name or Abbreviation, case-insensitive: -| Unit | Abbreviation | Example -| -----| ------------ | ------- -| Milliseconds | ms | 300ms -| Seconds | s, sec | 30s 30sec -| Minutes | m, min | 20m 20min -| Hours | h, hrs | 3h 3hrs -| Days | d, days | 5d 5 days -| Weeks | w, wks | 2w 2wks -| Months | mos | 3mos -| Years | y, yrs | 2y 2rs +| Unit | Abbreviation | Example | +|--------------|--------------|-----------| +| Milliseconds | ms | 300ms | +| Seconds | s, sec | 30s 30sec | +| Minutes | m, min | 20m 20min | +| Hours | h, hrs | 3h 3hrs | +| Days | d, days | 5d 5 days | +| Weeks | w, wks | 2w 2wks | +| Months | mos | 3mos | +| Years | y, yrs | 2y 2rs | For example, `10000ms` and `10,000 milliseconds` are equivalent. #### Date/Times -Currently only absolute date/time strings are supported. +Absolute and relative date/time strings are supported. ##### Absolute @@ -135,4 +132,32 @@ This parser builds a grammar for all specified formats. When applied and a match `java.time.format.DateTimeFormatter` for that date/time format is used to parse the value. The parsed result is then coaxed into a `Instant` after applying any context values like `Locale` or `ZoneId`. -Note the grammar is used to search for the actual formatter to use instead of attempting every `DateFormatterInstance`. \ No newline at end of file +Note the grammar is used to search for the actual formatter to use instead of attempting every `DateFormatterInstance`. + +##### Relative + +The class `RelativeDateTimeAdjusterParser` will adjust the current time based on a simple adjustment syntax, and return an +`Instant`. + +The syntax is adopted from [Splunk](https://docs.splunk.com/Documentation/Splunk/latest/Search/Specifytimemodifiersinyoursearch). + +```java +// optionally set the Clock, ZoneId, and Locale +Context context = new Context(); + +RelativeDateTimeAdjusterParser parser = new RelativeDateTimeAdjusterParser( context ); + +java.time.Instant hourAgo = parser.parseOrFail( "-60m" ).getResult(); + +java.time.Instant hourAgoOnTheHour = parser.parseOrFail( "-1h@h" ).getResult(); + +java.time.Instant weekAgoToday = parser.parseOrFail( "-7d@d" ).getResult(); + +java.time.Instant beginningOfCurrentWeek = parser.parseOrFail( "@w0" ).getResult(); // depends on locale + +java.time.Instant tomorrow = parser.parseOrFail( "+24h@s" ).getResult(); +``` + + + + diff --git a/mini-parsers-temporal/src/main/java/heretical/parser/temporal/AbsoluteDateTimeParser.java b/mini-parsers-temporal/src/main/java/heretical/parser/temporal/AbsoluteDateTimeParser.java index 90af73f..3dce1c1 100644 --- a/mini-parsers-temporal/src/main/java/heretical/parser/temporal/AbsoluteDateTimeParser.java +++ b/mini-parsers-temporal/src/main/java/heretical/parser/temporal/AbsoluteDateTimeParser.java @@ -8,14 +8,42 @@ package heretical.parser.temporal; +import java.time.Instant; + import heretical.parser.temporal.grammar.DateTimeGrammar; import org.parboiled.Rule; /** - * + * The AbsoluteDateTimeParser class will parse common date time formats, and return an {@link Instant}. + *

+ * Where common formats supported look like: + *

+ *   02/10/15 02:04
+ *   February/10/15 02:04
+ *   02/10/15 02:04AM
+ *   February 10th 2015, 02
+ *   November 09th 2018, 20:00:00
+ *   02/10/15 02
+ *   20150210
+ *   20150210T020430Z
+ *   2015-02-10T02:04:30+00:00
+ *   2015-02-10 02:04:30+00:00
+ * 
*/ public class AbsoluteDateTimeParser extends DateTimeParser { + /** + * Creates a new AbsoluteDateTimeParser instance. + */ + public AbsoluteDateTimeParser() + { + } + + /** + * Creates a new AbsoluteDateTimeParser instance. + * + * @param context of type Context + */ public AbsoluteDateTimeParser( Context context ) { super( context ); diff --git a/mini-parsers-temporal/src/main/java/heretical/parser/temporal/BaseTemporalExpressionParser.java b/mini-parsers-temporal/src/main/java/heretical/parser/temporal/BaseTemporalExpressionParser.java index bb788b1..0e5217a 100644 --- a/mini-parsers-temporal/src/main/java/heretical/parser/temporal/BaseTemporalExpressionParser.java +++ b/mini-parsers-temporal/src/main/java/heretical/parser/temporal/BaseTemporalExpressionParser.java @@ -27,9 +27,15 @@ public abstract class BaseTemporalExpressionParser> { private static final Logger LOG = LoggerFactory.getLogger( BaseTemporalExpressionParser.class ); - protected Context context; + + private final Context context; private Rule grammar; + public BaseTemporalExpressionParser() + { + this( new Context() ); + } + public BaseTemporalExpressionParser( Context context ) { this.context = context; diff --git a/mini-parsers-temporal/src/main/java/heretical/parser/temporal/Context.java b/mini-parsers-temporal/src/main/java/heretical/parser/temporal/Context.java index e9f3523..f1f3d0b 100644 --- a/mini-parsers-temporal/src/main/java/heretical/parser/temporal/Context.java +++ b/mini-parsers-temporal/src/main/java/heretical/parser/temporal/Context.java @@ -20,6 +20,11 @@ */ public class Context { + /** + * The default locale used by the parser. Defaults to the system default. + *

+ * {@code Locale.getDefault() + */ public static final Locale DEFAULT_LOCALE = Locale.getDefault(); Clock clock = Clock.systemUTC(); @@ -32,23 +37,47 @@ public Context() { } + /** + * Uses the given zoneId and the default locale. + * + * @param zoneId the zoneId to use + * @param locale the locale to use + */ public Context( ZoneId zoneId, Locale locale ) { setClock( Clock.system( zoneId ) ); setLocale( locale ); } + /** + * Uses the given clock and the default locale. + *

+ * Most commonly used for testing. + * + * @param clock the clock to use + * @param locale the locale to use + */ public Context( Clock clock, Locale locale ) { setClock( clock ); setLocale( locale ); } + /** + * Uses the given clock and the default locale. + *

+ * Most commonly used for testing. + * + * @param clock the clock to use + */ public Context( Clock clock ) { this.clock = clock; } + /** + * @return the clock used by the parser + */ public Clock getClock() { return clock; @@ -68,11 +97,17 @@ private void setLocale( Locale locale ) this.locale = locale; } + /** + * @return the locale used by the parser + */ public Locale getLocale() { return locale; } + /** + * @return the week field for the locale + */ public TemporalField getWeekField() { return WeekFields.of( getLocale() ).dayOfWeek(); diff --git a/mini-parsers-temporal/src/main/java/heretical/parser/temporal/DateTimeFormatParseException.java b/mini-parsers-temporal/src/main/java/heretical/parser/temporal/DateTimeFormatParseException.java index b1f2113..c1cbd1e 100644 --- a/mini-parsers-temporal/src/main/java/heretical/parser/temporal/DateTimeFormatParseException.java +++ b/mini-parsers-temporal/src/main/java/heretical/parser/temporal/DateTimeFormatParseException.java @@ -9,29 +9,48 @@ package heretical.parser.temporal; /** - * + * The DateTimeFormatParseException class is thrown when a date time format string cannot be parsed. */ public class DateTimeFormatParseException extends RuntimeException { + /** + * Creates a new DateTimeFormatParseException instance. + */ public DateTimeFormatParseException() { } + /** + * @param message of type String + */ public DateTimeFormatParseException( String message ) { super( message ); } + /** + * @param message of type String + * @param cause of type Throwable + */ public DateTimeFormatParseException( String message, Throwable cause ) { super( message, cause ); } + /** + * @param cause of type Throwable + */ public DateTimeFormatParseException( Throwable cause ) { super( cause ); } + /** + * @param message of type String + * @param cause of type Throwable + * @param enableSuppression of type boolean + * @param writableStackTrace of type boolean + */ public DateTimeFormatParseException( String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace ) { super( message, cause, enableSuppression, writableStackTrace ); diff --git a/mini-parsers-temporal/src/main/java/heretical/parser/temporal/DateTimeParser.java b/mini-parsers-temporal/src/main/java/heretical/parser/temporal/DateTimeParser.java index 1b8d100..bfa28b1 100644 --- a/mini-parsers-temporal/src/main/java/heretical/parser/temporal/DateTimeParser.java +++ b/mini-parsers-temporal/src/main/java/heretical/parser/temporal/DateTimeParser.java @@ -19,6 +19,10 @@ */ public abstract class DateTimeParser extends BaseTemporalExpressionParser { + public DateTimeParser() + { + } + public DateTimeParser( Context context ) { super( context ); diff --git a/mini-parsers-temporal/src/main/java/heretical/parser/temporal/DurationParser.java b/mini-parsers-temporal/src/main/java/heretical/parser/temporal/DurationParser.java index 70989d3..66037f0 100644 --- a/mini-parsers-temporal/src/main/java/heretical/parser/temporal/DurationParser.java +++ b/mini-parsers-temporal/src/main/java/heretical/parser/temporal/DurationParser.java @@ -16,10 +16,21 @@ import org.parboiled.Rule; /** + * The DurationParser class parses strings that represent durations. + *

+ * This grammar can distinguish between either ISO-8601 duration strings, like `PT20.345S`, or simplified + * natural language duration strings, like `10 days` or `15min`, and resolve them into `java.time.Duration` instances. + *

+ * See {@link ISODurationParser} or {@link NaturalDurationParser} for more specific parsers. + *

* This class is not thread-safe. */ public class DurationParser extends BaseTemporalExpressionParser { + public DurationParser() + { + } + public DurationParser( Context context ) { super( context ); diff --git a/mini-parsers-temporal/src/main/java/heretical/parser/temporal/ISODurationParser.java b/mini-parsers-temporal/src/main/java/heretical/parser/temporal/ISODurationParser.java index 7402b97..fa88fda 100644 --- a/mini-parsers-temporal/src/main/java/heretical/parser/temporal/ISODurationParser.java +++ b/mini-parsers-temporal/src/main/java/heretical/parser/temporal/ISODurationParser.java @@ -12,10 +12,31 @@ import org.parboiled.Rule; /** + * The ISODurationParser class parses strings that represent durations in ISO-8601 formats. + *

+ * Where an ISO duration format would look like the following: * + *

+ *   PT20.345S
+ *   +PT20.345S
+ *   -PT20.345S // negated duration
+ * 
+ * This class is not thread-safe. */ public class ISODurationParser extends DurationParser { + /** + * Creates a new ISODurationParser instance. + */ + public ISODurationParser() + { + } + + /** + * Creates a new ISODurationParser instance. + * + * @param context of type Context + */ public ISODurationParser( Context context ) { super( context ); diff --git a/mini-parsers-temporal/src/main/java/heretical/parser/temporal/NaturalDurationParser.java b/mini-parsers-temporal/src/main/java/heretical/parser/temporal/NaturalDurationParser.java index 865fa25..b51bdcc 100644 --- a/mini-parsers-temporal/src/main/java/heretical/parser/temporal/NaturalDurationParser.java +++ b/mini-parsers-temporal/src/main/java/heretical/parser/temporal/NaturalDurationParser.java @@ -12,10 +12,46 @@ import org.parboiled.Rule; /** + * The NaturalDurationParser class parses strings that represent durations a more natural language. + *

+ * Where a natural duration format would look like the following: * + *

+ *    90 seconds
+ *    10,000 seconds
+ *    3 months
+ *  
+ * A natural duration string is of the format: + *

+ *

+ * number[:space:]unit
+ * 
> + *

+ * Where `number` is any natural integer (with commas). + *

+ * And `unit` is either the Unit name or Abbreviation, case-insensitive: + *

+ *

+ * | Unit         | Abbreviation | Example   |
+ * |--------------|--------------|-----------|
+ * | Milliseconds | ms           | 300ms     |
+ * | Seconds      | s, sec       | 30s 30sec |
+ * | Minutes      | m, min       | 20m 20min |
+ * | Hours        | h, hrs       | 3h 3hrs   |
+ * | Days         | d, days      | 5d 5 days |
+ * | Weeks        | w, wks       | 2w 2wks   |
+ * | Months       | mos          | 3mos      |
+ * | Years        | y, yrs       | 2y 2rs    |
+ * 
+ *

+ * This class is not thread-safe. */ public class NaturalDurationParser extends DurationParser { + public NaturalDurationParser() + { + } + public NaturalDurationParser( Context context ) { super( context ); diff --git a/mini-parsers-temporal/src/main/java/heretical/parser/temporal/RelativeDateTimeAdjusterParser.java b/mini-parsers-temporal/src/main/java/heretical/parser/temporal/RelativeDateTimeAdjusterParser.java new file mode 100644 index 0000000..c330ade --- /dev/null +++ b/mini-parsers-temporal/src/main/java/heretical/parser/temporal/RelativeDateTimeAdjusterParser.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2018 Chris K Wensel . All Rights Reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package heretical.parser.temporal; + +import java.time.Instant; +import java.util.function.BiFunction; + +import heretical.parser.temporal.expression.AdjusterExp; +import heretical.parser.temporal.grammar.DateTimeAdjusterGrammar; +import org.parboiled.Rule; + +/** + * The RelativeDateTimeAdjusterParser class will adjust the current time based on a simple adjustment syntax, and return an + * {@link Instant}. + *

+ * The syntax is adopted from Splunk Docs. + *

+ * Where a time adjuster format would look like the following: + * + *

+ *   -1min
+ *   10days
+ *   1y
+ * 
+ *

+ * This class is not thread-safe. + */ +public class RelativeDateTimeAdjusterParser extends BaseTemporalExpressionParser + { + /** + * Creates a new RelativeDateTimeAdjusterParser instance. + */ + public RelativeDateTimeAdjusterParser() + { + } + + /** + * Creates a new RelativeDateTimeAdjusterParser instance. + * + * @param context of type Context + */ + public RelativeDateTimeAdjusterParser( Context context ) + { + super( context ); + } + + @Override + protected Class getParserClass() + { + return DateTimeAdjusterGrammar.class; + } + + @Override + protected Rule getGrammar( DateTimeAdjusterGrammar parser ) + { + return parser.Root(); + } + + @Override + protected BiFunction getFunction() + { + return ( context, expression ) -> expression.toInstant( context ); + } + } diff --git a/mini-parsers-temporal/src/main/java/heretical/parser/temporal/expression/AdjusterExp.java b/mini-parsers-temporal/src/main/java/heretical/parser/temporal/expression/AdjusterExp.java new file mode 100644 index 0000000..8b004b8 --- /dev/null +++ b/mini-parsers-temporal/src/main/java/heretical/parser/temporal/expression/AdjusterExp.java @@ -0,0 +1,256 @@ +/* + * Copyright (c) 2018 Chris K Wensel . All Rights Reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package heretical.parser.temporal.expression; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.temporal.ChronoField; +import java.time.temporal.TemporalField; +import java.time.temporal.WeekFields; +import java.util.Locale; +import java.util.function.Supplier; + +import heretical.parser.temporal.Context; +import heretical.parser.temporal.units.TimeUnit; + +/** + * Adjusts now. + *

+ * http://docs.splunk.com/Documentation/SplunkCloud/7.0.3/SearchReference/SearchTimeModifiers + * http://docs.splunk.com/Documentation/Splunk/7.1.3/Search/Specifytimemodifiersinyoursearch + */ +public class AdjusterExp extends DateTimeExp + { + final Locale locale = Locale.US; // controls which day is the first day of the week + final TemporalField weekField = WeekFields.of( locale ).dayOfWeek(); + + BinaryOp amountOp = BinaryOp.MINUS; // querying towards the past is assumed + int amount = 1; // 1 is assumed + TimeUnit amountUnit; + + TimeUnit snapUnit; + int snapOrdinal = 0; + + BinaryOp offsetOp; + int offset = 0; + TimeUnit offsetUnit; + + public AdjusterExp() + { + } + + public BinaryOp getAmountOp() + { + return amountOp; + } + + public boolean setAmountOp( String amountOp ) + { + return setAmountOp( BinaryOp.lookup( amountOp ) ); + } + + public boolean setAmountOp( BinaryOp amountOp ) + { + if( amountOp != null ) + { + this.amountOp = amountOp; + } + + return true; + } + + public int getAmount() + { + return amount; + } + + public boolean setAmount( String amount ) + { + return setAmount( Integer.parseInt( amount ) ); + } + + public boolean setAmount( int amount ) + { + this.amount = amount; + + return true; + } + + public TimeUnit getAmountUnit() + { + return amountUnit; + } + + public boolean setAmountUnit( TimeUnit amountUnit ) + { + this.amountUnit = amountUnit; + + return true; + } + + public TimeUnit getSnapUnit() + { + return snapUnit; + } + + public boolean setSnapUnit( TimeUnit snapUnit ) + { + this.snapUnit = snapUnit; + + return true; + } + + public int getSnapOrdinal() + { + return snapOrdinal; + } + + public boolean setSnapOrdinal( String ordinal ) + { + return setSnapOrdinal( Integer.parseInt( ordinal ) ); + } + + public boolean setSnapOrdinal( int snapOrdinal ) + { + this.snapOrdinal = snapOrdinal; + + return true; + } + + public BinaryOp getOffsetOp() + { + return offsetOp; + } + + public boolean setOffsetOp( String offsetOp ) + { + return setOffsetOp( BinaryOp.lookup( offsetOp ) ); + } + + public boolean setOffsetOp( BinaryOp offsetOp ) + { + if( offsetOp != null ) + this.offsetOp = offsetOp; + + return true; + } + + public int getOffset() + { + return offset; + } + + public boolean setOffset( String offset ) + { + return setOffset( Integer.parseInt( offset ) ); + } + + public boolean setOffset( int offset ) + { + this.offset = offset; + + return true; + } + + public TimeUnit getOffsetUnit() + { + return offsetUnit; + } + + public boolean setOffsetUnit( TimeUnit offsetUnit ) + { + this.offsetUnit = offsetUnit; + + return true; + } + + public Instant toInstant( Context context ) + { + + LocalDateTime dateTime = LocalDateTime.now( context.getClock() ); + + if( amountUnit != null ) + dateTime = applyBinaryOp( dateTime, amountOp, amount, amountUnit ); + + if( snapUnit != null ) + { + Supplier snapOrdinal = new Supplier() + { + int snapOrdinal = AdjusterExp.this.snapOrdinal; + + @Override + public Integer get() + { + try + { + return snapOrdinal; + } + finally + { + snapOrdinal = 0; + } + } + }; + + switch( snapUnit ) + { + case week: + dateTime = dateTime.with( t -> t.with( weekField, 1 + snapOrdinal.get() ) ); + } + + switch( snapUnit ) + { + case year: + dateTime = dateTime.with( t -> t.with( ChronoField.MONTH_OF_YEAR, 1 + snapOrdinal.get() ) ); + case month: + dateTime = dateTime.with( t -> t.with( ChronoField.DAY_OF_MONTH, 1 + snapOrdinal.get() ) ); + } + + switch( snapUnit ) + { + case year: + case month: + case week: + case day: + dateTime = dateTime.with( t -> t.with( ChronoField.HOUR_OF_DAY, snapOrdinal.get() ) ); + case hour: + dateTime = dateTime.with( t -> t.with( ChronoField.MINUTE_OF_HOUR, snapOrdinal.get() ) ); + case minute: + dateTime = dateTime.with( t -> t.with( ChronoField.SECOND_OF_MINUTE, snapOrdinal.get() ) ); + case second: + dateTime = dateTime.with( t -> t.with( ChronoField.MILLI_OF_SECOND, snapOrdinal.get() ) ); + } + + if( offsetOp != null ) + dateTime = applyBinaryOp( dateTime, offsetOp, offset, offsetUnit ); + } + + return dateTime.toInstant( ZoneOffset.UTC ); + } + + public LocalDateTime applyBinaryOp( LocalDateTime dateTime, BinaryOp op, int amount, TimeUnit unit ) + { + switch( op ) + { + case PLUS: + dateTime = dateTime.plus( amount, unit.unit() ); + break; + + case MINUS: + dateTime = dateTime.minus( amount, unit.unit() ); + break; + + default: + throw new IllegalStateException( "unknown op type" ); + } + + return dateTime; + } + } diff --git a/mini-parsers-temporal/src/main/java/heretical/parser/temporal/expression/BinaryOp.java b/mini-parsers-temporal/src/main/java/heretical/parser/temporal/expression/BinaryOp.java new file mode 100644 index 0000000..28f508d --- /dev/null +++ b/mini-parsers-temporal/src/main/java/heretical/parser/temporal/expression/BinaryOp.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2018 Chris K Wensel . All Rights Reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package heretical.parser.temporal.expression; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +public enum BinaryOp + { + PLUS( "+" ), + MINUS( "-" ); + + static Map map = new HashMap<>(); + + static + { + for( BinaryOp unaryOp : BinaryOp.values() ) + { + map.put( unaryOp.symbol, unaryOp ); + } + } + + public static BinaryOp lookup( String op ) + { + return map.get( op ); + } + + public static String chars() + { + Optional reduce = Arrays.stream( values() ).map( BinaryOp::charSymbol ).map( c -> Character.toString( c ) ).reduce( String::concat ); + + return reduce.orElse( "" ); + } + + String symbol; + + BinaryOp( String symbol ) + { + this.symbol = symbol; + } + + char charSymbol() + { + return symbol.charAt( 0 ); + } + } diff --git a/mini-parsers-temporal/src/main/java/heretical/parser/temporal/expression/OrdinalDateTimeExp.java b/mini-parsers-temporal/src/main/java/heretical/parser/temporal/expression/OrdinalDateTimeExp.java index 24b26b6..880d0a6 100644 --- a/mini-parsers-temporal/src/main/java/heretical/parser/temporal/expression/OrdinalDateTimeExp.java +++ b/mini-parsers-temporal/src/main/java/heretical/parser/temporal/expression/OrdinalDateTimeExp.java @@ -84,7 +84,7 @@ public Optional get() Month currentMonth = dateTime.getMonth(); int year = dateTime.getYear(); Optional requested = snapOrdinal.get(); - if( currentMonth.ordinal()+1 <= requested.orElse( 1 ) ) + if( currentMonth.ordinal() + 1 <= requested.orElse( 1 ) ) dateTime = dateTime.with( t -> t.with( ChronoField.YEAR, year - 1 ) ); dateTime = dateTime.with( t -> t.with( ChronoField.MONTH_OF_YEAR, requested.orElse( 1 ) ) ); diff --git a/mini-parsers-temporal/src/main/java/heretical/parser/temporal/format/DateTimeFormats.java b/mini-parsers-temporal/src/main/java/heretical/parser/temporal/format/DateTimeFormats.java index f8870d7..0889ac0 100644 --- a/mini-parsers-temporal/src/main/java/heretical/parser/temporal/format/DateTimeFormats.java +++ b/mini-parsers-temporal/src/main/java/heretical/parser/temporal/format/DateTimeFormats.java @@ -184,9 +184,9 @@ public static List parsePattern( String dateFormat ) { // these guys are not thread safe DateTimePatternGrammar parser = Parboiled.createParser( DateTimePatternGrammar.class ); - ParseRunner runner = new BasicParseRunner<>( parser.DateTimeRoot() ); + ParseRunner runner = new BasicParseRunner<>( parser.DateTimeRoot() ); - ParsingResult result = runner.run( dateFormat ); + ParsingResult result = runner.run( dateFormat ); LinkedList list = new LinkedList<>(); @@ -386,19 +386,19 @@ public DateTimeFormatter getParser() } public static final Comparator longToShort = ( lhs, rhs ) -> - { - lhs = lhs.replace( "/'", "" ); - rhs = rhs.replace( "/'", "" ); + { + lhs = lhs.replace( "/'", "" ); + rhs = rhs.replace( "/'", "" ); - int lhsLen = lhs.length(); - int rhsLen = rhs.length(); - int len = rhsLen - lhsLen; + int lhsLen = lhs.length(); + int rhsLen = rhs.length(); + int len = rhsLen - lhsLen; - if( len != 0 ) - return len; + if( len != 0 ) + return len; - return lhs.compareTo( rhs ); - }; + return lhs.compareTo( rhs ); + }; static Map patternMap = new TreeMap<>( longToShort ); diff --git a/mini-parsers-temporal/src/main/java/heretical/parser/temporal/grammar/DateTimeAdjusterGrammar.java b/mini-parsers-temporal/src/main/java/heretical/parser/temporal/grammar/DateTimeAdjusterGrammar.java new file mode 100644 index 0000000..c6210d2 --- /dev/null +++ b/mini-parsers-temporal/src/main/java/heretical/parser/temporal/grammar/DateTimeAdjusterGrammar.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2018 Chris K Wensel . All Rights Reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package heretical.parser.temporal.grammar; + +import java.util.Set; + +import heretical.parser.common.BaseSyntaxGrammar; +import heretical.parser.temporal.expression.AdjusterExp; +import heretical.parser.temporal.expression.BinaryOp; +import heretical.parser.temporal.units.TimeUnit; +import org.parboiled.Rule; +import org.parboiled.annotations.Cached; +import org.parboiled.common.Reference; +import org.parboiled.support.Var; + +/** + *
+ * http://docs.splunk.com/Documentation/SplunkCloud/7.0.3/SearchReference/SearchTimeModifiers
+ * http://docs.splunk.com/Documentation/Splunk/7.1.3/Search/Specifytimemodifiersinyoursearch
+ */
+public class DateTimeAdjusterGrammar extends BaseSyntaxGrammar
+  {
+  @SuppressWarnings("unused")
+  public DateTimeAdjusterGrammar()
+    {
+    }
+
+  public Rule Root()
+    {
+    Var relativeTime = new Var<>( new AdjusterExp() );
+
+    return Sequence(
+      FirstOf(
+        Now(), // now
+        Sequence(
+          Optional(
+            Adjust( relativeTime ) ) // [+|-][]
+          ,
+          Optional(
+            Snap( relativeTime ) // @[][[+|-]]
+          )
+        )
+      ),
+      push( relativeTime.get() ),
+      Spacing(),
+      EOI
+    );
+    }
+
+  public Rule Now()
+    {
+    return IgnoreCase( "now" );
+    }
+
+  public Rule Adjust( Var relativeTime )
+    {
+    Var adjustUnit = new Var<>();
+
+    return Sequence(
+      Optional( AnyOf( BinaryOp.chars() ), relativeTime.get().setAmountOp( match() ) ),
+      Optional( Number(), relativeTime.get().setAmount( match() ) ),
+      Units( adjustUnit ), relativeTime.get().setAmountUnit( adjustUnit.get() )
+    );
+    }
+
+  public Rule Snap( Var relativeTime )
+    {
+    Var snapUnit = new Var<>();
+    Var offsetUnit = new Var<>();
+
+    return Sequence(
+      '@',
+      Units( snapUnit ), relativeTime.get().setSnapUnit( snapUnit.get() ),
+      Optional( Number(), relativeTime.get().setSnapOrdinal( match() ) ),
+      Optional(
+        AnyOf( BinaryOp.chars() ), relativeTime.get().setOffsetOp( match() ),
+        Number(), relativeTime.get().setOffset( match() ),
+        Units( offsetUnit ), relativeTime.get().setOffsetUnit( offsetUnit.get() )
+      )
+    );
+    }
+
+  @Cached
+  public Rule Units( Var relativeTime )
+    {
+    Set tokens = TimeUnit.tokens();
+
+    Rule[] units = new Rule[ tokens.size() ];
+
+    int count = 0;
+
+    for( TimeUnit.Token token : tokens )
+      {
+      units[ count++ ] = Unit( relativeTime, token.abbreviation(), token.unit() );
+      }
+
+    return FirstOf( units );
+    }
+
+  public Rule Unit( Var relativeTime, String abbreviation, TimeUnit timeUnit )
+    {
+    Reference ref = new Reference<>( timeUnit );
+    return Sequence( abbreviation, relativeTime.set( ref.get() ) );
+    }
+  }
diff --git a/mini-parsers-temporal/src/main/java/heretical/parser/temporal/grammar/DateTimeGrammar.java b/mini-parsers-temporal/src/main/java/heretical/parser/temporal/grammar/DateTimeGrammar.java
index b6e73d2..9ce1b68 100644
--- a/mini-parsers-temporal/src/main/java/heretical/parser/temporal/grammar/DateTimeGrammar.java
+++ b/mini-parsers-temporal/src/main/java/heretical/parser/temporal/grammar/DateTimeGrammar.java
@@ -260,7 +260,6 @@ Rule RelativeLastUnitSymbol( CalendarUnit unit )
     );
     }
 
-
   Rule RelativeLastTerm()
     {
     Var var = new Var( new OffsetDateTimeExp() );
diff --git a/mini-parsers-temporal/src/main/java/heretical/parser/temporal/units/TimeUnit.java b/mini-parsers-temporal/src/main/java/heretical/parser/temporal/units/TimeUnit.java
new file mode 100644
index 0000000..8b1b3b9
--- /dev/null
+++ b/mini-parsers-temporal/src/main/java/heretical/parser/temporal/units/TimeUnit.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (c) 2018 Chris K Wensel . All Rights Reserved.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+package heretical.parser.temporal.units;
+
+import java.time.temporal.ChronoUnit;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.Set;
+import java.util.TreeSet;
+
+public enum TimeUnit
+  {
+    second( ChronoUnit.SECONDS, "s", "sec", "secs", "second", "seconds" ),
+    minute( ChronoUnit.MINUTES, "m", "min", "minute", "minutes" ),
+    hour( ChronoUnit.HOURS, "h", "hr", "hrs", "hour", "hours" ),
+    day( ChronoUnit.DAYS, "d", "day", "days" ),
+    week( ChronoUnit.WEEKS, "w", "week", "weeks" ),
+    month( ChronoUnit.MONTHS, "mon", "month", "months" ),
+    //        quarter(ChronoUnit. "q", "qtr", "qtrs", "quarter", "quarters"),
+    year( ChronoUnit.YEARS, "y", "yr", "yrs", "year", "years" );
+
+  ChronoUnit unit;
+  String[] abbreviations;
+
+  TimeUnit( ChronoUnit unit, String... abbreviations )
+    {
+    this.unit = unit;
+    this.abbreviations = abbreviations;
+
+    // long to short to guide the parser search tree
+    Arrays.sort( this.abbreviations, Comparator.comparing( String::length ).reversed() );
+    }
+
+  public ChronoUnit unit()
+    {
+    return unit;
+    }
+
+  public String[] abbreviations()
+    {
+    return abbreviations;
+    }
+
+  public static class Token implements Comparable
+    {
+    String abbreviation;
+    TimeUnit unit;
+
+    public Token( String abbreviation, TimeUnit unit )
+      {
+      this.abbreviation = abbreviation;
+      this.unit = unit;
+      }
+
+    public String abbreviation()
+      {
+      return abbreviation;
+      }
+
+    public TimeUnit unit()
+      {
+      return unit;
+      }
+
+    public int length()
+      {
+      return abbreviation.length();
+      }
+
+    @Override
+    public int compareTo( Object o )
+      {
+      return abbreviation.compareTo( o.toString() );
+      }
+    }
+
+  /**
+   * In order for the parser to effectively walk down all possible parse paths, we must give the parser all the
+   * abbreviations across all units from longest to shortest.
+   *
+   * @return a Set of all abbreviation to unit pairs
+   */
+  public static Set tokens()
+    {
+    // TreeSet also uses the comparator for identity, so we must provide a compare function when strings
+    // are of the same length
+    Set results = new TreeSet<>( Comparator.comparing( Token::length ).reversed().thenComparing( Token::compareTo ) );
+
+    for( TimeUnit unit : values() )
+      {
+      for( String abbreviation : unit.abbreviations )
+        {
+        results.add( new Token( abbreviation, unit ) );
+        }
+      }
+
+    return results;
+    }
+  }
diff --git a/mini-parsers-temporal/src/test/java/heretical/parser/temporal/api/APITest.java b/mini-parsers-temporal/src/test/java/heretical/parser/temporal/api/APITest.java
index 49a99a3..1870a45 100644
--- a/mini-parsers-temporal/src/test/java/heretical/parser/temporal/api/APITest.java
+++ b/mini-parsers-temporal/src/test/java/heretical/parser/temporal/api/APITest.java
@@ -21,9 +21,13 @@
 import heretical.parser.temporal.DurationParser;
 import heretical.parser.temporal.ISODurationParser;
 import heretical.parser.temporal.NaturalDurationParser;
+import heretical.parser.temporal.RelativeDateTimeAdjusterParser;
 import heretical.parser.temporal.TemporalResult;
+import heretical.parser.temporal.expression.AdjusterExp;
 import heretical.parser.temporal.expression.DateTimeExp;
 import heretical.parser.temporal.expression.DurationExp;
+import heretical.parser.temporal.util.FixedClockRule;
+import org.junit.Rule;
 import org.junit.Test;
 
 import static java.time.Duration.ZERO;
@@ -34,7 +38,10 @@
  */
 public class APITest
   {
-  private Context context = new Context();
+  @Rule
+  public FixedClockRule now = new FixedClockRule( "2015-02-10T02:04:30Z" );
+
+  private Context context = new Context( now.clock() );
 
   @Test
   public void absolute()
@@ -90,4 +97,18 @@ public void durationNaturalFail()
 
     assertEquals( ZERO.plus( 2, ChronoUnit.HOURS ), result.getResult() );
     }
+
+  @Test
+  public void adjuster()
+    {
+    TemporalResult result = new RelativeDateTimeAdjusterParser( context ).parseOrFail( "-120m@s" );
+
+    TemporalAccessor parsed = DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse( "2015-02-10T02:04:30+00:00" );
+    Instant instant = Instant.ofEpochSecond( parsed.getLong( ChronoField.INSTANT_SECONDS ) )
+      .plusMillis( parsed.getLong( ChronoField.MILLI_OF_SECOND ) );
+
+    instant = instant.minus( 120, ChronoUnit.MINUTES );
+
+    assertEquals( instant, result.getResult() );
+    }
   }
diff --git a/mini-parsers-temporal/src/test/java/heretical/parser/temporal/expression/RelativeTimeAdjusterExpTest.java b/mini-parsers-temporal/src/test/java/heretical/parser/temporal/expression/RelativeTimeAdjusterExpTest.java
new file mode 100644
index 0000000..7078670
--- /dev/null
+++ b/mini-parsers-temporal/src/test/java/heretical/parser/temporal/expression/RelativeTimeAdjusterExpTest.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (c) 2018 Chris K Wensel . All Rights Reserved.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+package heretical.parser.temporal.expression;
+
+import java.time.Instant;
+import java.time.temporal.ChronoField;
+import java.time.temporal.ChronoUnit;
+import java.time.temporal.TemporalField;
+import java.time.temporal.WeekFields;
+import java.util.Locale;
+
+import heretical.parser.temporal.Context;
+import heretical.parser.temporal.units.TimeUnit;
+import heretical.parser.temporal.util.FixedClockRule;
+import org.junit.Rule;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+public class RelativeTimeAdjusterExpTest
+  {
+  @Rule
+  public FixedClockRule now = new FixedClockRule();
+
+  public Context context = new Context( now.clock() );
+
+  protected AdjusterExp relativeTime()
+    {
+    return new AdjusterExp();
+    }
+
+  @Test
+  public void amountMinutes()
+    {
+    Instant time = now.clock().instant().minus( 1, ChronoUnit.MINUTES );
+    AdjusterExp adjusterExp = relativeTime();
+
+    adjusterExp.setAmountUnit( TimeUnit.minute );
+
+    assertEquals( time, adjusterExp.toInstant( context ) );
+
+    adjusterExp = relativeTime();
+    adjusterExp.setAmount( 1 );
+    adjusterExp.setAmountUnit( TimeUnit.minute );
+
+    assertEquals( time, adjusterExp.toInstant( context ) );
+
+    adjusterExp = relativeTime();
+    adjusterExp.setAmountOp( BinaryOp.MINUS );
+    adjusterExp.setAmount( 1 );
+    adjusterExp.setAmountUnit( TimeUnit.minute );
+
+    assertEquals( time, adjusterExp.toInstant( context ) );
+    }
+
+  @Test
+  public void amountDays()
+    {
+    Instant time = now.clock().instant().minus( 10, ChronoUnit.DAYS );
+    AdjusterExp adjusterExp = relativeTime();
+
+    adjusterExp.setAmountOp( BinaryOp.MINUS );
+    adjusterExp.setAmount( 10 );
+    adjusterExp.setAmountUnit( TimeUnit.day );
+
+    assertEquals( time, adjusterExp.toInstant( context ) );
+    }
+
+  @Test
+  public void snapMinute()
+    {
+    AdjusterExp adjusterExp = relativeTime();
+
+    adjusterExp.setSnapUnit( TimeUnit.minute );
+
+    assertEquals( now.with( t -> t.with( ChronoField.SECOND_OF_MINUTE, 0 ) ), adjusterExp.toInstant( context ) );
+    }
+
+  @Test
+  public void snapHour()
+    {
+    AdjusterExp adjusterExp;
+
+    adjusterExp = relativeTime();
+    adjusterExp.setSnapUnit( TimeUnit.hour );
+
+    Instant hour = now.with(
+      m -> m.with( ChronoField.MINUTE_OF_HOUR, 0 )
+        .with( ChronoField.SECOND_OF_MINUTE, 0 )
+    );
+    assertEquals( hour, adjusterExp.toInstant( context ) );
+    }
+
+  @Test
+  public void snapDay()
+    {
+    AdjusterExp adjusterExp = relativeTime();
+
+    adjusterExp.setSnapUnit( TimeUnit.day );
+
+    Instant day = now.with(
+      d -> d.with( ChronoField.HOUR_OF_DAY, 0 )
+        .with( ChronoField.MINUTE_OF_HOUR, 0 )
+        .with( ChronoField.SECOND_OF_MINUTE, 0 )
+    );
+    assertEquals( day, adjusterExp.toInstant( context ) );
+    }
+
+  @Test
+  public void snapMonth()
+    {
+    AdjusterExp adjusterExp = relativeTime();
+
+    adjusterExp.setSnapUnit( TimeUnit.month );
+
+    Instant month = now.with(
+      mon -> mon.with( ChronoField.DAY_OF_MONTH, 1 )
+        .with( ChronoField.HOUR_OF_DAY, 0 )
+        .with( ChronoField.MINUTE_OF_HOUR, 0 )
+        .with( ChronoField.SECOND_OF_MINUTE, 0 )
+    );
+    assertEquals( month, adjusterExp.toInstant( context ) );
+    }
+
+  @Test
+  public void snapYear()
+    {
+    AdjusterExp adjusterExp = relativeTime();
+
+    adjusterExp.setSnapUnit( TimeUnit.year );
+
+    Instant year = now.with(
+      y -> y.with( ChronoField.MONTH_OF_YEAR, 1 )
+        .with( ChronoField.DAY_OF_MONTH, 1 )
+        .with( ChronoField.HOUR_OF_DAY, 0 )
+        .with( ChronoField.MINUTE_OF_HOUR, 0 )
+        .with( ChronoField.SECOND_OF_MINUTE, 0 )
+    );
+    assertEquals( year, adjusterExp.toInstant( context ) );
+    }
+
+  @Test
+  public void snapWeek()
+    {
+    AdjusterExp adjusterExp = relativeTime();
+
+    adjusterExp.setSnapUnit( TimeUnit.week );
+
+    TemporalField weekField = WeekFields.of( Locale.US ).dayOfWeek();
+
+    Instant week = now.with( t -> t.with( weekField, 1 )
+      .with( ChronoField.HOUR_OF_DAY, 0 )
+      .with( ChronoField.MINUTE_OF_HOUR, 0 )
+      .with( ChronoField.SECOND_OF_MINUTE, 0 )
+    );
+    assertEquals( week, adjusterExp.toInstant( context ) );
+    }
+
+  @Test
+  public void snapMinuteOffset()
+    {
+    AdjusterExp adjusterExp = relativeTime();
+
+    adjusterExp.setSnapUnit( TimeUnit.minute );
+
+    assertEquals( now.with( t -> t.with( ChronoField.SECOND_OF_MINUTE, 0 ) ), adjusterExp.toInstant( context ) );
+    }
+  }
diff --git a/mini-parsers-temporal/src/test/java/heretical/parser/temporal/grammar/RelativeDateTimeAdjusterParserSyntaxTest.java b/mini-parsers-temporal/src/test/java/heretical/parser/temporal/grammar/RelativeDateTimeAdjusterParserSyntaxTest.java
new file mode 100644
index 0000000..9bc4199
--- /dev/null
+++ b/mini-parsers-temporal/src/test/java/heretical/parser/temporal/grammar/RelativeDateTimeAdjusterParserSyntaxTest.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (c) 2018 Chris K Wensel . All Rights Reserved.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+package heretical.parser.temporal.grammar;
+
+import java.time.Instant;
+import java.time.ZoneOffset;
+import java.time.temporal.ChronoUnit;
+
+import org.junit.Ignore;
+import org.junit.Test;
+
+import static java.time.temporal.ChronoField.*;
+
+public class RelativeDateTimeAdjusterParserSyntaxTest extends RelativeDateTimeAdjusterParserSyntaxTestCase
+  {
+  public RelativeDateTimeAdjusterParserSyntaxTest()
+    {
+    super( false, "2011-12-03T10:15:30.300Z" );
+    }
+
+  @Ignore
+  public void testOneOnly()
+    {
+    Instant time = now.localDateTime().minus( 1, ChronoUnit.MONTHS ).toInstant( ZoneOffset.UTC );
+    assertEquals( time, "mon" );
+    }
+
+  @Test
+  public void testAmount()
+    {
+    Instant time;
+
+    time = now.minus( 1, ChronoUnit.MINUTES );
+    assertEquals( time, "m" );
+    assertEquals( time, "-m" );
+    assertEquals( time, "-1m" );
+    assertEquals( time, "-1min" );
+    assertEquals( time, "-1minute" );
+    assertEquals( time, "-1minutes" );
+    assertNotMatched( "-1minutesw" );
+    assertNotEquals( time, "+m" );
+
+    time = now.minus( 1, ChronoUnit.HOURS );
+    assertEquals( time, "1h" );
+    assertEquals( time, "1hour" );
+    assertEquals( time, "-1hours" );
+    assertNotEquals( time, "+1h" );
+
+    time = now.minus( 10, ChronoUnit.DAYS );
+    assertEquals( time, "10d" );
+    assertEquals( time, "10days" );
+    assertEquals( time, "-10days" );
+    assertNotEquals( time, "+10days" );
+
+    time = now.minus( 10, ChronoUnit.WEEKS );
+    assertEquals( time, "10w" );
+    assertEquals( time, "-10week" );
+    assertEquals( time, "-10weeks" );
+    assertNotEquals( time, "+10weeks" );
+
+    time = now.minus( 1, ChronoUnit.MONTHS );
+    assertEquals( time, "mon" );
+    assertEquals( time, "-1mon" );
+    assertEquals( time, "-1month" );
+    assertEquals( time, "-1months" );
+    assertNotEquals( time, "+10mon" );
+
+    time = now.minus( 1, ChronoUnit.YEARS );
+    assertEquals( time, "y" );
+    assertEquals( time, "year" );
+    assertEquals( time, "years" );
+    assertEquals( time, "-1y" );
+    assertNotEquals( time, "+1year" );
+    }
+
+  @Test
+  public void testSnap()
+    {
+    Instant time;
+
+    time = now.with( t -> t.with( SECOND_OF_MINUTE, 0 ).with( MILLI_OF_SECOND, 0 ) );
+    assertEquals( time, "@m" );
+
+    time = now.with( t -> t.with( MINUTE_OF_HOUR, 0 ).with( SECOND_OF_MINUTE, 0 ).with( MILLI_OF_SECOND, 0 ) );
+    assertEquals( time, "@h" );
+
+    time = now.with( t -> t.with( HOUR_OF_DAY, 0 ).with( MINUTE_OF_HOUR, 0 ).with( SECOND_OF_MINUTE, 0 ).with( MILLI_OF_SECOND, 0 ) );
+    assertEquals( time, "@d" );
+
+    time = now.with( t -> t.with( now.getWeekField(), 1 ).with( HOUR_OF_DAY, 0 ).with( MINUTE_OF_HOUR, 0 ).with( SECOND_OF_MINUTE, 0 ).with( MILLI_OF_SECOND, 0 ) );
+    assertEquals( time, "@w" );
+
+    time = now.with( t -> t.with( DAY_OF_MONTH, 1 ).with( HOUR_OF_DAY, 0 ).with( MINUTE_OF_HOUR, 0 ).with( SECOND_OF_MINUTE, 0 ).with( MILLI_OF_SECOND, 0 ) );
+    assertEquals( time, "@mon" );
+
+    time = now.with( t -> t.with( MONTH_OF_YEAR, 1 ).with( DAY_OF_MONTH, 1 ).with( HOUR_OF_DAY, 0 ).with( MINUTE_OF_HOUR, 0 ).with( SECOND_OF_MINUTE, 0 ).with( MILLI_OF_SECOND, 0 ) );
+    assertEquals( time, "@y" );
+    }
+  }
diff --git a/mini-parsers-temporal/src/test/java/heretical/parser/temporal/grammar/RelativeDateTimeAdjusterParserSyntaxTestCase.java b/mini-parsers-temporal/src/test/java/heretical/parser/temporal/grammar/RelativeDateTimeAdjusterParserSyntaxTestCase.java
new file mode 100644
index 0000000..64b82a2
--- /dev/null
+++ b/mini-parsers-temporal/src/test/java/heretical/parser/temporal/grammar/RelativeDateTimeAdjusterParserSyntaxTestCase.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2018 Chris K Wensel . All Rights Reserved.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+package heretical.parser.temporal.grammar;
+
+import java.time.Instant;
+
+import heretical.parser.common.ParserTestCase;
+import heretical.parser.temporal.Context;
+import heretical.parser.temporal.expression.AdjusterExp;
+import heretical.parser.temporal.util.FixedClockRule;
+import org.junit.Assert;
+import org.junit.Rule;
+import org.parboiled.support.ParsingResult;
+
+public class RelativeDateTimeAdjusterParserSyntaxTestCase extends ParserTestCase
+  {
+  @Rule
+  public FixedClockRule now = new FixedClockRule();
+
+  public RelativeDateTimeAdjusterParserSyntaxTestCase( boolean useTracingRunning, String now )
+    {
+    super( useTracingRunning );
+    this.now.setNow( now );
+    }
+
+  @Override
+  protected Class getParserGrammarClass()
+    {
+    return DateTimeAdjusterGrammar.class;
+    }
+
+  @Override
+  protected org.parboiled.Rule getGrammarRoot( DateTimeAdjusterGrammar dateTimeAdjusterGrammar )
+    {
+    return dateTimeAdjusterGrammar.Root();
+    }
+
+  private Context getContext()
+    {
+    return new Context( now.clock() );
+    }
+
+  protected void assertEquals( Instant expected, String string )
+    {
+    ParsingResult parsingResult = assertParse( string );
+
+    Assert.assertTrue( "did not match: " + string, parsingResult.matched );
+
+    Assert.assertEquals( expected, parsingResult.resultValue.toInstant( getContext() ) );
+    }
+
+  protected void assertNotEquals( Instant expected, String query )
+    {
+    ParsingResult parsingResult = assertParse( query );
+
+    Assert.assertNotEquals( expected, parsingResult.resultValue.toInstant( getContext() ) );
+    }
+  }
diff --git a/mini-parsers-temporal/src/test/java/heretical/parser/temporal/grammar/RelativeDateTimeAdjusterRelativeAndRelativeSnapParseTest.java b/mini-parsers-temporal/src/test/java/heretical/parser/temporal/grammar/RelativeDateTimeAdjusterRelativeAndRelativeSnapParseTest.java
new file mode 100644
index 0000000..5c1d787
--- /dev/null
+++ b/mini-parsers-temporal/src/test/java/heretical/parser/temporal/grammar/RelativeDateTimeAdjusterRelativeAndRelativeSnapParseTest.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2018 Chris K Wensel . All Rights Reserved.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+package heretical.parser.temporal.grammar;
+
+import org.junit.Test;
+
+import static heretical.parser.temporal.util.FixedClockRule.toInstant;
+
+/**
+ * Splunk Docs
+ * 

+ * On April 28th, you decide to run a search at 14:05. + */ +public class RelativeDateTimeAdjusterRelativeAndRelativeSnapParseTest extends RelativeDateTimeAdjusterParserSyntaxTestCase + { + public RelativeDateTimeAdjusterRelativeAndRelativeSnapParseTest() + { + super( false, "2022-04-28T14:05:00Z" ); + } + + @Test + public void example() + { + // If you specify earliest=-2d, the search goes back exactly two days, starting at 14:05 on April 26th. + assertEquals( toInstant( "2022-04-26T14:05:00Z" ), "-2d" ); + assertEquals( toInstant( "2022-04-26T14:05:00Z" ), "2d" ); + // If you specify earliest=-2d@d, the search goes back to two days and snaps to the beginning of the day. + // The search looks for events starting from 00:00 on April 26th. + assertEquals( toInstant( "2022-04-26T00:00:00Z" ), "-2d@d" ); + assertEquals( toInstant( "2022-04-26T00:00:00Z" ), "2d@d" ); + } + } diff --git a/mini-parsers-temporal/src/test/java/heretical/parser/temporal/grammar/RelativeDateTimeAdjusterRelativeParseTest.java b/mini-parsers-temporal/src/test/java/heretical/parser/temporal/grammar/RelativeDateTimeAdjusterRelativeParseTest.java new file mode 100644 index 0000000..1c04200 --- /dev/null +++ b/mini-parsers-temporal/src/test/java/heretical/parser/temporal/grammar/RelativeDateTimeAdjusterRelativeParseTest.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2018 Chris K Wensel . All Rights Reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package heretical.parser.temporal.grammar; + +import org.junit.Test; + +import static heretical.parser.temporal.util.FixedClockRule.toInstant; + +/** + * Splunk Modifiers + *

+ * For these examples, the current time is Wednesday, 09 February 2022, 01:37:05 P.M. + * Also note that 24h is usually but not always equivalent to 1d because of Daylight Savings Time boundaries. + */ +public class RelativeDateTimeAdjusterRelativeParseTest extends RelativeDateTimeAdjusterParserSyntaxTestCase + { + public RelativeDateTimeAdjusterRelativeParseTest() + { + super( false, "2022-02-09T13:37:05Z" ); + } + + @Test + public void example() + { + // past + // now Now, the current time Wednesday, 09 February 2022, 01:37:05 P.M. + assertEquals( toInstant( "2022-02-09T13:37:05Z" ), "now" ); + // -60m 60 minutes ago Wednesday, 09 February 2022, 12:37:05 P.M. -60m@s + assertEquals( toInstant( "2022-02-09T12:37:05Z" ), "-60m" ); + assertEquals( toInstant( "2022-02-09T12:37:05Z" ), "-60m@s" ); + assertEquals( toInstant( "2022-02-09T12:37:05Z" ), "60m@s" ); + // -1h@h 1 hour ago, to the hour Wednesday, 09 February 2022, 12:00:00 P.M. + assertEquals( toInstant( "2022-02-09T12:00:00Z" ), "-1h@h" ); + // -1d@d Yesterday Tuesday, 08 February 2022, 12:00:00 A.M. + assertEquals( toInstant( "2022-02-08T00:00:00Z" ), "-1d@d" ); + assertEquals( toInstant( "2022-02-08T00:00:00Z" ), "1d@d" ); + // -24h 24 hours ago (yesterday) Tuesday, 08 February 2022, 01:37:05 P.M. -24h@s + assertEquals( toInstant( "2022-02-08T13:37:05Z" ), "-24h" ); + assertEquals( toInstant( "2022-02-08T13:37:05Z" ), "-24h@s" ); + // -7d@d 7 days ago, 1 week ago today Wednesday, 02 February 2022, 12:00:00 A.M. + assertEquals( toInstant( "2022-02-02T00:00:00Z" ), "-7d@d" ); + // -7d@m 7 days ago, snap to minute boundary Wednesday, 02 February 2022, 01:37:00 P.M. + assertEquals( toInstant( "2022-02-02T13:37:00Z" ), "-7d@m" ); + + // week + // @w0 Beginning of the current week Sunday, 06 February 2022, 12:00:00 A.M. + assertEquals( toInstant( "2022-02-06T00:00:00Z" ), "@w0" ); + + // future + // +1d@d Tomorrow Thursday, 10 February 2022, 12:00:00 A.M. + assertEquals( toInstant( "2022-02-10T00:00:00Z" ), "+1d@d" ); + // +24h 24 hours from now, tomorrow Thursday, 10 February 2022, 01:37:05 P.M. +24h@s + assertEquals( toInstant( "2022-02-10T13:37:05Z" ), "+24h" ); + assertEquals( toInstant( "2022-02-10T13:37:05Z" ), "+24h@s" ); + } + + @Test + public void exampleChained() + { + // past + // @d-2h Snap to the beginning of today (12 A.M.) and subtract 2 hours from that time. 10 P.M. last night. + assertEquals( toInstant( "2022-02-08T22:00:00Z" ), "@d-2h" ); + // -mon@mon+7d One month ago, snapped to the first of the month at midnight, and add 7 days. The 8th of last month at 12 A.M. + assertEquals( toInstant( "2022-01-08T00:00:00Z" ), "-mon@mon+7d" ); + } + } diff --git a/mini-parsers-temporal/src/test/java/heretical/parser/temporal/grammar/RelativeDateTimeParserSyntaxTest.java b/mini-parsers-temporal/src/test/java/heretical/parser/temporal/grammar/RelativeDateTimeParserSyntaxTest.java index 6349dc3..571e516 100644 --- a/mini-parsers-temporal/src/test/java/heretical/parser/temporal/grammar/RelativeDateTimeParserSyntaxTest.java +++ b/mini-parsers-temporal/src/test/java/heretical/parser/temporal/grammar/RelativeDateTimeParserSyntaxTest.java @@ -14,7 +14,7 @@ import static heretical.parser.temporal.grammar.TestDateTimeUtil.*; /** - * + * Unsupported */ @Ignore public class RelativeDateTimeParserSyntaxTest extends DateTimeParserSyntaxTestCase diff --git a/mini-parsers-temporal/src/test/java/heretical/parser/temporal/grammar/RelaxedDateTimeParserSyntaxTest.java b/mini-parsers-temporal/src/test/java/heretical/parser/temporal/grammar/RelaxedDateTimeParserSyntaxTest.java index 1bd8678..47e685b 100644 --- a/mini-parsers-temporal/src/test/java/heretical/parser/temporal/grammar/RelaxedDateTimeParserSyntaxTest.java +++ b/mini-parsers-temporal/src/test/java/heretical/parser/temporal/grammar/RelaxedDateTimeParserSyntaxTest.java @@ -14,7 +14,7 @@ import static heretical.parser.temporal.grammar.TestDateTimeUtil.*; /** - * + * Unsupported */ @Ignore public class RelaxedDateTimeParserSyntaxTest extends DateTimeParserSyntaxTestCase @@ -25,6 +25,7 @@ public RelaxedDateTimeParserSyntaxTest() } /** + * */ @Test public void test() diff --git a/mini-parsers-temporal/src/test/java/heretical/parser/temporal/util/FixedClockRule.java b/mini-parsers-temporal/src/test/java/heretical/parser/temporal/util/FixedClockRule.java new file mode 100644 index 0000000..995f8c0 --- /dev/null +++ b/mini-parsers-temporal/src/test/java/heretical/parser/temporal/util/FixedClockRule.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2018 Chris K Wensel . All Rights Reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package heretical.parser.temporal.util; + +import java.time.Clock; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAdjuster; +import java.time.temporal.TemporalField; +import java.time.temporal.TemporalUnit; +import java.time.temporal.WeekFields; +import java.util.Locale; + +import org.junit.rules.ExternalResource; + +/** + * + */ +public class FixedClockRule extends ExternalResource + { + public static final String NOW = "2022-12-03T10:15:30Z"; + + protected Locale locale = Locale.US; + protected TemporalField weekField; + private String now = NOW; + + public FixedClockRule() + { + } + + public FixedClockRule( String now ) + { + this.now = now; + } + + public Locale getLocale() + { + return locale; + } + + public void setLocale( Locale locale ) + { + this.locale = locale; + this.weekField = null; + } + + public TemporalField getWeekField() + { + if( weekField == null ) + { + weekField = WeekFields.of( locale ).dayOfWeek(); + } + + return weekField; + } + + public void setNow( String now ) + { + this.now = now; + } + + public Instant getNow() + { + return toInstant( now ); + } + + public static Instant toInstant( String isoInstant ) + { + return Instant.from( DateTimeFormatter.ISO_INSTANT.parse( isoInstant ) ); + } + + public Clock clock() + { + Instant instant = getNow(); + return Clock.fixed( instant, ZoneOffset.UTC ); + } + + public Instant instant() + { + return clock().instant(); + } + + public LocalDateTime localDateTime() + { + return LocalDateTime.ofInstant( instant(), ZoneOffset.UTC ); + } + + public Instant with( TemporalAdjuster adjuster ) + { + return localDateTime().with( adjuster ).toInstant( ZoneOffset.UTC ); + } + + public Instant plus( long amountToAdd, TemporalUnit unit ) + { + return localDateTime().plus( amountToAdd, unit ).toInstant( ZoneOffset.UTC ); + } + + public Instant minus( long amountToSubtract, TemporalUnit unit ) + { + return localDateTime().minus( amountToSubtract, unit ).toInstant( ZoneOffset.UTC ); + } + }