Merge pull request #478 from kag0/parse-period

Add support for getting value as Period
This commit is contained in:
Martynas Mickevičius 2017-10-06 15:18:17 +03:00 committed by GitHub
commit aa6958469a
6 changed files with 212 additions and 1 deletions

View File

@ -1296,7 +1296,27 @@ must be lowercase. Exactly these strings are supported:
- `m`, `minute`, `minutes`
- `h`, `hour`, `hours`
- `d`, `day`, `days`
### Period Format
Similar to the `getDuration()` method, there is a `getPeriod()` method
available for getting time units as a `java.time.Period`.
This can use the general "units format" described above; bare
numbers are taken to be in days, while strings are
parsed as a number plus an optional unit string.
The supported unit strings for period are case sensitive and
must be lowercase. Exactly these strings are supported:
- `d`, `day`, `days`
- `w`, `week`, `weeks`
- `m`, `mo`, `month`, `months` (note that if you are using `getTemporal()`
which may return either a `java.time.Duration` or a `java.time.Period`
you will want to use `mo` rather than `m` to prevent your unit being
parsed as minutes)
- `y`, `year`, `years`
### Size in bytes format
Implementations may wish to support a `getBytes()` returning a

View File

@ -4,6 +4,8 @@
package com.typesafe.config;
import java.time.Duration;
import java.time.Period;
import java.time.temporal.TemporalAmount;
import java.util.List;
import java.util.Map;
import java.util.Set;
@ -793,6 +795,44 @@ public interface Config extends ConfigMergeable {
*/
Duration getDuration(String path);
/**
* Gets a value as a java.time.Period. If the value is
* already a number, then it's taken as days; if it's
* a string, it's parsed understanding units suffixes like
* "10d" or "5w" as documented in the <a
* href="https://github.com/typesafehub/config/blob/master/HOCON.md">the
* spec</a>. This method never returns null.
*
* @since 1.3.0
*
* @param path
* path expression
* @return the period value at the requested path
* @throws ConfigException.Missing
* if value is absent or null
* @throws ConfigException.WrongType
* if value is not convertible to Long or String
* @throws ConfigException.BadValue
* if value cannot be parsed as a number of the given TimeUnit
*/
Period getPeriod(String path);
/**
* Gets a value as a java.time.temporal.TemporalAmount.
* This method will first try get get the value as a java.time.Duration, and if unsuccessful,
* then as a java.time.Period.
* This means that values like "5m" will be parsed as 5 minutes rather than 5 months
* @param path path expression
* @return the temporal value at the requested path
* @throws ConfigException.Missing
* if value is absent or null
* @throws ConfigException.WrongType
* if value is not convertible to Long or String
* @throws ConfigException.BadValue
* if value cannot be parsed as a TemporalAmount
*/
TemporalAmount getTemporal(String path);
/**
* Gets a list value (with any element type) as a {@link ConfigList}, which
* implements {@code java.util.List<ConfigValue>}. Throws if the path is

View File

@ -7,7 +7,11 @@ import java.io.ObjectStreamException;
import java.io.Serializable;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.time.DateTimeException;
import java.time.Duration;
import java.time.Period;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAmount;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.HashMap;
@ -322,6 +326,21 @@ final class SimpleConfig implements Config, MergeableValue, Serializable {
return Duration.ofNanos(nanos);
}
@Override
public Period getPeriod(String path){
ConfigValue v = find(path, ConfigValueType.STRING);
return parsePeriod((String) v.unwrapped(), v.origin(), path);
}
@Override
public TemporalAmount getTemporal(String path){
try{
return getDuration(path);
} catch (ConfigException.BadValue e){
return getPeriod(path);
}
}
@SuppressWarnings("unchecked")
private <T> List<T> getHomogeneousUnwrappedList(String path,
ConfigValueType expected) {
@ -583,6 +602,90 @@ final class SimpleConfig implements Config, MergeableValue, Serializable {
return s.substring(i + 1);
}
/**
* Parses a period string. If no units are specified in the string, it is
* assumed to be in days. The returned period is in days.
* The purpose of this function is to implement the period-related methods
* in the ConfigObject interface.
*
* @param input
* the string to parse
* @param originForException
* origin of the value being parsed
* @param pathForException
* path to include in exceptions
* @return duration in days
* @throws ConfigException
* if string is invalid
*/
public static Period parsePeriod(String input,
ConfigOrigin originForException, String pathForException) {
String s = ConfigImplUtil.unicodeTrim(input);
String originalUnitString = getUnits(s);
String unitString = originalUnitString;
String numberString = ConfigImplUtil.unicodeTrim(s.substring(0, s.length()
- unitString.length()));
ChronoUnit units;
// this would be caught later anyway, but the error message
// is more helpful if we check it here.
if (numberString.length() == 0)
throw new ConfigException.BadValue(originForException,
pathForException, "No number in period value '" + input
+ "'");
if (unitString.length() > 2 && !unitString.endsWith("s"))
unitString = unitString + "s";
// note that this is deliberately case-sensitive
if (unitString.equals("") || unitString.equals("d") || unitString.equals("days")) {
units = ChronoUnit.DAYS;
} else if (unitString.equals("w") || unitString.equals("weeks")) {
units = ChronoUnit.WEEKS;
} else if (unitString.equals("m") || unitString.equals("mo") || unitString.equals("months")) {
units = ChronoUnit.MONTHS;
} else if (unitString.equals("y") || unitString.equals("years")) {
units = ChronoUnit.YEARS;
} else {
throw new ConfigException.BadValue(originForException,
pathForException, "Could not parse time unit '"
+ originalUnitString
+ "' (try d, w, mo, y)");
}
try {
return periodOf(Integer.parseInt(numberString), units);
} catch (NumberFormatException e) {
throw new ConfigException.BadValue(originForException,
pathForException, "Could not parse duration number '"
+ numberString + "'");
}
}
private static Period periodOf(int n, ChronoUnit unit){
if(unit.isTimeBased()){
throw new DateTimeException(unit + " cannot be converted to a java.time.Period");
}
switch (unit){
case DAYS:
return Period.ofDays(n);
case WEEKS:
return Period.ofWeeks(n);
case MONTHS:
return Period.ofMonths(n);
case YEARS:
return Period.ofYears(n);
default:
throw new DateTimeException(unit + " cannot be converted to a java.time.Period");
}
}
/**
* Parses a duration string. If no units are specified in the string, it is
* assumed to be in milliseconds. The returned duration is in nanoseconds.

View File

@ -65,6 +65,14 @@
"minusLargeNanos" : -4878955355435272204ns
},
"periods" : {
"day" : 1d,
"dayAsNumber": 2,
"week": 3 weeks,
"month": 5 mo,
"year": 8y
},
"memsizes" : {
"meg" : 1M,
"megsList" : [1M, 1024K, 1048576],

View File

@ -3,13 +3,16 @@
*/
package com.typesafe.config.impl
import java.time.temporal.{ ChronoUnit, TemporalUnit }
import org.junit.Assert._
import org.junit._
import com.typesafe.config._
import java.util.concurrent.TimeUnit
import scala.collection.JavaConverters._
import com.typesafe.config.ConfigResolveOptions
import java.util.concurrent.TimeUnit.{ SECONDS, NANOSECONDS, MICROSECONDS, MILLISECONDS, MINUTES, DAYS, HOURS }
import java.util.concurrent.TimeUnit.{ DAYS, HOURS, MICROSECONDS, MILLISECONDS, MINUTES, NANOSECONDS, SECONDS }
class ConfigTest extends TestUtils {
@ -811,6 +814,13 @@ class ConfigTest extends TestUtils {
assertDurationAsTimeUnit(HOURS)
assertDurationAsTimeUnit(DAYS)
// periods
assertEquals(1, conf.getPeriod("periods.day").get(ChronoUnit.DAYS))
assertEquals(2, conf.getPeriod("periods.dayAsNumber").getDays)
assertEquals(3 * 7, conf.getTemporal("periods.week").get(ChronoUnit.DAYS))
assertEquals(5, conf.getTemporal("periods.month").get(ChronoUnit.MONTHS))
assertEquals(8, conf.getTemporal("periods.year").get(ChronoUnit.YEARS))
// should get size in bytes
assertEquals(1024 * 1024L, conf.getBytes("memsizes.meg"))
assertEquals(1024 * 1024L, conf.getBytes("memsizes.megAsNumber"))

View File

@ -3,6 +3,9 @@
*/
package com.typesafe.config.impl
import java.time.{ LocalDate, Period }
import java.time.temporal.ChronoUnit
import org.junit.Assert._
import org.junit._
import com.typesafe.config._
@ -40,6 +43,33 @@ class UnitParserTest extends TestUtils {
assertTrue(e2.getMessage.contains("duration number"))
}
@Test
def parsePeriod() = {
val oneYears = List(
"1y", "1 y", "1year", "1 years", " 1y ", " 1 y ",
"365", "365d", "365 d", "365 days", " 365 days ", "365day",
"12m", "12mo", "12 m", " 12 mo ", "12 months", "12month")
val epochDate = LocalDate.ofEpochDay(0)
val oneYear = ChronoUnit.DAYS.between(epochDate, epochDate.plus(Period.ofYears(1)))
for (y <- oneYears) {
val period = SimpleConfig.parsePeriod(y, fakeOrigin(), "test")
val dayCount = ChronoUnit.DAYS.between(epochDate, epochDate.plus(period))
assertEquals(oneYear, dayCount)
}
// bad units
val e = intercept[ConfigException.BadValue] {
SimpleConfig.parsePeriod("100 dollars", fakeOrigin(), "test")
}
assertTrue(s"${e.getMessage} was not the expected error message", e.getMessage.contains("time unit"))
// bad number
val e2 = intercept[ConfigException.BadValue] {
SimpleConfig.parsePeriod("1 00 seconds", fakeOrigin(), "test")
}
assertTrue(s"${e2.getMessage} was not the expected error message", e2.getMessage.contains("time unit 'seconds'"))
}
// https://github.com/typesafehub/config/issues/117
// this broke because "1d" is a valid double for parseDouble
@Test