You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tephra.apache.org by po...@apache.org on 2017/09/22 02:42:05 UTC

[6/6] incubator-tephra git commit: TEPHRA-245 Add TimeMathParser to parse relative time

TEPHRA-245 Add TimeMathParser to parse relative time

Signed-off-by: poorna <po...@apache.org>


Project: http://git-wip-us.apache.org/repos/asf/incubator-tephra/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-tephra/commit/4ade1438
Tree: http://git-wip-us.apache.org/repos/asf/incubator-tephra/tree/4ade1438
Diff: http://git-wip-us.apache.org/repos/asf/incubator-tephra/diff/4ade1438

Branch: refs/heads/master
Commit: 4ade1438399d9e932979fdefa273b037f73b570f
Parents: cd7bd2b
Author: poorna <po...@cask.co>
Authored: Wed Sep 13 17:08:29 2017 -0700
Committer: poorna <po...@apache.org>
Committed: Thu Sep 21 19:41:32 2017 -0700

----------------------------------------------------------------------
 .../org/apache/tephra/util/TimeMathParser.java  | 175 +++++++++++++++++++
 .../apache/tephra/util/TimeMathParserTest.java  | 117 +++++++++++++
 2 files changed, 292 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-tephra/blob/4ade1438/tephra-core/src/main/java/org/apache/tephra/util/TimeMathParser.java
