Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support of conversion to java.time classes #593

Merged
merged 2 commits into from
Mar 23, 2025
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.beust.jcommander.converters;

import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.util.List;

/**
* Converter to {@link Instant}.
*/
public final class InstantConverter extends JavaTimeConverter<Instant> {

public InstantConverter(String optionName) {
super(optionName, Instant.class);
}

@Override
protected List<DateTimeFormatter> supportedFormats() {
return List.of(DateTimeFormatter.ISO_INSTANT);
}

@Override
protected Instant parse(String value, DateTimeFormatter formatter) {
try {
long ms = Long.parseLong(value);
return Instant.ofEpochMilli(ms);
} catch (NumberFormatException e) {
return formatter.parse(value, Instant::from);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.beust.jcommander.converters;

import com.beust.jcommander.ParameterException;

import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.temporal.TemporalAccessor;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;

/**
* Base class for all {@link java.time} converters.
*
* @param <T> concrete type to parse into
*/
public abstract class JavaTimeConverter<T extends TemporalAccessor> extends BaseConverter<T> {

private final Class<T> toClass;

/**
* Inheritor constructors should have only 1 parameter - optionName.
*
* @param optionName name of the option
* @param toClass type to parse into
*/
protected JavaTimeConverter(String optionName, Class<T> toClass) {
super(optionName);
this.toClass = toClass;
}

@Override
public final T convert(String value) {
return supportedFormats().stream()
.map(new Mapper(value))
.filter(Objects::nonNull)
.findFirst()
.orElseThrow(() -> new ParameterException(errorMessage(value)));
}

/**
* Supported formats for this type, e.g. {@code HH:mm:ss}
*
* @return list of supported formats
*/
protected abstract List<DateTimeFormatter> supportedFormats();

/**
* Parse the value using the specified formatter.
*
* @param value value to parse
* @param formatter formatter specifying supported format
* @return parsed value
*/
protected abstract T parse(String value, DateTimeFormatter formatter);

private String errorMessage(String value) {
return getErrorString(value, "a " + toClass.getSimpleName());
}

private final class Mapper implements Function<DateTimeFormatter, T> {

private final String value;

private Mapper(String value) {
this.value = value;
}

@Override
public T apply(DateTimeFormatter formatter) {
try {
return parse(value, formatter);
} catch (DateTimeParseException exc) {
return null;
} catch (Exception exc) {
throw new ParameterException(errorMessage(value), exc);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.beust.jcommander.converters;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.List;

/**
* Converter to {@link LocalDate}.
*/
public class LocalDateConverter extends JavaTimeConverter<LocalDate> {

public LocalDateConverter(String optionName) {
super(optionName, LocalDate.class);
}

@Override
protected List<DateTimeFormatter> supportedFormats() {
return List.of(DateTimeFormatter.ISO_LOCAL_DATE, DateTimeFormatter.ofPattern("dd-MM-yyyy"));
}

@Override
protected LocalDate parse(String value, DateTimeFormatter formatter) {
return LocalDate.parse(value, formatter);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.beust.jcommander.converters;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.util.List;

/**
* Converter for {@link LocalDateTime}.
*/
public class LocalDateTimeConverter extends JavaTimeConverter<LocalDateTime> {

public LocalDateTimeConverter(String optionName) {
super(optionName, LocalDateTime.class);
}

@Override
protected List<DateTimeFormatter> supportedFormats() {
return List.of(
DateTimeFormatter.ISO_LOCAL_DATE_TIME,
new DateTimeFormatterBuilder().appendPattern("dd-MM-yyyy").appendLiteral('T').append(DateTimeFormatter.ISO_LOCAL_TIME).toFormatter()
);
}

@Override
protected LocalDateTime parse(String value, DateTimeFormatter formatter) {
return LocalDateTime.parse(value, formatter);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.beust.jcommander.converters;

import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.List;

/**
* Converter for {@link LocalTime}.
*/
public class LocalTimeConverter extends JavaTimeConverter<LocalTime> {

public LocalTimeConverter(String optionName) {
super(optionName, LocalTime.class);
}

@Override
protected List<DateTimeFormatter> supportedFormats() {
return List.of(DateTimeFormatter.ISO_LOCAL_TIME);
}

@Override
protected LocalTime parse(String value, DateTimeFormatter formatter) {
return LocalTime.parse(value, formatter);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.beust.jcommander.converters;

import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;

/**
* Converter for {@link OffsetDateTime}.
*/
public class OffsetDateTimeConverter extends JavaTimeConverter<OffsetDateTime> {

public OffsetDateTimeConverter(String optionName) {
super(optionName, OffsetDateTime.class);
}

@Override
protected List<DateTimeFormatter> supportedFormats() {
return List.of(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
}

@Override
protected OffsetDateTime parse(String value, DateTimeFormatter formatter) {
return OffsetDateTime.parse(value, formatter);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.beust.jcommander.converters;

import java.time.OffsetTime;
import java.time.format.DateTimeFormatter;
import java.util.List;

/**
* Converter for {@link OffsetTime}.
*/
public class OffsetTimeConverter extends JavaTimeConverter<OffsetTime> {

public OffsetTimeConverter(String optionName) {
super(optionName, OffsetTime.class);
}

@Override
protected List<DateTimeFormatter> supportedFormats() {
return List.of(DateTimeFormatter.ISO_OFFSET_TIME);
}

@Override
protected OffsetTime parse(String value, DateTimeFormatter formatter) {
return OffsetTime.parse(value, formatter);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.beust.jcommander.converters;

import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;

/**
* Converter for {@link ZonedDateTime}.
*/
public class ZonedDateTimeConverter extends JavaTimeConverter<ZonedDateTime> {

public ZonedDateTimeConverter(String optionName) {
super(optionName, ZonedDateTime.class);
}

@Override
protected List<DateTimeFormatter> supportedFormats() {
return List.of(DateTimeFormatter.ISO_ZONED_DATE_TIME);
}

@Override
protected ZonedDateTime parse(String value, DateTimeFormatter formatter) {
return ZonedDateTime.parse(value, formatter);
}
}
Original file line number Diff line number Diff line change
@@ -26,16 +26,30 @@
import com.beust.jcommander.converters.FileConverter;
import com.beust.jcommander.converters.FloatConverter;
import com.beust.jcommander.converters.ISO8601DateConverter;
import com.beust.jcommander.converters.InstantConverter;
import com.beust.jcommander.converters.IntegerConverter;
import com.beust.jcommander.converters.LocalDateConverter;
import com.beust.jcommander.converters.LocalDateTimeConverter;
import com.beust.jcommander.converters.LocalTimeConverter;
import com.beust.jcommander.converters.LongConverter;
import com.beust.jcommander.converters.OffsetDateTimeConverter;
import com.beust.jcommander.converters.OffsetTimeConverter;
import com.beust.jcommander.converters.StringConverter;
import com.beust.jcommander.converters.PathConverter;
import com.beust.jcommander.converters.URIConverter;
import com.beust.jcommander.converters.URLConverter;
import com.beust.jcommander.converters.ZonedDateTimeConverter;

import java.io.File;
import java.lang.NoClassDefFoundError;
import java.math.BigDecimal;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.time.ZonedDateTime;
import java.util.Date;
import java.net.URI;
import java.net.URL;
@@ -67,6 +81,14 @@ public class DefaultConverterFactory implements IStringConverterFactory {
classConverters.put(URI.class, URIConverter.class);
classConverters.put(URL.class, URLConverter.class);

classConverters.put(Instant.class, InstantConverter.class);
classConverters.put(LocalDate.class, LocalDateConverter.class);
classConverters.put(LocalDateTime.class, LocalDateTimeConverter.class);
classConverters.put(LocalTime.class, LocalTimeConverter.class);
classConverters.put(OffsetDateTime.class, OffsetDateTimeConverter.class);
classConverters.put(OffsetTime.class, OffsetTimeConverter.class);
classConverters.put(ZonedDateTime.class, ZonedDateTimeConverter.class);

try {
classConverters.put(Path.class, PathConverter.class);
} catch (NoClassDefFoundError ex) {
43 changes: 43 additions & 0 deletions src/test/java/com/beust/jcommander/JCommanderTest.java
Original file line number Diff line number Diff line change
@@ -39,6 +39,14 @@
import java.nio.file.Path;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.*;
import java.util.ResourceBundle;

@@ -1519,4 +1527,39 @@ public void validate(String name, Integer i) throws ParameterException {
}
}
}

@Test
public void javaTimeTest() {
String[] argv = {
"-i", "0",
"-ld", "1970-01-12",
"-ldt", "1970-01-12T11:15:09.000000333",
"-lt", "11:15:09.000000333",
"-odt", "1970-01-12T11:15:09.000000333+01:00",
"-ot", "11:15:09.000000333+01:00",
"-zdt", "1970-01-12T11:15:09.000000333Z"
};
JavaTimeParameters params = new JavaTimeParameters();
JCommander.newBuilder().addObject(params).build().parse(argv);

Assert.assertEquals(params.instant, Instant.EPOCH, "Instant parsed incorrectly");

LocalDate localDate = LocalDate.of(1970, 1, 12);
Assert.assertEquals(params.localDate, localDate, "LocalDate parsed incorrectly");

LocalTime localTime = LocalTime.of(11, 15, 9, 333);
Assert.assertEquals(params.localDateTime, LocalDateTime.of(localDate, localTime), "LocalDateTime parsed incorrectly");

Assert.assertEquals(params.localTime, localTime, "LocalTime parsed incorrectly");

ZoneOffset zoneOffset = ZoneOffset.ofHours(1);
OffsetDateTime offsetDateTime = OffsetDateTime.of(localDate, localTime, zoneOffset);
Assert.assertEquals(params.offsetDateTime, offsetDateTime, "OffsetDateTime parsed incorrectly");

OffsetTime offsetTime = OffsetTime.of(localTime, zoneOffset);
Assert.assertEquals(params.offsetTime, offsetTime, "OffsetTime parsed incorrectly");

ZonedDateTime zonedDateTime = ZonedDateTime.of(localDate, localTime, ZoneOffset.UTC);
Assert.assertEquals(params.zonedDateTime, zonedDateTime, "ZonedDateTime parsed incorrectly");
}
}
35 changes: 35 additions & 0 deletions src/test/java/com/beust/jcommander/args/JavaTimeParameters.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.beust.jcommander.args;

import com.beust.jcommander.Parameter;

import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.time.ZonedDateTime;

public class JavaTimeParameters {

@Parameter(names = "-i")
public Instant instant;

@Parameter(names = "-ld")
public LocalDate localDate;

@Parameter(names = "-ldt")
public LocalDateTime localDateTime;

@Parameter(names = "-lt")
public LocalTime localTime;

@Parameter(names = "-odt")
public OffsetDateTime offsetDateTime;

@Parameter(names = "-ot")
public OffsetTime offsetTime;

@Parameter(names = "-zdt")
public ZonedDateTime zonedDateTime;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.beust.jcommander.converters;

import com.beust.jcommander.ParameterException;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

import java.time.Instant;

import static org.testng.Assert.assertEquals;

public class InstantConverterTest {

private final InstantConverter converter = new InstantConverter("--op");

@Test(dataProvider = "data")
public void longValue_ShouldConvert(String msValue, String expectedIsoValue) {
Instant actual = converter.convert(msValue);
Instant expected = Instant.parse(expectedIsoValue);
assertEquals(actual, expected, "Incorrectly parsed instant from millis");
}

@DataProvider(name = "data")
public static Object[][] data() {
return new Object[][]{
{"111", "1970-01-01T00:00:00.111Z"},
{"1234567890", "1970-01-15T06:56:07.890Z"},
{"-11", "1969-12-31T23:59:59.989Z"}
};
}

@Test(dataProvider = "data")
public void isoValue_ShouldConvert(String expectedMsValue, String isoValue) {
Instant actual = converter.convert(isoValue);
Instant expected = Instant.ofEpochMilli(Long.parseLong(expectedMsValue));
assertEquals(actual, expected, "Incorrectly parsed instant from ISO string");
}

@Test(expectedExceptions = ParameterException.class)
public void unsupportedFormatValue_ShouldThrowException() {
converter.convert("qwerty");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.beust.jcommander.converters;

import com.beust.jcommander.ParameterException;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

import java.time.LocalDate;
import java.time.Month;

import static org.testng.Assert.assertEquals;

public class LocalDateConverterTest {

private final LocalDateConverter converter = new LocalDateConverter("-p");

@Test(dataProvider = "supported")
public void supportedFormats_ShouldConvert(String value) {
LocalDate actual = converter.convert(value);
LocalDate expected = LocalDate.of(2023, Month.MAY, 11);
assertEquals(actual, expected, "Incorrectly parsed local date");
}

@DataProvider(name = "supported")
public static Object[][] supported() {
return new Object[][]{
{"2023-05-11"},
{"11-05-2023"}
};
}

@Test(dataProvider = "unsupported", expectedExceptions = ParameterException.class)
public void unsupportedFormats_ShouldThrowException(String value) {
converter.convert(value);
}

@DataProvider(name = "unsupported")
public static Object[][] unsupported() {
return new Object[][]{
{"2023:05:11"},
{"asd"}
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.beust.jcommander.converters;

import com.beust.jcommander.ParameterException;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

import java.time.LocalDateTime;
import java.time.Month;

import static org.testng.Assert.assertEquals;

public class LocalDateTimeConverterTest {

private final LocalDateTimeConverter converter = new LocalDateTimeConverter("-p");

@Test(dataProvider = "supported")
public void supportedFormats_ShouldConvert(String value) {
LocalDateTime actual = converter.convert(value);
LocalDateTime expected = LocalDateTime.of(2023, Month.MAY, 11, 9, 15, 19);
assertEquals(actual, expected, "Incorrectly parsed local date time");
}

@DataProvider(name = "supported")
public static Object[][] supported() {
return new Object[][]{
{"2023-05-11T09:15:19"},
{"11-05-2023T09:15:19"}
};
}

@Test(dataProvider = "unsupported", expectedExceptions = ParameterException.class)
public void unsupportedFormats_ShouldThrowException(String value) {
converter.convert(value);
}

@DataProvider(name = "unsupported")
public static Object[][] unsupported() {
return new Object[][]{
{"2023/05/11T09:15:19"},
{"qwe"}
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.beust.jcommander.converters;

import com.beust.jcommander.ParameterException;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

import java.time.LocalTime;

import static org.testng.Assert.assertEquals;

public class LocalTimeConverterTest {

private final LocalTimeConverter converter = new LocalTimeConverter("-p");

@Test(dataProvider = "supported")
public void supportedFormats_ShouldConvert(String value) {
LocalTime actual = converter.convert(value);
LocalTime expected = LocalTime.of(10, 11, 0, 0);
assertEquals(actual, expected, "Incorrectly parsed local time");
}

@DataProvider(name = "supported")
public static Object[][] supported() {
return new Object[][]{
{"10:11:00.000"},
{"10:11"}
};
}

@Test(dataProvider = "unsupported", expectedExceptions = ParameterException.class)
public void unsupportedFormats_ShouldThrowException(String value) {
converter.convert(value);
}

@DataProvider(name = "unsupported")
public static Object[][] unsupported() {
return new Object[][]{
{"10-11"},
{"qwe"}
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.beust.jcommander.converters;

import com.beust.jcommander.ParameterException;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

import java.time.LocalDateTime;
import java.time.Month;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;

import static org.testng.Assert.assertEquals;

public class OffsetDateTimeTest {

private final OffsetDateTimeConverter converter = new OffsetDateTimeConverter("-p");

@Test(dataProvider = "supported")
public void supportedFormats_ShouldConvert(String value) {
OffsetDateTime actual = converter.convert(value);
OffsetDateTime expected = OffsetDateTime.of(LocalDateTime.of(2023, Month.MAY, 11, 9, 15, 19), ZoneOffset.UTC);
assertEquals(actual, expected, "Incorrectly parsed offset date time");
}

@DataProvider(name = "supported")
public static Object[][] supported() {
return new Object[][]{
{"2023-05-11T09:15:19+00:00"}
};
}

@Test(dataProvider = "unsupported", expectedExceptions = ParameterException.class)
public void unsupportedFormats_ShouldThrowException(String value) {
converter.convert(value);
}

@DataProvider(name = "unsupported")
public static Object[][] unsupported() {
return new Object[][]{
{"2023-05-11T09:15:19"},
{"qwe"}
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.beust.jcommander.converters;

import com.beust.jcommander.ParameterException;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

import java.time.LocalTime;
import java.time.OffsetTime;
import java.time.ZoneOffset;

import static org.testng.Assert.assertEquals;

public class OffsetTimeConverterTest {

private final OffsetTimeConverter converter = new OffsetTimeConverter("-p");

@Test(dataProvider = "supported")
public void supportedFormats_ShouldConvert(String value) {
OffsetTime actual = converter.convert(value);
OffsetTime expected = OffsetTime.of(LocalTime.of(10, 11, 0, 0), ZoneOffset.UTC);
assertEquals(actual, expected, "Incorrectly parsed offset time");
}

@DataProvider(name = "supported")
public static Object[][] supported() {
return new Object[][]{
{"10:11:00.000+00:00"},
{"10:11+00:00"}
};
}

@Test(dataProvider = "unsupported", expectedExceptions = ParameterException.class)
public void unsupportedFormats_ShouldThrowException(String value) {
converter.convert(value);
}

@DataProvider(name = "unsupported")
public static Object[][] unsupported() {
return new Object[][]{
{"10-11"},
{"10:11"},
{"qwe"}
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.beust.jcommander.converters;

import com.beust.jcommander.ParameterException;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

import java.time.LocalDate;
import java.time.LocalTime;
import java.time.Month;
import java.time.ZoneId;
import java.time.ZonedDateTime;

import static org.testng.Assert.assertEquals;

public class ZonedDateTimeConverterTest {

private final ZonedDateTimeConverter converter = new ZonedDateTimeConverter("-p");

@Test(dataProvider = "supported")
public void supportedFormats_ShouldConvert(String value, String expectedZoneId) {
ZonedDateTime actual = converter.convert(value);
ZonedDateTime expected = ZonedDateTime.of(LocalDate.of(2023, Month.MAY, 11), LocalTime.of(10, 15, 0, 0), ZoneId.of(expectedZoneId));
assertEquals(actual, expected, "Incorrectly parsed zoned date time");
}

@DataProvider(name = "supported")
public static Object[][] supported() {
return new Object[][]{
{"2023-05-11T10:15:00+00:00[UTC]", "UTC"},
{"2023-05-11T10:15:00.000+00:00[GMT]", "GMT"},
{"2023-05-11T10:15Z", "Z"}
};
}

@Test(dataProvider = "unsupported", expectedExceptions = ParameterException.class)
public void unsupportedFormats_ShouldThrowException(String value) {
converter.convert(value);
}

@DataProvider(name = "unsupported")
public static Object[][] unsupported() {
return new Object[][]{
{"2023-05-11T10:15"},
{"qwe"}
};
}
}