Commit dc4d71f9 authored by Grubhart's avatar Grubhart Committed by Phillip Webb

Add Period converter support

Add converter support for `javax.time.Period` including:

	String -> Period
	Number -> Period
	Period -> String

Period to Number conversion is not supported since `Period` has no
ability to deduce the number of calendar days in the period.

See gh-21136
parent a8f56b57
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
......@@ -130,6 +130,19 @@ public class JavaCompilerFieldValuesParser implements FieldValuesParser {
DATA_SIZE_SUFFIX = Collections.unmodifiableMap(values);
}
private static final String PERIOD_OF = "Period.of";
private static final Map<String, String> PERIOD_SUFFIX;
static {
Map<String, String> values = new HashMap<>();
values.put("Days", "d");
values.put("Weeks", "w");
values.put("Months", "m");
values.put("Years", "y");
PERIOD_SUFFIX = Collections.unmodifiableMap(values);
}
private final Map<String, Object> fieldValues = new HashMap<>();
private final Map<String, Object> staticFinals = new HashMap<>();
......@@ -194,6 +207,10 @@ public class JavaCompilerFieldValuesParser implements FieldValuesParser {
if (dataSizeValue != null) {
return dataSizeValue;
}
Object periodValue = getFactoryValue(expression, factoryValue, PERIOD_OF, PERIOD_SUFFIX);
if (periodValue != null) {
return periodValue;
}
return factoryValue;
}
......
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
......@@ -100,6 +100,11 @@ public abstract class AbstractFieldValuesProcessorTests {
assertThat(values.get("dataSizeMegabytes")).isEqualTo("20MB");
assertThat(values.get("dataSizeGigabytes")).isEqualTo("30GB");
assertThat(values.get("dataSizeTerabytes")).isEqualTo("40TB");
assertThat(values.get("periodNone")).isNull();
assertThat(values.get("periodDays")).isEqualTo("3d");
assertThat(values.get("periodWeeks")).isEqualTo("2w");
assertThat(values.get("periodMonths")).isEqualTo("10m");
assertThat(values.get("periodYears")).isEqualTo("15y");
}
@SupportedAnnotationTypes({ "org.springframework.boot.configurationsample.ConfigurationProperties" })
......
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
......@@ -19,6 +19,7 @@ package org.springframework.boot.configurationsample.fieldvalues;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Period;
import org.springframework.boot.configurationsample.ConfigurationProperties;
import org.springframework.util.MimeType;
......@@ -136,4 +137,14 @@ public class FieldValues {
private DataSize dataSizeTerabytes = DataSize.ofTerabytes(40);
private Period periodNone;
private Period periodDays = Period.ofDays(3);
private Period periodWeeks = Period.ofWeeks(2);
private Period periodMonths = Period.ofMonths(10);
private Period periodYears = Period.ofYears(15);
}
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
......@@ -110,8 +110,11 @@ public class ApplicationConversionService extends FormattingConversionService {
public static void addApplicationConverters(ConverterRegistry registry) {
addDelimitedStringConverters(registry);
registry.addConverter(new StringToDurationConverter());
registry.addConverter(new StringToPeriodConverter());
registry.addConverter(new DurationToStringConverter());
registry.addConverter(new PeriodToStringConverter());
registry.addConverter(new NumberToDurationConverter());
registry.addConverter(new NumberToPeriodConverter());
registry.addConverter(new DurationToNumberConverter());
registry.addConverter(new StringToDataSizeConverter());
registry.addConverter(new NumberToDataSizeConverter());
......
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.boot.convert;
import java.time.Period;
import java.util.Collections;
import java.util.Set;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.GenericConverter;
/**
* {@link Converter} to convert from a {@link Number} to a {@link Period}. Supports
* {@link Period#parse(CharSequence)} as well a more readable {@code 10m} form.
*
* @author Eddú Meléndez
* @author Edson Chávez
* @see PeriodFormat
* @see PeriodUnit
*/
final class NumberToPeriodConverter implements GenericConverter {
private final StringToPeriodConverter delegate = new StringToPeriodConverter();
@Override
public Set<ConvertiblePair> getConvertibleTypes() {
return Collections.singleton(new ConvertiblePair(Number.class, Period.class));
}
@Override
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
return this.delegate.convert((source != null) ? source.toString() : null, TypeDescriptor.valueOf(String.class),
targetType);
}
}
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.boot.convert;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.time.Period;
/**
* Annotation that can be used to indicate the format to use when converting a
* {@link Period}.
*
* @author Eddú Meléndez
* @author Edson Chávez
* @since 2.3.0
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PeriodFormat {
/**
* The {@link Period} format style.
* @return the period format style.
*/
PeriodStyle value();
}
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.boot.convert;
import java.time.Period;
import java.time.temporal.ChronoUnit;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* A standard set of {@link Period} units.
*
* @author Eddú Meléndez
* @author Edson Chávez
* @since 2.3.0
* @see Period
*/
public enum PeriodStyle {
/**
* Simple formatting, for example '1d'.
*/
SIMPLE("^([\\+\\-]?\\d+)([a-zA-Z]{0,2})$") {
@Override
public Period parse(String value, ChronoUnit unit) {
try {
Matcher matcher = matcher(value);
Assert.state(matcher.matches(), "Does not match simple period pattern");
String suffix = matcher.group(2);
return (StringUtils.hasLength(suffix) ? Unit.fromSuffix(suffix) : Unit.fromChronoUnit(unit))
.parse(matcher.group(1));
}
catch (Exception ex) {
throw new IllegalArgumentException("'" + value + "' is not a valid simple period", ex);
}
}
@Override
public String print(Period value, ChronoUnit unit) {
return Unit.fromChronoUnit(unit).print(value);
}
},
/**
* ISO-8601 formatting.
*/
ISO8601("^[\\+\\-]?P.*$") {
@Override
public Period parse(String value, ChronoUnit unit) {
try {
return Period.parse(value);
}
catch (Exception ex) {
throw new IllegalArgumentException("'" + value + "' is not a valid ISO-8601 period", ex);
}
}
@Override
public String print(Period value, ChronoUnit unit) {
return value.toString();
}
};
private final Pattern pattern;
PeriodStyle(String pattern) {
this.pattern = Pattern.compile(pattern);
}
protected final boolean matches(String value) {
return this.pattern.matcher(value).matches();
}
protected final Matcher matcher(String value) {
return this.pattern.matcher(value);
}
/**
* Parse the given value to a Period.
* @param value the value to parse
* @return a period
*/
public Period parse(String value) {
return parse(value, null);
}
/**
* Parse the given value to a period.
* @param value the value to parse
* @param unit the period unit to use if the value doesn't specify one ({@code null}
* will default to d)
* @return a period
*/
public abstract Period parse(String value, ChronoUnit unit);
/**
* Print the specified period.
* @param value the value to print
* @return the printed result
*/
public String print(Period value) {
return print(value, null);
}
/**
* Print the specified period using the given unit.
* @param value the value to print
* @param unit the value to use for printing
* @return the printed result
*/
public abstract String print(Period value, ChronoUnit unit);
/**
* Detect the style then parse the value to return a period.
* @param value the value to parse
* @return the parsed period
* @throws IllegalStateException if the value is not a known style or cannot be parsed
*/
public static Period detectAndParse(String value) {
return detectAndParse(value, null);
}
/**
* Detect the style then parse the value to return a period.
* @param value the value to parse
* @param unit the period unit to use if the value doesn't specify one ({@code null}
* will default to ms)
* @return the parsed period
* @throws IllegalStateException if the value is not a known style or cannot be parsed
*/
public static Period detectAndParse(String value, ChronoUnit unit) {
return detect(value).parse(value, unit);
}
/**
* Detect the style from the given source value.
* @param value the source value
* @return the period style
* @throws IllegalStateException if the value is not a known style
*/
public static PeriodStyle detect(String value) {
Assert.notNull(value, "Value must not be null");
for (PeriodStyle candidate : values()) {
if (candidate.matches(value)) {
return candidate;
}
}
throw new IllegalArgumentException("'" + value + "' is not a valid period");
}
enum Unit {
/**
* Days, represented by suffix {@code d}.
*/
DAYS(ChronoUnit.DAYS, "d", Period::getDays),
/**
* Months, represented by suffix {@code m}.
*/
MONTHS(ChronoUnit.MONTHS, "m", Period::getMonths),
/**
* Years, represented by suffix {@code y}.
*/
YEARS(ChronoUnit.YEARS, "y", Period::getYears);
private final ChronoUnit chronoUnit;
private final String suffix;
private final Function<Period, Integer> intValue;
Unit(ChronoUnit chronoUnit, String suffix, Function<Period, Integer> intValue) {
this.chronoUnit = chronoUnit;
this.suffix = suffix;
this.intValue = intValue;
}
/**
* Return the {@link Unit} matching the specified {@code suffix}.
* @param suffix one of the standard suffixes
* @return the {@link Unit} matching the specified {@code suffix}
* @throws IllegalArgumentException if the suffix does not match the suffix of any
* of this enum's constants
*/
public static Unit fromSuffix(String suffix) {
for (Unit candidate : values()) {
if (candidate.suffix.equalsIgnoreCase(suffix)) {
return candidate;
}
}
throw new IllegalArgumentException("Unknown unit suffix '" + suffix + "'");
}
public Period parse(String value) {
int intValue = Integer.parseInt(value);
if (ChronoUnit.DAYS == this.chronoUnit) {
return Period.ofDays(intValue);
}
else if (ChronoUnit.WEEKS == this.chronoUnit) {
return Period.ofWeeks(intValue);
}
else if (ChronoUnit.MONTHS == this.chronoUnit) {
return Period.ofMonths(intValue);
}
else if (ChronoUnit.YEARS == this.chronoUnit) {
return Period.ofYears(intValue);
}
throw new IllegalArgumentException("Unknow unit '" + this.chronoUnit + "'");
}
public String print(Period value) {
return longValue(value) + this.suffix;
}
public long longValue(Period value) {
return this.intValue.apply(value);
}
public static Unit fromChronoUnit(ChronoUnit chronoUnit) {
if (chronoUnit == null) {
return Unit.DAYS;
}
for (Unit candidate : values()) {
if (candidate.chronoUnit == chronoUnit) {
return candidate;
}
}
throw new IllegalArgumentException("Unknown unit " + chronoUnit);
}
}
}
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.boot.convert;
import java.time.Period;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.Set;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.GenericConverter;
import org.springframework.util.ObjectUtils;
/**
* {@link Converter} to convert from a {@link Period} to a {@link String}.
*
* @author Eddú Meléndez
* @author Edson Chávez
* @since 2.3.0
* @see Period
*/
public class PeriodToStringConverter implements GenericConverter {
@Override
public Set<ConvertiblePair> getConvertibleTypes() {
return Collections.singleton(new ConvertiblePair(Period.class, String.class));
}
@Override
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
if (ObjectUtils.isEmpty(source)) {
return null;
}
return convert((Period) source, getPeriodStyle(sourceType), getPeriodUnit(sourceType));
}
private PeriodStyle getPeriodStyle(TypeDescriptor sourceType) {
PeriodFormat annotation = sourceType.getAnnotation(PeriodFormat.class);
return (annotation != null) ? annotation.value() : null;
}
private String convert(Period source, PeriodStyle style, ChronoUnit unit) {
style = (style != null) ? style : PeriodStyle.ISO8601;
return style.print(source, unit);
}
private ChronoUnit getPeriodUnit(TypeDescriptor sourceType) {
PeriodUnit annotation = sourceType.getAnnotation(PeriodUnit.class);
return (annotation != null) ? annotation.value() : null;
}
}
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.boot.convert;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.time.Period;
import java.time.temporal.ChronoUnit;
/**
* Annotation that can be used to change the default unit used when converting a
* {@link Period}.
*
* @author Eddú Meléndez
* @author Edson Chávez
* @since 2.3.0
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PeriodUnit {
/**
* The Period unit to use if one is not specified.
* @return the Period unit
*/
ChronoUnit value();
}
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.boot.convert;
import java.time.Period;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.Set;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.GenericConverter;
import org.springframework.util.ObjectUtils;
/**
* {@link Converter} to convert from a {@link String} to a {@link Period}. Supports
* {@link Period#parse(CharSequence)} as well a more readable form.
*
* @author Eddú Meléndez
* @author Edson Chávez
* @since 2.3.0
* @see PeriodUnit
*/
public class StringToPeriodConverter implements GenericConverter {
@Override
public Set<GenericConverter.ConvertiblePair> getConvertibleTypes() {
return Collections.singleton(new GenericConverter.ConvertiblePair(String.class, Period.class));
}
@Override
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
if (ObjectUtils.isEmpty(source)) {
return null;
}
return convert(source.toString(), getStyle(targetType), getPeriodUnit(targetType));
}
private PeriodStyle getStyle(TypeDescriptor targetType) {
PeriodFormat annotation = targetType.getAnnotation(PeriodFormat.class);
return (annotation != null) ? annotation.value() : null;
}
private ChronoUnit getPeriodUnit(TypeDescriptor targetType) {
PeriodUnit annotation = targetType.getAnnotation(PeriodUnit.class);
return (annotation != null) ? annotation.value() : null;
}
private Period convert(String source, PeriodStyle style, ChronoUnit unit) {
style = (style != null) ? style : PeriodStyle.detect(source);
return style.parse(source, unit);
}
}
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.boot.convert;
import java.time.Period;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.convert.TypeDescriptor;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Create a mock {@link TypeDescriptor} with optional {@link PeriodUnit @PeriodUnit} and
* {@link PeriodFormat @PeriodFormat} annotations.
*
* @author Eddú Meléndez
* @author Edson Chávez
*/
public final class MockPeriodTypeDescriptor {
private MockPeriodTypeDescriptor() {
}
@SuppressWarnings({ "rawtypes", "unchecked" })
public static TypeDescriptor get(ChronoUnit unit, PeriodStyle style) {
TypeDescriptor descriptor = mock(TypeDescriptor.class);
if (unit != null) {
PeriodUnit unitAnnotation = AnnotationUtils.synthesizeAnnotation(Collections.singletonMap("value", unit),
PeriodUnit.class, null);
given(descriptor.getAnnotation(PeriodUnit.class)).willReturn(unitAnnotation);
}
if (style != null) {
PeriodFormat formatAnnotation = AnnotationUtils
.synthesizeAnnotation(Collections.singletonMap("value", style), PeriodFormat.class, null);
given(descriptor.getAnnotation(PeriodFormat.class)).willReturn(formatAnnotation);
}
given(descriptor.getType()).willReturn((Class) Period.class);
given(descriptor.getObjectType()).willReturn((Class) Period.class);
return descriptor;
}
}
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.boot.convert;
import java.time.Period;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.stream.Stream;
import org.junit.jupiter.params.provider.Arguments;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link NumberToPeriodConverter}.
*
* @author Eddú Meléndez
* @author Edson Chávez
*/
class NumberToPeriodConverterTests {
@ConversionServiceTest
void convertWhenSimpleWithoutSuffixShouldReturnPeriod(ConversionService conversionService) {
assertThat(convert(conversionService, 10)).isEqualTo(Period.ofDays(10));
assertThat(convert(conversionService, +10)).isEqualTo(Period.ofDays(10));
assertThat(convert(conversionService, -10)).isEqualTo(Period.ofDays(-10));
}
@ConversionServiceTest
void convertWhenSimpleWithoutSuffixButWithAnnotationShouldReturnPeriod(ConversionService conversionService) {
assertThat(convert(conversionService, 10, ChronoUnit.MONTHS)).isEqualTo(Period.ofMonths(10));
assertThat(convert(conversionService, +10, ChronoUnit.MONTHS)).isEqualTo(Period.ofMonths(10));
assertThat(convert(conversionService, -10, ChronoUnit.MONTHS)).isEqualTo(Period.ofMonths(-10));
}
private Period convert(ConversionService conversionService, Integer source) {
return conversionService.convert(source, Period.class);
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private Period convert(ConversionService conversionService, Integer source, ChronoUnit defaultUnit) {
TypeDescriptor targetType = mock(TypeDescriptor.class);
if (defaultUnit != null) {
PeriodUnit unitAnnotation = AnnotationUtils
.synthesizeAnnotation(Collections.singletonMap("value", defaultUnit), PeriodUnit.class, null);
given(targetType.getAnnotation(PeriodUnit.class)).willReturn(unitAnnotation);
}
given(targetType.getType()).willReturn((Class) Period.class);
return (Period) conversionService.convert(source, TypeDescriptor.forObject(source), targetType);
}
static Stream<? extends Arguments> conversionServices() {
return ConversionServiceArguments.with(new NumberToPeriodConverter());
}
}
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.boot.convert;
import java.time.Period;
import java.time.temporal.ChronoUnit;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link PeriodStyle}.
*
* @author Eddú Meléndez
* @author Edson Chávez
*/
class PeriodStyleTests {
@Test
void detectAndParseWhenValueIsNullShouldThrowException() {
assertThatIllegalArgumentException().isThrownBy(() -> PeriodStyle.detectAndParse(null))
.withMessageContaining("Value must not be null");
}
@Test
void detectAndParseWhenIso8601ShouldReturnPeriod() {
assertThat(PeriodStyle.detectAndParse("P15M")).isEqualTo(Period.parse("P15M"));
assertThat(PeriodStyle.detectAndParse("-P15M")).isEqualTo(Period.parse("P-15M"));
assertThat(PeriodStyle.detectAndParse("+P15M")).isEqualTo(Period.parse("P15M"));
assertThat(PeriodStyle.detectAndParse("P2D")).isEqualTo(Period.parse("P2D"));
assertThat(PeriodStyle.detectAndParse("-P20Y")).isEqualTo(Period.parse("P-20Y"));
}
@Test
void detectAndParseWhenSimpleDaysShouldReturnPeriod() {
assertThat(PeriodStyle.detectAndParse("10d")).isEqualTo(Period.ofDays(10));
assertThat(PeriodStyle.detectAndParse("10D")).isEqualTo(Period.ofDays(10));
assertThat(PeriodStyle.detectAndParse("+10d")).isEqualTo(Period.ofDays(10));
assertThat(PeriodStyle.detectAndParse("-10D")).isEqualTo(Period.ofDays(-10));
}
@Test
void detectAndParseWhenSimpleMonthsShouldReturnPeriod() {
assertThat(PeriodStyle.detectAndParse("10m")).isEqualTo(Period.ofMonths(10));
assertThat(PeriodStyle.detectAndParse("10M")).isEqualTo(Period.ofMonths(10));
assertThat(PeriodStyle.detectAndParse("+10m")).isEqualTo(Period.ofMonths(10));
assertThat(PeriodStyle.detectAndParse("-10M")).isEqualTo(Period.ofMonths(-10));
}
@Test
void detectAndParseWhenSimpleYearsShouldReturnPeriod() {
assertThat(PeriodStyle.detectAndParse("10y")).isEqualTo(Period.ofYears(10));
assertThat(PeriodStyle.detectAndParse("10Y")).isEqualTo(Period.ofYears(10));
assertThat(PeriodStyle.detectAndParse("+10y")).isEqualTo(Period.ofYears(10));
assertThat(PeriodStyle.detectAndParse("-10Y")).isEqualTo(Period.ofYears(-10));
}
@Test
void detectAndParseWhenSimpleWithoutSuffixShouldReturnPeriod() {
assertThat(PeriodStyle.detectAndParse("10")).isEqualTo(Period.ofDays(10));
assertThat(PeriodStyle.detectAndParse("+10")).isEqualTo(Period.ofDays(10));
assertThat(PeriodStyle.detectAndParse("-10")).isEqualTo(Period.ofDays(-10));
}
@Test
void detectAndParseWhenSimpleWithoutSuffixButWithChronoUnitShouldReturnPeriod() {
assertThat(PeriodStyle.detectAndParse("10", ChronoUnit.MONTHS)).isEqualTo(Period.ofMonths(10));
assertThat(PeriodStyle.detectAndParse("+10", ChronoUnit.MONTHS)).isEqualTo(Period.ofMonths(10));
assertThat(PeriodStyle.detectAndParse("-10", ChronoUnit.MONTHS)).isEqualTo(Period.ofMonths(-10));
}
@Test
void detectAndParseWhenBadFormatShouldThrowException() {
assertThatIllegalArgumentException().isThrownBy(() -> PeriodStyle.detectAndParse("10foo"))
.withMessageContaining("'10foo' is not a valid period");
}
@Test
void detectWhenSimpleShouldReturnSimple() {
assertThat(PeriodStyle.detect("10")).isEqualTo(PeriodStyle.SIMPLE);
assertThat(PeriodStyle.detect("+10")).isEqualTo(PeriodStyle.SIMPLE);
assertThat(PeriodStyle.detect("-10")).isEqualTo(PeriodStyle.SIMPLE);
assertThat(PeriodStyle.detect("10m")).isEqualTo(PeriodStyle.SIMPLE);
assertThat(PeriodStyle.detect("10y")).isEqualTo(PeriodStyle.SIMPLE);
assertThat(PeriodStyle.detect("10d")).isEqualTo(PeriodStyle.SIMPLE);
assertThat(PeriodStyle.detect("10D")).isEqualTo(PeriodStyle.SIMPLE);
}
@Test
void detectWhenIso8601ShouldReturnIso8601() {
assertThat(PeriodStyle.detect("P20")).isEqualTo(PeriodStyle.ISO8601);
assertThat(PeriodStyle.detect("-P15M")).isEqualTo(PeriodStyle.ISO8601);
assertThat(PeriodStyle.detect("+P15M")).isEqualTo(PeriodStyle.ISO8601);
assertThat(PeriodStyle.detect("P10Y")).isEqualTo(PeriodStyle.ISO8601);
assertThat(PeriodStyle.detect("P2D")).isEqualTo(PeriodStyle.ISO8601);
assertThat(PeriodStyle.detect("-P6")).isEqualTo(PeriodStyle.ISO8601);
assertThat(PeriodStyle.detect("-P-6M")).isEqualTo(PeriodStyle.ISO8601);
}
@Test
void detectWhenUnknownShouldThrowException() {
assertThatIllegalArgumentException().isThrownBy(() -> PeriodStyle.detect("bad"))
.withMessageContaining("'bad' is not a valid period");
}
@Test
void parseIso8601ShouldParse() {
assertThat(PeriodStyle.ISO8601.parse("P20D")).isEqualTo(Period.parse("P20D"));
assertThat(PeriodStyle.ISO8601.parse("P15M")).isEqualTo(Period.parse("P15M"));
assertThat(PeriodStyle.ISO8601.parse("+P15M")).isEqualTo(Period.parse("P15M"));
assertThat(PeriodStyle.ISO8601.parse("P10Y")).isEqualTo(Period.parse("P10Y"));
assertThat(PeriodStyle.ISO8601.parse("P2D")).isEqualTo(Period.parse("P2D"));
assertThat(PeriodStyle.ISO8601.parse("-P6D")).isEqualTo(Period.parse("-P6D"));
assertThat(PeriodStyle.ISO8601.parse("-P-6Y+3M")).isEqualTo(Period.parse("-P-6Y+3M"));
}
@Test
void parseIso8601WithUnitShouldIgnoreUnit() {
assertThat(PeriodStyle.ISO8601.parse("P20D", ChronoUnit.SECONDS)).isEqualTo(Period.parse("P20D"));
assertThat(PeriodStyle.ISO8601.parse("P15M", ChronoUnit.SECONDS)).isEqualTo(Period.parse("P15M"));
assertThat(PeriodStyle.ISO8601.parse("+P15M", ChronoUnit.SECONDS)).isEqualTo(Period.parse("P15M"));
assertThat(PeriodStyle.ISO8601.parse("P10Y", ChronoUnit.SECONDS)).isEqualTo(Period.parse("P10Y"));
assertThat(PeriodStyle.ISO8601.parse("P2D", ChronoUnit.SECONDS)).isEqualTo(Period.parse("P2D"));
assertThat(PeriodStyle.ISO8601.parse("-P6D", ChronoUnit.SECONDS)).isEqualTo(Period.parse("-P6D"));
assertThat(PeriodStyle.ISO8601.parse("-P-6Y+3M", ChronoUnit.SECONDS)).isEqualTo(Period.parse("-P-6Y+3M"));
}
@Test
void parseIso8601WhenSimpleShouldThrowException() {
assertThatIllegalArgumentException().isThrownBy(() -> PeriodStyle.ISO8601.parse("10d"))
.withMessageContaining("'10d' is not a valid ISO-8601 period");
}
@Test
void parseSimpleShouldParse() {
assertThat(PeriodStyle.SIMPLE.parse("10m")).isEqualTo(Period.ofMonths(10));
}
@Test
void parseSimpleWithUnitShouldUseUnitAsFallback() {
assertThat(PeriodStyle.SIMPLE.parse("10m", ChronoUnit.DAYS)).isEqualTo(Period.ofMonths(10));
assertThat(PeriodStyle.SIMPLE.parse("10", ChronoUnit.MONTHS)).isEqualTo(Period.ofMonths(10));
}
@Test
void parseSimpleWhenUnknownUnitShouldThrowException() {
assertThatIllegalArgumentException().isThrownBy(() -> PeriodStyle.SIMPLE.parse("10mb"))
.satisfies((ex) -> assertThat(ex.getCause().getMessage()).isEqualTo("Unknown unit suffix 'mb'"));
}
@Test
void parseSimpleWhenIso8601ShouldThrowException() {
assertThatIllegalArgumentException().isThrownBy(() -> PeriodStyle.SIMPLE.parse("PT10H"))
.withMessageContaining("'PT10H' is not a valid simple period");
}
@Test
void printIso8601ShouldPrint() {
Period period = Period.parse("-P-6M+3D");
assertThat(PeriodStyle.ISO8601.print(period)).isEqualTo("P6M-3D");
}
@Test
void printIso8601ShouldIgnoreUnit() {
Period period = Period.parse("-P3Y");
assertThat(PeriodStyle.ISO8601.print(period, ChronoUnit.DAYS)).isEqualTo("P-3Y");
}
@Test
void printSimpleWithoutUnitShouldPrintInDays() {
Period period = Period.ofMonths(1);
assertThat(PeriodStyle.SIMPLE.print(period)).isEqualTo("0d");
}
@Test
void printSimpleWithUnitShouldPrintInUnit() {
Period period = Period.ofYears(1000);
assertThat(PeriodStyle.SIMPLE.print(period, ChronoUnit.YEARS)).isEqualTo("1000y");
}
}
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.boot.convert;
import java.time.Period;
import java.time.temporal.ChronoUnit;
import java.util.stream.Stream;
import org.junit.jupiter.params.provider.Arguments;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link PeriodToStringConverter}.
*
* @author Eddú Melendez
* @author Edson Chávez
*/
class PeriodToStringConverterTests {
@ConversionServiceTest
void convertWithoutStyleShouldReturnIso8601(ConversionService conversionService) {
String converted = conversionService.convert(Period.ofDays(1), String.class);
assertThat(converted).isEqualTo(Period.ofDays(1).toString());
}
@ConversionServiceTest
void convertWithFormatShouldUseFormatAndDays(ConversionService conversionService) {
String converted = (String) conversionService.convert(Period.ofMonths(1),
MockPeriodTypeDescriptor.get(null, PeriodStyle.SIMPLE), TypeDescriptor.valueOf(String.class));
assertThat(converted).isEqualTo("0d");
}
@ConversionServiceTest
void convertWithFormatAndUnitShouldUseFormatAndUnit(ConversionService conversionService) {
String converted = (String) conversionService.convert(Period.ofYears(1),
MockPeriodTypeDescriptor.get(ChronoUnit.YEARS, PeriodStyle.SIMPLE),
TypeDescriptor.valueOf(String.class));
assertThat(converted).isEqualTo("1y");
}
static Stream<? extends Arguments> conversionServices() throws Exception {
return ConversionServiceArguments.with(new PeriodToStringConverter());
}
}
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.boot.convert;
import java.time.Period;
import java.time.temporal.ChronoUnit;
import java.util.stream.Stream;
import org.junit.jupiter.params.provider.Arguments;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link StringToPeriodConverter}.
*
* @author Eddú Meléndez
* @author Edson Chávez
*/
public class StringToPeriodConverterTest {
@ConversionServiceTest
void convertWhenIso8601ShouldReturnPeriod(ConversionService conversionService) {
assertThat(convert(conversionService, "P2Y")).isEqualTo(Period.parse("P2Y"));
assertThat(convert(conversionService, "P3M")).isEqualTo(Period.parse("P3M"));
assertThat(convert(conversionService, "P4W")).isEqualTo(Period.parse("P4W"));
assertThat(convert(conversionService, "P5D")).isEqualTo(Period.parse("P5D"));
assertThat(convert(conversionService, "P1Y2M3D")).isEqualTo(Period.parse("P1Y2M3D"));
assertThat(convert(conversionService, "P1Y2M3W4D")).isEqualTo(Period.parse("P1Y2M3W4D"));
assertThat(convert(conversionService, "P-1Y2M")).isEqualTo(Period.parse("P-1Y2M"));
assertThat(convert(conversionService, "-P1Y2M")).isEqualTo(Period.parse("-P1Y2M"));
}
@ConversionServiceTest
void convertWhenSimpleDaysShouldReturnPeriod(ConversionService conversionService) {
assertThat(convert(conversionService, "10d")).isEqualTo(Period.ofDays(10));
assertThat(convert(conversionService, "10D")).isEqualTo(Period.ofDays(10));
assertThat(convert(conversionService, "+10d")).isEqualTo(Period.ofDays(10));
assertThat(convert(conversionService, "-10D")).isEqualTo(Period.ofDays(-10));
}
@ConversionServiceTest
void convertWhenSimpleMonthsShouldReturnPeriod(ConversionService conversionService) {
assertThat(convert(conversionService, "10m")).isEqualTo(Period.ofMonths(10));
assertThat(convert(conversionService, "10M")).isEqualTo(Period.ofMonths(10));
assertThat(convert(conversionService, "+10m")).isEqualTo(Period.ofMonths(10));
assertThat(convert(conversionService, "-10M")).isEqualTo(Period.ofMonths(-10));
}
@ConversionServiceTest
void convertWhenSimpleYearsShouldReturnPeriod(ConversionService conversionService) {
assertThat(convert(conversionService, "10y")).isEqualTo(Period.ofYears(10));
assertThat(convert(conversionService, "10Y")).isEqualTo(Period.ofYears(10));
assertThat(convert(conversionService, "+10y")).isEqualTo(Period.ofYears(10));
assertThat(convert(conversionService, "-10Y")).isEqualTo(Period.ofYears(-10));
}
@ConversionServiceTest
void convertWhenSimpleWithoutSuffixShouldReturnPeriod(ConversionService conversionService) {
assertThat(convert(conversionService, "10")).isEqualTo(Period.ofDays(10));
assertThat(convert(conversionService, "+10")).isEqualTo(Period.ofDays(10));
assertThat(convert(conversionService, "-10")).isEqualTo(Period.ofDays(-10));
}
@ConversionServiceTest
void convertWhenSimpleWithoutSuffixButWithAnnotationShouldReturnPeriod(ConversionService conversionService) {
assertThat(convert(conversionService, "10", ChronoUnit.MONTHS, null)).isEqualTo(Period.ofMonths(10));
assertThat(convert(conversionService, "+10", ChronoUnit.MONTHS, null)).isEqualTo(Period.ofMonths(10));
assertThat(convert(conversionService, "-10", ChronoUnit.MONTHS, null)).isEqualTo(Period.ofMonths(-10));
}
private Period convert(ConversionService conversionService, String source) {
return conversionService.convert(source, Period.class);
}
private Period convert(ConversionService conversionService, String source, ChronoUnit unit, PeriodStyle style) {
return (Period) conversionService.convert(source, TypeDescriptor.forObject(source),
MockPeriodTypeDescriptor.get(unit, style));
}
static Stream<? extends Arguments> conversionServices() {
return ConversionServiceArguments.with(new StringToPeriodConverter());
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment