Add multi-unit DurationFormat.Style for duration parsing/printing

This adds the COMPOSITE style, which allows multiple segments each
similar to the SIMPLE style.

See gh-30396
Closes gh-33262
This commit is contained in:
Simon Baslé
2024-07-30 16:55:09 +02:00
parent f967f6f9f0
commit 6174d95ba2
4 changed files with 268 additions and 30 deletions

View File

@@ -27,7 +27,9 @@ import org.junit.jupiter.api.Test;
import org.springframework.format.annotation.DurationFormat.Unit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatException;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.springframework.format.annotation.DurationFormat.Style.COMPOSITE;
import static org.springframework.format.annotation.DurationFormat.Style.ISO8601;
import static org.springframework.format.annotation.DurationFormat.Style.SIMPLE;
@@ -132,6 +134,60 @@ class DurationFormatterUtilsTests {
.withCause(new DateTimeParseException("Text cannot be parsed to a Duration", "", 0));
}
@Test
void parseComposite() {
assertThat(DurationFormatterUtils.parse("1d2h34m57s28ms3us2ns", COMPOSITE))
.isEqualTo(Duration.ofDays(1).plusHours(2)
.plusMinutes(34).plusSeconds(57)
.plusMillis(28).plusNanos(3002));
}
@Test
void parseCompositeWithExplicitPlusSign() {
assertThat(DurationFormatterUtils.parse("+1d2h34m57s28ms3us2ns", COMPOSITE))
.isEqualTo(Duration.ofDays(1).plusHours(2)
.plusMinutes(34).plusSeconds(57)
.plusMillis(28).plusNanos(3002));
}
@Test
void parseCompositeWithExplicitMinusSign() {
assertThat(DurationFormatterUtils.parse("-1d2h34m57s28ms3us2ns", COMPOSITE))
.isEqualTo(Duration.ofDays(-1).plusHours(-2)
.plusMinutes(-34).plusSeconds(-57)
.plusMillis(-28).plusNanos(-3002));
}
@Test
void parseCompositePartial() {
assertThat(DurationFormatterUtils.parse("34m57s", COMPOSITE))
.isEqualTo(Duration.ofMinutes(34).plusSeconds(57));
}
@Test
void parseCompositePartialWithSpaces() {
assertThat(DurationFormatterUtils.parse("34m 57s", COMPOSITE))
.isEqualTo(Duration.ofMinutes(34).plusSeconds(57));
}
@Test //Kotlin style compatibility
void parseCompositeNegativeWithSpacesAndParenthesis() {
assertThat(DurationFormatterUtils.parse("-(34m 57s)", COMPOSITE))
.isEqualTo(Duration.ofMinutes(-34).plusSeconds(-57));
}
@Test
void parseCompositeBadSign() {
assertThatException().isThrownBy(() -> DurationFormatterUtils.parse("+-34m57s", COMPOSITE))
.havingCause().withMessage("Does not match composite duration pattern");
}
@Test
void parseCompositeBadUnit() {
assertThatException().isThrownBy(() -> DurationFormatterUtils.parse("34mo57s", COMPOSITE))
.havingCause().withMessage("Does not match composite duration pattern");
}
@Test
void printSimple() {
assertThat(DurationFormatterUtils.print(Duration.ofNanos(12345), SIMPLE, Unit.NANOS))
@@ -164,6 +220,26 @@ class DurationFormatterUtilsTests {
.isEqualTo("PT-3S");
}
@Test
void printCompositePositive() {
Duration composite = DurationFormatterUtils.parse("+1d2h34m57s28ms3us2ns", COMPOSITE);
assertThat(DurationFormatterUtils.print(composite, COMPOSITE))
.isEqualTo("1d2h34m57s28ms3us2ns");
}
@Test
void printCompositeZero() {
assertThat(DurationFormatterUtils.print(Duration.ZERO, COMPOSITE))
.isEqualTo("0s");
}
@Test
void printCompositeNegative() {
Duration composite = DurationFormatterUtils.parse("-1d2h34m57s28ms3us2ns", COMPOSITE);
assertThat(DurationFormatterUtils.print(composite, COMPOSITE))
.isEqualTo("-1d2h34m57s28ms3us2ns");
}
@Test
void detectAndParse() {
assertThat(DurationFormatterUtils.detectAndParse("PT1.234S", Unit.NANOS))
@@ -177,6 +253,10 @@ class DurationFormatterUtilsTests {
assertThat(DurationFormatterUtils.detectAndParse("1234", Unit.NANOS))
.as("simple without suffix")
.isEqualTo(Duration.ofNanos(1234));
assertThat(DurationFormatterUtils.detectAndParse("3s45ms", Unit.NANOS))
.as("composite")
.isEqualTo(Duration.ofMillis(3045));
}
@Test
@@ -192,6 +272,10 @@ class DurationFormatterUtilsTests {
assertThat(DurationFormatterUtils.detectAndParse("1234"))
.as("simple without suffix")
.isEqualTo(Duration.ofMillis(1234));
assertThat(DurationFormatterUtils.detectAndParse("3s45ms"))
.as("composite")
.isEqualTo(Duration.ofMillis(3045));
}
@Test
@@ -210,6 +294,10 @@ class DurationFormatterUtilsTests {
.as("invalid yet matching ISO8601 pattern")
.isEqualTo(ISO8601);
assertThat(DurationFormatterUtils.detect("-(1d 2h 34m 2ns)"))
.as("COMPOSITE")
.isEqualTo(COMPOSITE);
assertThatIllegalArgumentException().isThrownBy(() -> DurationFormatterUtils.detect("WPT2H-4M"))
.withMessage("'WPT2H-4M' is not a valid duration, cannot detect any known style")
.withNoCause();

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2002-2024 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.format.datetime.standard
import org.assertj.core.api.Assertions
import org.junit.jupiter.api.Test
import org.springframework.format.annotation.DurationFormat
import kotlin.time.Duration
import kotlin.time.toKotlinDuration
/**
* Tests for [DurationFormatterUtils] compatibility with Kotlin.
*
* @author Simon Baslé
*/
class DurationFormatterUtilsKotlinTests {
@Test
fun `Composite style parses Kotlin toString() with seconds resolution`() {
val kotlinString = "-(1d 2h 34m 57s)"
val duration = Duration.parse(kotlinString)
val parseResult = DurationFormatterUtils.parse(kotlinString, DurationFormat.Style.COMPOSITE)
.toKotlinDuration()
Assertions.assertThat(parseResult).isEqualTo(duration)
}
@Test
fun `Composite style fails Kotlin toString() with sub-second resolution`() {
val kotlinString = "-(1d 2h 34m 57.028003002s)"
Assertions.assertThatException().isThrownBy {
DurationFormatterUtils.parse(kotlinString, DurationFormat.Style.COMPOSITE) }
.withMessage("'$kotlinString' is not a valid composite duration")
}
@Test
fun `Detect and parse fails Kotlin toString() with sub-second resolution`() {
val kotlinString = "-(1d 2h 34m 57.028003002s)"
Assertions.assertThatException().isThrownBy { DurationFormatterUtils.detectAndParse(kotlinString) }
.withMessage("'$kotlinString' is not a valid duration, cannot detect any known style")
}
}