This is an automated email from the ASF dual-hosted git repository.

xuba pushed a commit to branch v0.7.x-test-front
in repository https://gitbox.apache.org/repos/asf/amoro.git

commit 4620af8fed0866985e0d66402d9dd3f67cfd32da
Author: Xavier Bai <[email protected]>
AuthorDate: Wed Mar 20 10:22:10 2024 +0800

    [WAP] Enhance TimeUtils and fix config parsing error
---
 .../arctic/api/config/DataExpirationConfig.java    | 166 ++++++++--
 .../maintainer/IcebergTableMaintainer.java         |   4 +-
 .../maintainer/MixedTableMaintainer.java           |   4 +-
 .../arctic/server/table/DataExpirationConfig.java  | 101 +++---
 .../arctic/server/utils/ConfigurationUtil.java     | 222 +------------
 .../java/com/netease/arctic/utils/TimeUtils.java   | 342 +++++++++++++++++++++
 6 files changed, 543 insertions(+), 296 deletions(-)

diff --git 
a/ams/api/src/main/java/com/netease/arctic/api/config/DataExpirationConfig.java 
b/ams/api/src/main/java/com/netease/arctic/api/config/DataExpirationConfig.java
index aff7ba623..f4536084b 100644
--- 
a/ams/api/src/main/java/com/netease/arctic/api/config/DataExpirationConfig.java
+++ 
b/ams/api/src/main/java/com/netease/arctic/api/config/DataExpirationConfig.java
@@ -19,17 +19,23 @@
 package com.netease.arctic.api.config;
 
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Objects;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Sets;
+import com.netease.arctic.table.ArcticTable;
+import com.netease.arctic.table.TableProperties;
+import com.netease.arctic.utils.CompatiblePropertyUtil;
+import com.netease.arctic.utils.TimeUtils;
 import org.apache.commons.lang3.StringUtils;
-import 
org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
-import org.apache.iceberg.relocated.com.google.common.base.Objects;
-import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
-import org.apache.iceberg.relocated.com.google.common.collect.Sets;
 import org.apache.iceberg.types.Type;
 import org.apache.iceberg.types.Types;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.util.Locale;
+import java.util.Map;
 import java.util.Set;
 
 /** Data expiration configuration. */