----------------------------------------------------------------------
diff --git a/tephra-core/src/main/java/org/apache/tephra/util/TimeMathParser.java b/tephra-core/src/main/java/org/apache/tephra/util/TimeMathParser.java
new file mode 100644
index 0000000..f06e3c8
--- /dev/null
+++ b/tephra-core/src/main/java/org/apache/tephra/util/TimeMathParser.java
@@ -0,0 +1,175 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.tephra.util;
+
+import com.google.common.base.Preconditions;
+
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Utility class for parsing time strings into timestamps, with support for some basic time math.
+ * Math syntax includes addition and subtraction in minutes, hours, and days.
+ * The "now" keyword translates to the current epoch time in seconds, and can be strung together with
+ * various additions and subtractions.  For example, "now" will translate into the current epoch time in seconds.
+ * "now-5s" is 5 seconds before now, "now-1d" is one day before now, and "now-1d+4h" is 20 hours
+ * before now.
+ *
+ */
+public class TimeMathParser {
+
+  private static final String NOW = "now";
+  private static final String VALID_UNITS = "ms|s|m|h|d";
+  private static final Pattern OP_PATTERN = Pattern.compile("([-+])(\\d+)(" + VALID_UNITS + ")");
+  private static final Pattern RESOLUTION_PATTERN = Pattern.compile("(\\d+)(" + VALID_UNITS + ")");
+  private static final Pattern TIMESTAMP_PATTERN = Pattern.compile("^(\\d+)$");
+
+  private static long convertToMilliseconds(String op, long num, String unitStr) {
+    long milliseconds;
+    if ("ms".equals(unitStr)) {
+      milliseconds = num;
+    } else if ("s".equals(unitStr)) {
+      milliseconds = TimeUnit.SECONDS.toMillis(num);
+    } else if ("m".equals(unitStr)) {
+      milliseconds = TimeUnit.MINUTES.toMillis(num);
+    } else if ("h".equals(unitStr)) {
+      milliseconds = TimeUnit.HOURS.toMillis(num);
+    } else if ("d".equals(unitStr)) {
+      milliseconds = TimeUnit.DAYS.toMillis(num);
+    } else {
+      throw new IllegalArgumentException("invalid time unit " + unitStr +
+                                           ", should be one of 'ms', 's', 'm', 'h', 'd'");
+    }
+
+    if ("+".equals(op)) {
+      return milliseconds;
+    } else if ("-".equals(op)) {
+      return 0 - milliseconds;
+    } else {
+      throw new IllegalArgumentException("invalid operation " + op + ", should be either '+' or '-'");
+    }
+  }
+
+  private static long convertToMilliseconds(long num, String unitStr) {
+    return convertToMilliseconds("+", num, unitStr);
+  }
+
+  /**
+   * @return the current time in seconds
+   */
+  @SuppressWarnings({"unused", "WeakerAccess"})
+  public static long nowInSeconds() {
+    return TimeUnit.SECONDS.convert(System.currentTimeMillis(), TimeUnit.MILLISECONDS);
+  }
+
+  @SuppressWarnings("WeakerAccess")
+  public static int resolutionInSeconds(String resolutionStr) {
+    Matcher matcher = RESOLUTION_PATTERN.matcher(resolutionStr);
+    int output = 0;
+    while (matcher.find()) {
+      output += TimeUnit.MILLISECONDS.toSeconds(convertToMilliseconds(Long.parseLong(matcher.group(1)),
+                                                                      matcher.group(2)));
+    }
+    return output;
+  }
+
+  /**
+   * Parses a time in String format into a long value. If the input is numeric we assume the input is in seconds.
+   *
+   * @param timeStr the string to parse
+   * @return the parsed time in seconds
+   */
+  @SuppressWarnings("WeakerAccess")
+  public static long parseTimeInSeconds(String timeStr) {
+    return parseTime(timeStr, TimeUnit.SECONDS);
+  }
+
+  /**
+   * Parses a time in String format into a long value
+   *
+   * @param timeStr the string to parse
+   * @param timeUnit the unit of time to return, if timeStr is numeric then it is assumed to be in the unit timeUnit
+   * @return the parsed time
+   */
+  @SuppressWarnings("WeakerAccess")
+  public static long parseTime(String timeStr, TimeUnit timeUnit) {
+    return parseTime(System.currentTimeMillis(), timeStr, timeUnit);
+  }
+
+  /**
+   * Parses a time in String format into a long value. If the input is numeric we assume the input is in seconds.
+   *
+   * @param now the present time in seconds
+   * @param timeStr the string to parse
+   * @return the parsed time in seconds
+   */
+  @SuppressWarnings("WeakerAccess")
+  public static long parseTimeInSeconds(long now, String timeStr) {
+    return parseTime(TimeUnit.MILLISECONDS.convert(now, TimeUnit.SECONDS), timeStr, TimeUnit.SECONDS);
+  }
+
+  /**
+   * Parses a time in String format into a long value
+   *
+   * @param now the present time in milliseconds
+   * @param timeStr the string to parse
+   * @param timeUnit the unit of time to return, if timeStr is numeric then it is assumed to be in the unit timeUnit
+   * @return the parsed time
+   * @throws IllegalArgumentException if the format of timeStr is bad
+   */
+  @SuppressWarnings("WeakerAccess")
+  public static long parseTime(long now, String timeStr, TimeUnit timeUnit) {
+    Preconditions.checkNotNull(timeStr);
+
+    if (NOW.equals(timeStr.toUpperCase())) {
+      return now;
+    }
+    // if its a numeric timestamp, assume units are correct
+    Matcher matcher = TIMESTAMP_PATTERN.matcher(timeStr);
+    if (matcher.matches()) {
+      return Long.parseLong(timeStr);
+    }
+
+    // if its some time math pattern like now-1d-6h
+    long output = now;
+    if (timeStr.startsWith(NOW)) {
+      matcher = OP_PATTERN.matcher(timeStr);
+      // start at 3 to take into account the NOW at the start of the string
+      int prevEndPos = 3;
+      while (matcher.find()) {
+        // happens if there are unexpected things in-between, like "now 2h-1m"
+        if (matcher.start() != prevEndPos) {
+          throw new IllegalArgumentException("invalid time format " + timeStr);
+        }
+        // group 1 should be '+' or '-', group 2 is the number of units, and group 3 is the unit.  ex: 6h
+        output += convertToMilliseconds(matcher.group(1), Long.parseLong(matcher.group(2)), matcher.group(3));
+        prevEndPos = matcher.end();
+      }
+      // happens if the end of the string is invalid, like "now-6h 30m"
+      if (prevEndPos != timeStr.length()) {
+        throw new IllegalArgumentException("invalid time format " + timeStr);
+      }
+    } else {
+      throw new IllegalArgumentException("invalid time format " + timeStr);
+    }
+    return timeUnit.convert(output, TimeUnit.MILLISECONDS);
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-tephra/blob/4ade1438/tephra-core/src/test/java/org/apache/tephra/util/TimeMathParserTest.java
----------------------------------------------------------------------
diff --git a/tephra-core/src/test/java/org/apache/tephra/util/TimeMathParserTest.java b/tephra-core/src/test/java/org/apache/tephra/util/TimeMathParserTest.java
new file mode 100644
index 0000000..553e030
--- /dev/null
+++ b/tephra-core/src/test/java/org/apache/tephra/util/TimeMathParserTest.java
@@ -0,0 +1,117 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.tephra.util;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Test {@link TimeMathParser} class.
+ */
+public class TimeMathParserTest {
+
+  @Test
+  public void testGetNowInSeconds() {
+    long now = TimeUnit.SECONDS.convert(System.currentTimeMillis(), TimeUnit.MILLISECONDS);
+    // in case we're on the border between seconds
+    long result = TimeMathParser.nowInSeconds();
+    Assert.assertTrue((result - now == 1) || result == now);
+  }
+
+  @Test
+  public void testParseTimestamp() {
+    Assert.assertEquals(1234567890, TimeMathParser.parseTimeInSeconds("1234567890"));
+    Assert.assertEquals(1234567890, TimeMathParser.parseTimeInSeconds(0, "1234567890"));
+  }
+
+  @Test
+  public void testParseNow() {
+    long now = TimeMathParser.nowInSeconds();
+    long result = TimeMathParser.parseTimeInSeconds("now");
+    // in case we're on the border between seconds
+    Assert.assertTrue((result - now) == 1 || (result == now));
+    Assert.assertEquals(1234567890, TimeMathParser.parseTimeInSeconds(1234567890, "now"));
+  }
+
+  @Test
+  public void testOneOperation() {
+    long now = TimeMathParser.nowInSeconds();
+    Assert.assertEquals(now - 7, TimeMathParser.parseTimeInSeconds(now, "now-7s"));
+    Assert.assertEquals(now - 7 * 60, TimeMathParser.parseTimeInSeconds(now, "now-7m"));
+    Assert.assertEquals(now - 7 * 3600, TimeMathParser.parseTimeInSeconds(now, "now-7h"));
+    Assert.assertEquals(now - 7 * 86400, TimeMathParser.parseTimeInSeconds(now, "now-7d"));
+    Assert.assertEquals(now + 7, TimeMathParser.parseTimeInSeconds(now, "now+7s"));
+    Assert.assertEquals(now + 7 * 60, TimeMathParser.parseTimeInSeconds(now, "now+7m"));
+    Assert.assertEquals(now + 7 * 3600, TimeMathParser.parseTimeInSeconds(now, "now+7h"));
+    Assert.assertEquals(now + 7 * 86400, TimeMathParser.parseTimeInSeconds(now, "now+7d"));
+    Assert.assertEquals(System.currentTimeMillis() - 10, TimeMathParser.parseTime("now-10ms",
+                                                                                  TimeUnit.MILLISECONDS), 1);
+    Assert.assertEquals(System.currentTimeMillis() + 50, TimeMathParser.parseTime("now+50ms",
+                                                                                  TimeUnit.MILLISECONDS), 1);
+  }
+
+  @Test
+  public void testResolutionParsing() {
+    String resolution = "60s";
+    Assert.assertEquals(60, TimeMathParser.resolutionInSeconds(resolution));
+    resolution = "2m";
+    Assert.assertEquals(120, TimeMathParser.resolutionInSeconds(resolution));
+    resolution = "3h";
+    Assert.assertEquals(10800, TimeMathParser.resolutionInSeconds(resolution));
+    resolution = "1d";
+    Assert.assertEquals(86400, TimeMathParser.resolutionInSeconds(resolution));
+    resolution = "1h3m";
+    Assert.assertEquals(3780, TimeMathParser.resolutionInSeconds(resolution));
+  }
+
+  @Test
+  public void testMultipleOperations() {
+    long now = TimeMathParser.nowInSeconds();
+    Assert.assertEquals(now - 7 * 86400 + 3 * 3600 - 13 * 60 + 11,
+                        TimeMathParser.parseTimeInSeconds(now, "now-7d+3h-13m+11s"));
+  }
+
+  // happens if input is supposed to be url encoded but is not
+  @Test(expected = IllegalArgumentException.class)
+  public void testSpaceInsteadOfPlusThrowsException() {
+    long now = TimeMathParser.nowInSeconds();
+    TimeMathParser.parseTimeInSeconds(now, "now 6h");
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testGibberishInMiddleThrowsException() {
+    long now = TimeMathParser.nowInSeconds();
+    TimeMathParser.parseTimeInSeconds(now, "now-3d+23lnkfasd-6h");
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testInvalidOperationThrowsException() {
+    long now = TimeMathParser.nowInSeconds();
+    TimeMathParser.parseTimeInSeconds(now, "now/1d");
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testInvalidUnitThrowsException() {
+    long now = TimeMathParser.nowInSeconds();
+    TimeMathParser.parseTimeInSeconds(now, "now-1w");
+  }
+}