@@ -40,23 +46,29 @@ public class DataExpirationConfig {
   // data-expire.field
   private String expirationField;
   // data-expire.level
+  @JsonProperty(defaultValue = TableProperties.DATA_EXPIRATION_LEVEL_DEFAULT)
   private ExpireLevel expirationLevel;
   // data-expire.retention-time
   private long retentionTime;
   // data-expire.datetime-string-pattern
+  @JsonProperty(defaultValue = 
TableProperties.DATA_EXPIRATION_DATE_STRING_PATTERN_DEFAULT)
   private String dateTimePattern;
   // data-expire.datetime-number-format
+  @JsonProperty(defaultValue = 
TableProperties.DATA_EXPIRATION_DATE_NUMBER_FORMAT_DEFAULT)
   private String numberDateFormat;
-  // data-expire.since
-  private Since since;
+  // data-expire.base-on-rule
+  @JsonProperty(defaultValue = 
TableProperties.DATA_EXPIRATION_BASE_ON_RULE_DEFAULT)
+  private BaseOnRule baseOnRule;
+  // Retention time must be positive
+  public static final long INVALID_RETENTION_TIME = 0L;
 
-  @VisibleForTesting
+  @com.google.common.annotations.VisibleForTesting
   public enum ExpireLevel {
     PARTITION,
     FILE;
 
     public static ExpireLevel fromString(String level) {
-      Preconditions.checkArgument(null != level, "Invalid level type: null");
+      com.google.common.base.Preconditions.checkArgument(null != level, 
"Invalid level type: null");
       try {
         return ExpireLevel.valueOf(level.toUpperCase(Locale.ENGLISH));
       } catch (IllegalArgumentException e) {
@@ -66,14 +78,15 @@ public class DataExpirationConfig {
   }
 
   @VisibleForTesting
-  public enum Since {
-    LATEST_SNAPSHOT,
-    CURRENT_TIMESTAMP;
+  public enum BaseOnRule {
+    LAST_COMMIT_TIME,
+    CURRENT_TIME;
 
-    public static Since fromString(String since) {
-      Preconditions.checkArgument(null != since, "data-expire.since is 
invalid: null");
+    public static BaseOnRule fromString(String since) {
+      com.google.common.base.Preconditions.checkArgument(
+          null != since, TableProperties.DATA_EXPIRATION_BASE_ON_RULE + " is 
invalid: null");
       try {
-        return Since.valueOf(since.toUpperCase(Locale.ENGLISH));
+        return BaseOnRule.valueOf(since.toUpperCase(Locale.ENGLISH));
       } catch (IllegalArgumentException e) {
         throw new IllegalArgumentException(
             String.format("Unable to expire data since: %s", since), e);
@@ -95,14 +108,115 @@ public class DataExpirationConfig {
       long retentionTime,
       String dateTimePattern,
       String numberDateFormat,
-      Since since) {
+      BaseOnRule baseOnRule) {
     this.enabled = enabled;
     this.expirationField = expirationField;
     this.expirationLevel = expirationLevel;
     this.retentionTime = retentionTime;
     this.dateTimePattern = dateTimePattern;
     this.numberDateFormat = numberDateFormat;
-    this.since = since;
+    this.baseOnRule = baseOnRule;
+  }
+
+  public DataExpirationConfig(ArcticTable table) {
+    Map<String, String> properties = table.properties();
+    expirationField =
+        CompatiblePropertyUtil.propertyAsString(
+            properties, TableProperties.DATA_EXPIRATION_FIELD, null);
+    Types.NestedField field = table.schema().findField(expirationField);
+    com.google.common.base.Preconditions.checkArgument(
+        StringUtils.isNoneBlank(expirationField) && null != field,
+        String.format(
+            "Field(%s) used to determine data expiration is illegal for 
table(%s)",
+            expirationField, table.name()));
+    Type.TypeID typeID = field.type().typeId();
+    Preconditions.checkArgument(
+        FIELD_TYPES.contains(typeID),
+        String.format(
+            "The type(%s) of filed(%s) is incompatible for table(%s)",
+            typeID.name(), expirationField, table.name()));
+
+    expirationLevel =
+        ExpireLevel.fromString(
+            CompatiblePropertyUtil.propertyAsString(
+                properties,
+                TableProperties.DATA_EXPIRATION_LEVEL,
+                TableProperties.DATA_EXPIRATION_LEVEL_DEFAULT));
+    retentionTime =
+        parseRetentionToMillis(
+            CompatiblePropertyUtil.propertyAsString(
+                properties, TableProperties.DATA_EXPIRATION_RETENTION_TIME, 
null));
+    dateTimePattern =
+        CompatiblePropertyUtil.propertyAsString(
+            properties,
+            TableProperties.DATA_EXPIRATION_DATE_STRING_PATTERN,
+            TableProperties.DATA_EXPIRATION_DATE_STRING_PATTERN_DEFAULT);
+    numberDateFormat =
+        CompatiblePropertyUtil.propertyAsString(
+            properties,
+            TableProperties.DATA_EXPIRATION_DATE_NUMBER_FORMAT,
+            TableProperties.DATA_EXPIRATION_DATE_NUMBER_FORMAT_DEFAULT);
+    baseOnRule =
+        BaseOnRule.fromString(
+            CompatiblePropertyUtil.propertyAsString(
+                properties,
+                TableProperties.DATA_EXPIRATION_BASE_ON_RULE,
+                TableProperties.DATA_EXPIRATION_BASE_ON_RULE_DEFAULT));
+  }
+
+  public static DataExpirationConfig parse(Map<String, String> properties) {
+    boolean gcEnabled =
+        CompatiblePropertyUtil.propertyAsBoolean(
+            properties, org.apache.iceberg.TableProperties.GC_ENABLED, true);
+
+    return new DataExpirationConfig()
+        .setEnabled(
+            gcEnabled
+                && CompatiblePropertyUtil.propertyAsBoolean(
+                    properties,
+                    TableProperties.ENABLE_DATA_EXPIRATION,
+                    TableProperties.ENABLE_DATA_EXPIRATION_DEFAULT))
+        .setExpirationLevel(
+            ExpireLevel.fromString(
+                CompatiblePropertyUtil.propertyAsString(
+                    properties,
+                    TableProperties.DATA_EXPIRATION_LEVEL,
+                    TableProperties.DATA_EXPIRATION_LEVEL_DEFAULT)))
+        .setExpirationField(
+            CompatiblePropertyUtil.propertyAsString(
+                properties, TableProperties.DATA_EXPIRATION_FIELD, null))
+        .setDateTimePattern(
+            CompatiblePropertyUtil.propertyAsString(
+                properties,
+                TableProperties.DATA_EXPIRATION_DATE_STRING_PATTERN,
+                TableProperties.DATA_EXPIRATION_DATE_STRING_PATTERN_DEFAULT))
+        .setNumberDateFormat(
+            CompatiblePropertyUtil.propertyAsString(
+                properties,
+                TableProperties.DATA_EXPIRATION_DATE_NUMBER_FORMAT,
+                TableProperties.DATA_EXPIRATION_DATE_NUMBER_FORMAT_DEFAULT))
+        .setBaseOnRule(
+            BaseOnRule.fromString(
+                CompatiblePropertyUtil.propertyAsString(
+                    properties,
+                    TableProperties.DATA_EXPIRATION_BASE_ON_RULE,
+                    TableProperties.DATA_EXPIRATION_BASE_ON_RULE_DEFAULT)))
+        .setRetentionTime(
+            parseRetentionToMillis(
+                CompatiblePropertyUtil.propertyAsString(
+                    properties, 
TableProperties.DATA_EXPIRATION_RETENTION_TIME, null)));
+  }
+
+  private static long parseRetentionToMillis(String retention) {
+    try {
+      return TimeUtils.parseTime(retention).toMillis();
+    } catch (Exception e) {
+      return INVALID_RETENTION_TIME;
+    }
+  }
+
+  public boolean isPositive() {
+    return retentionTime > INVALID_RETENTION_TIME;
   }
 
   public boolean isEnabled() {
@@ -159,12 +273,12 @@ public class DataExpirationConfig {
     return this;
   }
 
-  public Since getSince() {
-    return since;
+  public BaseOnRule getBaseOnRule() {
+    return baseOnRule;
   }
 
-  public DataExpirationConfig setSince(Since since) {
-    this.since = since;
+  public DataExpirationConfig setBaseOnRule(BaseOnRule baseOnRule) {
+    this.baseOnRule = baseOnRule;
     return this;
   }
 
@@ -179,11 +293,11 @@ public class DataExpirationConfig {
     DataExpirationConfig config = (DataExpirationConfig) o;
     return enabled == config.enabled
         && retentionTime == config.retentionTime
-        && Objects.equal(expirationField, config.expirationField)
+        && com.google.common.base.Objects.equal(expirationField, 
config.expirationField)
         && expirationLevel == config.expirationLevel
-        && Objects.equal(dateTimePattern, config.dateTimePattern)
-        && Objects.equal(numberDateFormat, config.numberDateFormat)
-        && since == config.since;
+        && com.google.common.base.Objects.equal(dateTimePattern, 
config.dateTimePattern)
+        && com.google.common.base.Objects.equal(numberDateFormat, 
config.numberDateFormat)
+        && baseOnRule == config.baseOnRule;
   }
 
   @Override
@@ -195,12 +309,12 @@ public class DataExpirationConfig {
         retentionTime,
         dateTimePattern,
         numberDateFormat,
-        since);
+        baseOnRule);
   }
 
   public boolean isValid(Types.NestedField field, String name) {
     return isEnabled()
-        && getRetentionTime() > 0
+        && isPositive()
         && validateExpirationField(field, name, getExpirationField());
   }
 
diff --git 
a/ams/server/src/main/java/com/netease/arctic/server/optimizing/maintainer/IcebergTableMaintainer.java
 
b/ams/server/src/main/java/com/netease/arctic/server/optimizing/maintainer/IcebergTableMaintainer.java
index a16e6a5b0..d4c04f917 100644
--- 
a/ams/server/src/main/java/com/netease/arctic/server/optimizing/maintainer/IcebergTableMaintainer.java
+++ 
b/ams/server/src/main/java/com/netease/arctic/server/optimizing/maintainer/IcebergTableMaintainer.java
@@ -265,8 +265,8 @@ public class IcebergTableMaintainer implements 
TableMaintainer {
    *     zone
    */
   @VisibleForTesting
-  public void expireDataFrom(DataExpirationConfig expirationConfig, Instant 
instant) {
-    if (instant.equals(Instant.MIN)) {
+  protected void expireDataFrom(DataExpirationConfig expirationConfig, Instant 
instant) {
+    if (instant.equals(Instant.MIN) || !expirationConfig.isPositive()) {
       return;
     }
 
diff --git 
a/ams/server/src/main/java/com/netease/arctic/server/optimizing/maintainer/MixedTableMaintainer.java
 
b/ams/server/src/main/java/com/netease/arctic/server/optimizing/maintainer/MixedTableMaintainer.java
index e97e1a422..0251d53b2 100644
--- 
a/ams/server/src/main/java/com/netease/arctic/server/optimizing/maintainer/MixedTableMaintainer.java
+++ 
b/ams/server/src/main/java/com/netease/arctic/server/optimizing/maintainer/MixedTableMaintainer.java
@@ -174,8 +174,8 @@ public class MixedTableMaintainer implements 
TableMaintainer {
   }
 
   @VisibleForTesting
-  public void expireDataFrom(DataExpirationConfig expirationConfig, Instant 
instant) {
-    if (instant.equals(Instant.MIN)) {
+  protected void expireDataFrom(DataExpirationConfig expirationConfig, Instant 
instant) {
+    if (instant.equals(Instant.MIN) || !expirationConfig.isPositive()) {
       return;
     }
 
diff --git 
a/ams/server/src/main/java/com/netease/arctic/server/table/DataExpirationConfig.java
 
b/ams/server/src/main/java/com/netease/arctic/server/table/DataExpirationConfig.java
index e53b2d45e..f750368d8 100644
--- 
a/ams/server/src/main/java/com/netease/arctic/server/table/DataExpirationConfig.java
+++ 
b/ams/server/src/main/java/com/netease/arctic/server/table/DataExpirationConfig.java
@@ -24,10 +24,10 @@ import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Objects;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.Sets;
-import com.netease.arctic.server.utils.ConfigurationUtil;
 import com.netease.arctic.table.ArcticTable;
 import com.netease.arctic.table.TableProperties;
 import com.netease.arctic.utils.CompatiblePropertyUtil;
+import com.netease.arctic.utils.TimeUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.iceberg.types.Type;
 import org.apache.iceberg.types.Types;
@@ -38,7 +38,10 @@ import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 
-/** TODO Use DataExpirationConfig class in API module, this class shall be 
removed after 0.7.0 */
+/**
+ * TODO Use {@link com.netease.arctic.api.config.DataExpirationConfig} class 
in API module, this
+ * class shall be removed after 0.7.0
+ */
 @Deprecated
 @JsonIgnoreProperties(ignoreUnknown = true)
 public class DataExpirationConfig {
@@ -60,6 +63,8 @@ public class DataExpirationConfig {
   // data-expire.base-on-rule
   @JsonProperty(defaultValue = 
TableProperties.DATA_EXPIRATION_BASE_ON_RULE_DEFAULT)
   private BaseOnRule baseOnRule;
+  // Retention time must be positive
+  public static final long INVALID_RETENTION_TIME = 0L;
 
   @VisibleForTesting
   public enum ExpireLevel {
@@ -141,14 +146,10 @@ public class DataExpirationConfig {
                 properties,
                 TableProperties.DATA_EXPIRATION_LEVEL,
                 TableProperties.DATA_EXPIRATION_LEVEL_DEFAULT));
-
-    String retention =
-        CompatiblePropertyUtil.propertyAsString(
-            properties, TableProperties.DATA_EXPIRATION_RETENTION_TIME, null);
-    if (StringUtils.isNotBlank(retention)) {
-      retentionTime = 
ConfigurationUtil.TimeUtils.parseDuration(retention).toMillis();
-    }
-
+    retentionTime =
+        parseRetentionToMillis(
+            CompatiblePropertyUtil.propertyAsString(
+                properties, TableProperties.DATA_EXPIRATION_RETENTION_TIME, 
null));
     dateTimePattern =
         CompatiblePropertyUtil.propertyAsString(
             properties,
@@ -171,47 +172,55 @@ public class DataExpirationConfig {
     boolean gcEnabled =
         CompatiblePropertyUtil.propertyAsBoolean(
             properties, org.apache.iceberg.TableProperties.GC_ENABLED, true);
-    DataExpirationConfig config =
-        new DataExpirationConfig()
-            .setEnabled(
-                gcEnabled
-                    && CompatiblePropertyUtil.propertyAsBoolean(
-                        properties,
-                        TableProperties.ENABLE_DATA_EXPIRATION,
-                        TableProperties.ENABLE_DATA_EXPIRATION_DEFAULT))
-            .setExpirationLevel(
-                ExpireLevel.fromString(
-                    CompatiblePropertyUtil.propertyAsString(
-                        properties,
-                        TableProperties.DATA_EXPIRATION_LEVEL,
-                        TableProperties.DATA_EXPIRATION_LEVEL_DEFAULT)))
-            .setExpirationField(
-                CompatiblePropertyUtil.propertyAsString(
-                    properties, TableProperties.DATA_EXPIRATION_FIELD, null))
-            .setDateTimePattern(
+
+    return new DataExpirationConfig()
+        .setEnabled(
+            gcEnabled
+                && CompatiblePropertyUtil.propertyAsBoolean(
+                    properties,
+                    TableProperties.ENABLE_DATA_EXPIRATION,
+                    TableProperties.ENABLE_DATA_EXPIRATION_DEFAULT))
+        .setExpirationLevel(
+            ExpireLevel.fromString(
                 CompatiblePropertyUtil.propertyAsString(
                     properties,
-                    TableProperties.DATA_EXPIRATION_DATE_STRING_PATTERN,
-                    
TableProperties.DATA_EXPIRATION_DATE_STRING_PATTERN_DEFAULT))
-            .setNumberDateFormat(
+                    TableProperties.DATA_EXPIRATION_LEVEL,
+                    TableProperties.DATA_EXPIRATION_LEVEL_DEFAULT)))
+        .setExpirationField(
+            CompatiblePropertyUtil.propertyAsString(
+                properties, TableProperties.DATA_EXPIRATION_FIELD, null))
+        .setDateTimePattern(
+            CompatiblePropertyUtil.propertyAsString(
+                properties,
+                TableProperties.DATA_EXPIRATION_DATE_STRING_PATTERN,
+                TableProperties.DATA_EXPIRATION_DATE_STRING_PATTERN_DEFAULT))
+        .setNumberDateFormat(
+            CompatiblePropertyUtil.propertyAsString(
+                properties,
+                TableProperties.DATA_EXPIRATION_DATE_NUMBER_FORMAT,
+                TableProperties.DATA_EXPIRATION_DATE_NUMBER_FORMAT_DEFAULT))
+        .setBaseOnRule(
+            BaseOnRule.fromString(
                 CompatiblePropertyUtil.propertyAsString(
                     properties,
-                    TableProperties.DATA_EXPIRATION_DATE_NUMBER_FORMAT,
-                    
TableProperties.DATA_EXPIRATION_DATE_NUMBER_FORMAT_DEFAULT))
-            .setBaseOnRule(
-                BaseOnRule.fromString(
-                    CompatiblePropertyUtil.propertyAsString(
-                        properties,
-                        TableProperties.DATA_EXPIRATION_BASE_ON_RULE,
-                        
TableProperties.DATA_EXPIRATION_BASE_ON_RULE_DEFAULT)));
-    String retention =
-        CompatiblePropertyUtil.propertyAsString(
-            properties, TableProperties.DATA_EXPIRATION_RETENTION_TIME, null);
-    if (StringUtils.isNotBlank(retention)) {
-      
config.setRetentionTime(ConfigurationUtil.TimeUtils.parseDuration(retention).toMillis());
+                    TableProperties.DATA_EXPIRATION_BASE_ON_RULE,
+                    TableProperties.DATA_EXPIRATION_BASE_ON_RULE_DEFAULT)))
+        .setRetentionTime(
+            parseRetentionToMillis(
+                CompatiblePropertyUtil.propertyAsString(
+                    properties, 
TableProperties.DATA_EXPIRATION_RETENTION_TIME, null)));
+  }
+
+  private static long parseRetentionToMillis(String retention) {
+    try {
+      return TimeUtils.parseTime(retention).toMillis();
+    } catch (Exception e) {
+      return INVALID_RETENTION_TIME;
     }
+  }
 
-    return config;
+  public boolean isPositive() {
+    return retentionTime > INVALID_RETENTION_TIME;
   }
 
   public boolean isEnabled() {
@@ -309,7 +318,7 @@ public class DataExpirationConfig {
 
   public boolean isValid(Types.NestedField field, String name) {
     return isEnabled()
-        && getRetentionTime() > 0
+        && isPositive()
         && validateExpirationField(field, name, getExpirationField());
   }
 
diff --git 
a/ams/server/src/main/java/com/netease/arctic/server/utils/ConfigurationUtil.java
 
b/ams/server/src/main/java/com/netease/arctic/server/utils/ConfigurationUtil.java
index 86f22f05d..def707f4d 100755
--- 
a/ams/server/src/main/java/com/netease/arctic/server/utils/ConfigurationUtil.java
+++ 
b/ams/server/src/main/java/com/netease/arctic/server/utils/ConfigurationUtil.java
@@ -19,23 +19,20 @@
 package com.netease.arctic.server.utils;
 
 import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.netease.arctic.utils.TimeUtils;
 
 import javax.annotation.Nonnull;
 
 import java.io.File;
 import java.time.Duration;
-import java.time.temporal.ChronoUnit;
 import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Properties;
 import java.util.Set;
 import java.util.stream.Collectors;
-import java.util.stream.IntStream;
 
 /** Utility class for {@link Configurations} related helper functions. */
 public class ConfigurationUtil {
@@ -292,219 +289,4 @@ public class ConfigurationUtil {
 
     return Double.parseDouble(o.toString());
   }
-
-  /** Collection of utilities about time intervals. */
-  public static class TimeUtils {
-
-    private static final Map<String, ChronoUnit> LABEL_TO_UNIT_MAP =
-        Collections.unmodifiableMap(initMap());
-
-    /**
-     * Parse the given string to a java {@link Duration}. The string is in 
format "{length
-     * value}{time unit label}", e.g. "123ms", "321 s". If no time unit label 
is specified, it will
-     * be considered as milliseconds.
-     *
-     * <p>Supported time unit labels are:
-     *
-     * <ul>
-     *   <li>DAYS: "d", "day"
-     *   <li>HOURS: "h", "hour"
-     *   <li>MINUTES: "min", "minute"
-     *   <li>SECONDS: "s", "sec", "second"
-     *   <li>MILLISECONDS: "ms", "milli", "millisecond"
-     *   <li>MICROSECONDS: "µs", "micro", "microsecond"
-     *   <li>NANOSECONDS: "ns", "nano", "nanosecond"
-     * </ul>
-     *
-     * @param text string to parse.
-     */
-    public static Duration parseDuration(String text) {
-      checkNotNull(text);
-
-      final String trimmed = text.trim();
-      checkArgument(!trimmed.isEmpty(), "argument is an empty- or 
whitespace-only string");
-
-      final int len = trimmed.length();
-      int pos = 0;
-
-      char current;
-      while (pos < len && (current = trimmed.charAt(pos)) >= '0' && current <= 
'9') {
-        pos++;
-      }
-
-      final String number = trimmed.substring(0, pos);
-      final String unitLabel = 
trimmed.substring(pos).trim().toLowerCase(Locale.US);
-
-      if (number.isEmpty()) {
-        throw new NumberFormatException("text does not start with a number");
-      }
-
-      final long value;
-      try {
-        value = Long.parseLong(number); // this throws a NumberFormatException 
on overflow
-      } catch (NumberFormatException e) {
-        throw new IllegalArgumentException(
-            "The value '"
-                + number
-                + "' cannot be re represented as 64bit number (numeric 
overflow).");
-      }
-
-      if (unitLabel.isEmpty()) {
-        return Duration.of(value, ChronoUnit.MILLIS);
-      }
-
-      ChronoUnit unit = LABEL_TO_UNIT_MAP.get(unitLabel);
-      if (unit != null) {
-        return Duration.of(value, unit);
-      } else {
-        throw new IllegalArgumentException(
-            "Time interval unit label '"
-                + unitLabel
-                + "' does not match any of the recognized units: "
-                + TimeUnit.getAllUnits());
-      }
-    }
-
-    private static Map<String, ChronoUnit> initMap() {
-      Map<String, ChronoUnit> labelToUnit = new HashMap<>();
-      for (TimeUnit timeUnit : TimeUnit.values()) {
-        for (String label : timeUnit.getLabels()) {
-          labelToUnit.put(label, timeUnit.getUnit());
-        }
-      }
-      return labelToUnit;
-    }
-
-    /**
-     * @param duration to convert to string
-     * @return duration string in millis
-     */
-    public static String getStringInMillis(final Duration duration) {
-      return duration.toMillis() + TimeUnit.MILLISECONDS.labels.get(0);
-    }
-
-    /**
-     * Pretty prints the duration as a lowest granularity unit that does not 
lose precision.
-     *
-     * <p>Examples:
-     *
-     * <pre>{@code
-     * Duration.ofMilliseconds(60000) will be printed as 1 min
-     * Duration.ofHours(1).plusSeconds(1) will be printed as 3601 s
-     * }</pre>
-     *
-     * <b>NOTE:</b> It supports only durations that fit into long.
-     */
-    public static String formatWithHighestUnit(Duration duration) {
-      long nanos = duration.toNanos();
-
-      List<TimeUnit> orderedUnits =
-          Arrays.asList(
-              TimeUnit.NANOSECONDS,
-              TimeUnit.MICROSECONDS,
-              TimeUnit.MILLISECONDS,
-              TimeUnit.SECONDS,
-              TimeUnit.MINUTES,
-              TimeUnit.HOURS,
-              TimeUnit.DAYS);
-
-      TimeUnit highestIntegerUnit =
-          IntStream.range(0, orderedUnits.size())
-              .sequential()
-              .filter(idx -> nanos % 
orderedUnits.get(idx).unit.getDuration().toNanos() != 0)
-              .boxed()
-              .findFirst()
-              .map(
-                  idx -> {
-                    if (idx == 0) {
-                      return orderedUnits.get(0);
-                    } else {
-                      return orderedUnits.get(idx - 1);
-                    }
-                  })
-              .orElse(TimeUnit.MILLISECONDS);
-
-      return String.format(
-          "%d %s",
-          nanos / highestIntegerUnit.unit.getDuration().toNanos(),
-          highestIntegerUnit.getLabels().get(0));
-    }
-
-    private static ChronoUnit toChronoUnit(java.util.concurrent.TimeUnit 
timeUnit) {
-      switch (timeUnit) {
-        case NANOSECONDS:
-          return ChronoUnit.NANOS;
-        case MICROSECONDS:
-          return ChronoUnit.MICROS;
-        case MILLISECONDS:
-          return ChronoUnit.MILLIS;
-        case SECONDS:
-          return ChronoUnit.SECONDS;
-        case MINUTES:
-          return ChronoUnit.MINUTES;
-        case HOURS:
-          return ChronoUnit.HOURS;
-        case DAYS:
-          return ChronoUnit.DAYS;
-        default:
-          throw new IllegalArgumentException(String.format("Unsupported time 
unit %s.", timeUnit));
-      }
-    }
-
-    /** Enum which defines time unit, mostly used to parse value from 
configuration file. */
-    private enum TimeUnit {
-      DAYS(ChronoUnit.DAYS, singular("d"), plural("day")),
-      HOURS(ChronoUnit.HOURS, singular("h"), plural("hour")),
-      MINUTES(ChronoUnit.MINUTES, singular("min"), plural("minute")),
-      SECONDS(ChronoUnit.SECONDS, singular("s"), plural("sec"), 
plural("second")),
-      MILLISECONDS(ChronoUnit.MILLIS, singular("ms"), plural("milli"), 
plural("millisecond")),
-      MICROSECONDS(ChronoUnit.MICROS, singular("µs"), plural("micro"), 
plural("microsecond")),
-      NANOSECONDS(ChronoUnit.NANOS, singular("ns"), plural("nano"), 
plural("nanosecond"));
-
-      private static final String PLURAL_SUFFIX = "s";
-
-      private final List<String> labels;
-
-      private final ChronoUnit unit;
-
-      TimeUnit(ChronoUnit unit, String[]... labels) {
-        this.unit = unit;
-        this.labels = 
Arrays.stream(labels).flatMap(Arrays::stream).collect(Collectors.toList());
-      }
-
-      /**
-       * @param label the original label
-       * @return the singular format of the original label
-       */
-      private static String[] singular(String label) {
-        return new String[] {label};
-      }
-
-      /**
-       * @param label the original label
-       * @return both the singular format and plural format of the original 
label
-       */
-      private static String[] plural(String label) {
-        return new String[] {label, label + PLURAL_SUFFIX};
-      }
-
-      public static String getAllUnits() {
-        return Arrays.stream(TimeUnit.values())
-            .map(TimeUnit::createTimeUnitString)
-            .collect(Collectors.joining(", "));
-      }
-
-      private static String createTimeUnitString(TimeUnit timeUnit) {
-        return timeUnit.name() + ": (" + String.join(" | ", 
timeUnit.getLabels()) + ")";
-      }
-
-      public List<String> getLabels() {
-        return labels;
-      }
-
-      public ChronoUnit getUnit() {
-        return unit;
-      }
-    }
-  }
 }
diff --git a/core/src/main/java/com/netease/arctic/utils/TimeUtils.java 
b/core/src/main/java/com/netease/arctic/utils/TimeUtils.java
new file mode 100644
index 000000000..089288b3f
--- /dev/null
+++ b/core/src/main/java/com/netease/arctic/utils/TimeUtils.java
@@ -0,0 +1,342 @@
+/*
+ * 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 com.netease.arctic.utils;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.base.Objects;
+import org.apache.commons.lang3.StringUtils;
+
+import java.time.Duration;
+import java.time.Period;
+import java.time.temporal.ChronoUnit;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+/** Collection of utilities about time intervals. */
+public class TimeUtils {
+
+  private static Map<String, ChronoUnit> initMap() {
+    Map<String, ChronoUnit> labelToUnit = new HashMap<>();
+    for (TimeUnit timeUnit : TimeUnit.values()) {
+      for (String label : timeUnit.getLabels()) {
+        labelToUnit.put(label, timeUnit.getUnit());
+      }
+    }
+    return labelToUnit;
+  }
+
+  private static final Map<String, ChronoUnit> LABEL_TO_UNIT_MAP =
+      Collections.unmodifiableMap(initMap());
+
+  public static class Time {
+    String number;
+    ChronoUnit unit;
+
+    public Time(String number, String unitLabel) {
+      if (StringUtils.isBlank(number)) {
+        throw new IllegalArgumentException("Number text cannot be empty");
+      }
+
+      if (!LABEL_TO_UNIT_MAP.containsKey(unitLabel)) {
+        throw new IllegalArgumentException(
+            "Time interval unit label '"
+                + unitLabel
+                + "' does not match any of the recognized units: "
+                + TimeUnit.getAllUnits());
+      }
+
+      this.number = number;
+      this.unit = LABEL_TO_UNIT_MAP.get(unitLabel);
+    }
+
+    public String getNumber() {
+      return number;
+    }
+
+    public ChronoUnit getUnit() {
+      return unit;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (!(o instanceof Time)) {
+        return false;
+      }
+      Time time = (Time) o;
+      return Objects.equal(number, time.number) && Objects.equal(unit, 
time.unit);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hashCode(number, unit);
+    }
+
+    /** @return duration in millis */
+    public long toMillis() {
+      if (isPeriod()) {
+        // Estimated 30 days per month
+        return parsePeriod(this).toTotalMonths() * 30 * 24 * 60 * 60 * 1000;
+      } else {
+        return parseDuration(this).toMillis();
+      }
+    }
+
+    public boolean isPeriod() {
+      return unit.isDateBased() && !ChronoUnit.DAYS.equals(unit);
+    }
+  }
+
+  public static Time parseTime(String text) {
+    checkNotNull(text);
+
+    final String trimmed = text.trim();
+    checkArgument(!trimmed.isEmpty(), "argument is an empty- or 
whitespace-only string");
+
+    final int len = trimmed.length();
+    int pos = 0;
+
+    char current;
+    while (pos < len && (current = trimmed.charAt(pos)) >= '0' && current <= 
'9') {
+      pos++;
+    }
+
+    String number = trimmed.substring(0, pos);
+    String unitLabel = trimmed.substring(pos).trim().toLowerCase(Locale.US);
+
+    return new Time(number, unitLabel);
+  }
+
+  /**
+   * Parse the given string to a java {@link Duration}. The string is in 
format "{length value}{time
+   * unit label}", e.g. "123ms", "321 s". If no time unit label is specified, 
it will be considered
+   * as milliseconds.
+   *
+   * <p>Supported time unit labels are:
+   *
+   * <ul>
+   *   <li>DAYS: "d", "day"
+   *   <li>HOURS: "h", "hour"
+   *   <li>MINUTES: "min", "minute"
+   *   <li>SECONDS: "s", "sec", "second"
+   *   <li>MILLISECONDS: "ms", "milli", "millisecond"
+   *   <li>MICROSECONDS: "µs", "micro", "microsecond"
+   *   <li>NANOSECONDS: "ns", "nano", "nanosecond"
+   * </ul>
+   *
+   * @param text string to parse.
+   */
+  public static Duration parseDuration(String text) {
+    return parseDuration(parseTime(text));
+  }
+
+  public static Duration parseDuration(Time time) {
+    if (time.isPeriod()) {
+      throw new IllegalArgumentException(
+          "The time unit '" + time.getUnit() + "' cannot parse to Duration");
+    }
+
+    try {
+      long value = Long.parseLong(time.getNumber());
+      return Duration.of(value, time.getUnit());
+    } catch (NumberFormatException e) {
+      throw new IllegalArgumentException(
+          "The value '"
+              + time.getNumber()
+              + "' cannot be re represented as 64bit number (numeric 
overflow).");
+    }
+  }
+
+  /**
+   * Parse the given string to a java {@link Period}. The string is in format 
"{length value}{time
+   * unit label}", e.g. "13m", "1 y".
+   *
+   * <p>Supported time unit labels are:
+   *
+   * <ul>
+   *   <li>MONTHS: "m", "month"
+   *   <li>YEARS: "y", "year"
+   * </ul>
+   *
+   * @param text string to parse.
+   */
+  public static Period parsePeriod(String text) {
+    return parsePeriod(parseTime(text));
+  }
+
+  public static Period parsePeriod(Time time) {
+    if (!time.isPeriod()) {
+      throw new IllegalArgumentException(
+          "The time unit '" + time.getUnit() + "' cannot parse to Period");
+    }
+
+    try {
+      int value = Integer.parseInt(time.getNumber());
+      switch (time.getUnit()) {
+        case MONTHS:
+          return Period.ofMonths(value);
+        case YEARS:
+          return Period.ofYears(value);
+        default:
+          throw new IllegalArgumentException("Unsupported time unit: " + 
time.getUnit());
+      }
+    } catch (NumberFormatException e) {
+      throw new IllegalArgumentException(
+          "The value '"
+              + time.getNumber()
+              + "' cannot be re represented as 32bit number (numeric 
overflow).");
+    }
+  }
+
+  /**
+   * Pretty prints the duration as a lowest granularity unit that does not 
lose precision.
+   *
+   * <p>Examples:
+   *
+   * <pre>{@code
+   * Duration.ofMilliseconds(60000) will be printed as 1 min
+   * Duration.ofHours(1).plusSeconds(1) will be printed as 3601 s
+   * }</pre>
+   *
+   * <b>NOTE:</b> It supports only durations that fit into long.
+   */
+  public static String formatWithHighestUnit(Duration duration) {
+    long nanos = duration.toNanos();
+
+    List<TimeUnit> orderedUnits =
+        Arrays.asList(
+            TimeUnit.NANOSECONDS,
+            TimeUnit.MICROSECONDS,
+            TimeUnit.MILLISECONDS,
+            TimeUnit.SECONDS,
+            TimeUnit.MINUTES,
+            TimeUnit.HOURS,
+            TimeUnit.DAYS);
+
+    TimeUnit highestIntegerUnit =
+        IntStream.range(0, orderedUnits.size())
+            .sequential()
+            .filter(idx -> nanos % 
orderedUnits.get(idx).unit.getDuration().toNanos() != 0)
+            .boxed()
+            .findFirst()
+            .map(
+                idx -> {
+                  if (idx == 0) {
+                    return orderedUnits.get(0);
+                  } else {
+                    return orderedUnits.get(idx - 1);
+                  }
+                })
+            .orElse(TimeUnit.MILLISECONDS);
+
+    return String.format(
+        "%d %s",
+        nanos / highestIntegerUnit.unit.getDuration().toNanos(),
+        highestIntegerUnit.getLabels().get(0));
+  }
+
+  private static ChronoUnit toChronoUnit(java.util.concurrent.TimeUnit 
timeUnit) {
+    switch (timeUnit) {
+      case NANOSECONDS:
+        return ChronoUnit.NANOS;
+      case MICROSECONDS:
+        return ChronoUnit.MICROS;
+      case MILLISECONDS:
+        return ChronoUnit.MILLIS;
+      case SECONDS:
+        return ChronoUnit.SECONDS;
+      case MINUTES:
+        return ChronoUnit.MINUTES;
+      case HOURS:
+        return ChronoUnit.HOURS;
+      case DAYS:
+        return ChronoUnit.DAYS;
+      default:
+        throw new IllegalArgumentException(String.format("Unsupported time 
unit %s.", timeUnit));
+    }
+  }
+
+  /** Enum which defines time unit, mostly used to parse value from 
configuration file. */
+  private enum TimeUnit {
+    YEARS(ChronoUnit.YEARS, singular("y"), plural("year")),
+    MONTHS(ChronoUnit.MONTHS, singular("m"), plural("month")),
+    DAYS(ChronoUnit.DAYS, singular("d"), plural("day")),
+    HOURS(ChronoUnit.HOURS, singular("h"), plural("hour")),
+    MINUTES(ChronoUnit.MINUTES, singular("min"), plural("minute")),
+    SECONDS(ChronoUnit.SECONDS, singular("s"), plural("sec"), 
plural("second")),
+    MILLISECONDS(ChronoUnit.MILLIS, singular("ms"), plural("milli"), 
plural("millisecond")),
+    MICROSECONDS(ChronoUnit.MICROS, singular("µs"), plural("micro"), 
plural("microsecond")),
+    NANOSECONDS(ChronoUnit.NANOS, singular("ns"), plural("nano"), 
plural("nanosecond"));
+
+    private static final String PLURAL_SUFFIX = "s";
+
+    private final List<String> labels;
+
+    private final ChronoUnit unit;
+
+    TimeUnit(ChronoUnit unit, String[]... labels) {
+      this.unit = unit;
+      this.labels = 
Arrays.stream(labels).flatMap(Arrays::stream).collect(Collectors.toList());
+    }
+
+    /**
+     * @param label the original label
+     * @return the singular format of the original label
+     */
+    private static String[] singular(String label) {
+      return new String[] {label};
+    }
+
+    /**
+     * @param label the original label
+     * @return both the singular format and plural format of the original label
+     */
+    private static String[] plural(String label) {
+      return new String[] {label, label + PLURAL_SUFFIX};
+    }
+
+    public static String getAllUnits() {
+      return Arrays.stream(TimeUnit.values())
+          .map(TimeUnit::createTimeUnitString)
+          .collect(Collectors.joining(", "));
+    }
+
+    private static String createTimeUnitString(TimeUnit timeUnit) {
+      return timeUnit.name() + ": (" + String.join(" | ", 
timeUnit.getLabels()) + ")";
+    }
+
+    public List<String> getLabels() {
+      return labels;
+    }
+
+    public ChronoUnit getUnit() {
+      return unit;
+    }
+  }
+}


Reply via email to