How to parse time based tokens in java ?(1m 1M 1d 1Y 1W 1S)
Asked Answered
L

8

7

I gets following strings from FE:

1m
5M
3D
30m
2h
1Y
3W

It corresponds to 1 minute, 5 months,3 days, 30 minutes, 2 hours, 1 year, 3 weeks.

Is there mean in java to parse it?

I want to manipulate(add/minus) with Instant(or LocalDatetTime). Is there way to do it in java?

Lazy answered 25/2, 2019 at 12:6 Comment(11)
Write your own class with all the fields mentioned and implement methods based on your requirement.Courante
You may want to look into java.time.Duration.parse(CharSequence) and java.time.Period.parse(CharSequence)Eventide
Are the 1m and 30m added together? Or was it a typo and one of the two is supposed to be s for seconds?Salvador
@Kevin Cruijssen, it is not a typo. We can get request to add 1 minute and some time later to reduce 30 minutesLazy
@ernest_k, good point but I have to add 'P' at the beginingLazy
Well, you cannot add or subtract weeks, months or years to or from an Instant.Visible
@Ole V.V., it works: Period period = Period.parse("P1W"); LocalDateTime localDateTime = LocalDateTime.now(); LocalDateTime result = localDateTime.minus(period);Lazy
@Lazy I just discovered that a period of weeks work with Instant too. Months and years don’t. All do with LocalDateTime.Visible
@Ole V.V. I am not sure I got what you meanLazy
@Lazy Instant.now().plus(Period.parse("P5M")) throws an UnsupportedTemporalTypeException.Visible
@Ole V.V. thx it is clear now. Good pointLazy
V
5

Period & Duration

I consider the following solution simple and pretty general (not fully general).

public static TemporalAmount parse(String feString) {
    if (Character.isUpperCase(feString.charAt(feString.length() - 1))) {
        return Period.parse("P" + feString);
    } else {
        return Duration.parse("PT" + feString);
    }
}

It seems that your date-based units (year, month, week, day) are denoted with uppercase abbreviations (Y, M, W and D) while the time-based ones (hour and minute) are lowercase (h and m). So I test the case of the last character of the string to decide whether to parse into a Period or a Duration. I exploit the fact that both of Period.parse and Duration.parse accept the letters in either case.

You wanted to add or subtract the durations to and from Instant or LocalDateTime. This works in most cases. Let’s see:

    String[] timeAmountStrings = { "1m", "5M", "3D", "30m", "2h", "1Y", "3W" };
    LocalDateTime base = LocalDateTime.of(2019, Month.MARCH, 1, 0, 0);
    for (String tas : timeAmountStrings) {
        TemporalAmount amount = parse(tas);
        System.out.println("String: " + tas + " parsed: " + amount + " added: " + base.plus(amount));

        try {
            System.out.println("Added to Instant: " + Instant.EPOCH.plus(amount));
        } catch (DateTimeException dte) {
            System.out.println("Adding to Instant didn’t work: " + tas + ' ' + dte);
        }

        System.out.println();
    }

Output:

String: 1m parsed: PT1M added: 2019-03-01T00:01
Added to Instant: 1970-01-01T00:01:00Z

String: 5M parsed: P5M added: 2019-08-01T00:00
Adding to Instant didn’t work: 5M java.time.temporal.UnsupportedTemporalTypeException: Unsupported unit: Months

String: 3D parsed: P3D added: 2019-03-04T00:00
Added to Instant: 1970-01-04T00:00:00Z

String: 30m parsed: PT30M added: 2019-03-01T00:30
Added to Instant: 1970-01-01T00:30:00Z

String: 2h parsed: PT2H added: 2019-03-01T02:00
Added to Instant: 1970-01-01T02:00:00Z

String: 1Y parsed: P1Y added: 2020-03-01T00:00
Adding to Instant didn’t work: 1Y java.time.temporal.UnsupportedTemporalTypeException: Unsupported unit: Years

String: 3W parsed: P21D added: 2019-03-22T00:00
Added to Instant: 1970-01-22T00:00:00Z

We see that adding to LocalDateTime works in all cases. Adding to Instant works in most cases, only we cannot add a period of months or years to it.

Visible answered 25/2, 2019 at 15:4 Comment(4)
So we can use LocalDateTime as a private object and convert to to instant in the last line of methodLazy
Yes, @gstackoverflow, if you know what time zone to use for the conversion. And if you do, it may be safer to use a ZonedDateTime.Visible
@ Ole V.V. I am curious to know why ZonedDateTime is safer than LocalDateTime? Because of providing timezone during conversion?Lazy
Good question, @gstackoverflow, thanks. Because of summer time (DST) and other anomalies, the clock may be turned forward or backward on a particular date. When adding or subtracting hours or minutes across that point in time, ZonedDateTime will give you the correct number of hours or minutes. LocalDateTime won’t know about the anomaly and will just add or subtract as though no adjustment of time had taken place and thus not give the correct result.Visible
R
4

The Answer by Ole V.V. is correct, clever, and well-done. I would take it one step further.

PeriodDuration

The ThreeTen-Extra project adds functionality to the java.time classes built into Java 8 and later. Among its offerings is the PeriodDuration class, combining a Period (years-month-days) with a Duration (hours-minutes-seconds-nanos).

By the way, let me caution you about combining the two concepts. While that may seem fine intuitively, if you ponder a bit you may see it as problematic depending on your business logic.

Anyways, let’s adapt the code seen in the other Answer.

  • If the input is uppercase, we create a string in standard ISO 8601 duration format, and parse it as a Period, adding that result to our PeriodDuration object.
  • If the input is lowercase, we create a string in standard ISO 8601 duration format, parse it as a Duration, and add the result to our PeriodDuration object.

Code:

List < String > inputs = List.of( "1m" , "5M" , "3D" , "30m" , "2h" , "1Y" , "3W" );
PeriodDuration pd = PeriodDuration.ZERO;
for ( String input : inputs )
{
    String s = input.trim();
    String lastLetter = s.substring( s.length() - 1 );
    int codepoint = Character.codePointAt( lastLetter , 0 );
    if ( Character.isUpperCase( codepoint ) )
    {
        String x = "P" + s;
        Period p = Period.parse( x );
        pd = pd.plus( p );
    } else
    { // Else, lowercase.
        String x = "PT".concat( s ).toUpperCase();
        Duration d = Duration.parse(x );
        pd = pd.plus( d );
    }
}
System.out.println( "pd.toString(): " + pd );

P1Y5M24DT2H31M

Righthanded answered 25/2, 2019 at 21:32 Comment(3)
Very good answer. "if you ponder a bit you may see it as problematic depending on your business logic." Could you explain this point please ?Rivas
@Rivas Well, the spinning of the earth has nothing to do with the earth’s orbit around the sun. The first we track by time-of-day (noon is when the sun is directly overhead). The second we track by calendar. Combining the two is messy and awkward. So we have Leap Year most every four years. If you apply your span-of-time over dates that do or don’t cross Leap Day, your calendar result will vary by a day. If you simultaneously care about hours-minutes-seconds, well, that seems kind of silly in the face of being off by an entire day/date. It all depends on the logic of your business problem.Righthanded
Makes sense. ThanksRivas
C
1

Since both years and minutes are included, I recommend you to use a pair of Duration and Period parsing as mentioned in the comment. Use the static methods Period::parse(CharSequence text) and Duration.parse(CharSequence text) with their default formats (PnDTnHnMn.nS for Duration and PnYnMnD for Period) since they don't provide a way to translate custom expression like that.

For this use case, you have to create your own mapping between your expression and the given format.

Chromo answered 25/2, 2019 at 12:48 Comment(0)
S
1

There isn't a direct parse, but you could make your own:

// TODO: Proper variable/method names, comments, logging, code format, etc.
java.time.LocalDateTime changeDateTimeByString(java.time.LocalDateTime current, String part,
                                               boolean increaseOrDecrease){
  java.time.LocalDateTime newDateTime = null;

  Integer amount = Integer.parseInt(part.replaceAll("\\D",""));
  if(part.contains("S"))
    newDateTime = increaseOrDecrease ? current.plusSeconds(amount) : current.minusSeconds(amount);
  else if(part.contains("m"))
    newDateTime = increaseOrDecrease ? current.plusMinutes(amount) : current.minusMinutes(amount);
  else if(part.contains("h"))
    newDateTime = increaseOrDecrease ? current.plusHours(amount) : current.minusHours(amount);
  else if(part.contains("D"))
    newDateTime = increaseOrDecrease ? current.plusDays(amount) : current.minusDays(amount);
  else if(part.contains("W"))
    newDateTime = increaseOrDecrease ? current.plusDays(amount * 7) : current.minusDays(amount * 7);
  else if(part.contains("M"))
    newDateTime = increaseOrDecrease ? current.plusMonths(amount) : current.minusMonths(amount);
  else if(part.contains("Y"))
    newDateTime = increaseOrDecrease ? current.plusYears(amount) : current.minusYears(amount);
  else
    System.err.println("Unknown format: " + part);

  System.out.println("New dateTime after "+(increaseOrDecrease?"adding":"removing")+" "+part+": "+newDateTime);
  return newDateTime;
}

The parameters of the method are a String as specified by you in the question; a boolean whether you want to add or move this; and the input-date you want to modify. It will then return the modified date (Java 8+ java.time.LocalDateTime is used in this case). I.e.: changeDateTimeByString(dateTime, "1m", true); will add 1 minute; changeDateTimeByString(dateTime, "2Y", false); will remove 2 years.

Try it online.

Salvador answered 25/2, 2019 at 13:28 Comment(0)
P
0

my guess is you should create your own parser method like this one using java.util.concurrent.TimeUnit and then you can use any framework to add seconds

private static long convertToSeconds(int value, char unit)
{
    long ret = value;
    switch (unit)
    {
    case 'd':
        ret = TimeUnit.DAYS.toSeconds(value);
        break;
    case 'h':
        ret = TimeUnit.HOURS.toSeconds(value);
        break;
    case 'm':
        ret = TimeUnit.MINUTES.toSeconds(value);
        break;
    case 's':
        break;
    default:
        // fail
        break;
    }
    return ret;
}
Physiological answered 25/2, 2019 at 12:27 Comment(1)
are you seeking advice how to solve it or complete solution? :) adding month or year makes no sense in my opinion, there is no standard size for it. Months have different length and there are also leap yearsPhysiological
N
0

As already suggested by @Achinta Jha, you can implement yourself a simple parsing logic, with something like

public static void main(String[] args) {
        final String[] inputs = {"1m", "5M", "3D"/*, "30m", "2h", "1Y", "3W"*/};

        Arrays.stream(inputs)
                .map(x -> Parse(x))
                .forEach(System.out::println);

    }

    private static final Map<Character, Function<String, Duration>> parsers = new HashMap<>();
    static {
        parsers.put('m', txt -> Duration.ofMinutes(Integer.parseInt(txt)));
        parsers.put('M', txt -> Duration.ofDays(30 * Integer.parseInt(txt)));
        parsers.put('D', txt -> Duration.ofDays(Integer.parseInt(txt)));
    }

    private static Duration Parse(String txt) {
        Character last = txt.charAt(txt.length() - 1);
        Function<String, Duration> parser = parsers.get(last);
        return parser.apply(txt.substring(0, txt.length() - 1));
    }

This is a simple, explanatory implementation, with neither error/nulls handling nor full support for the proposed inputs. Note that Duration does not support a static ofMonths method: I supposed you intended 30 days, but is up to you to adjust the code to your desired semantic.

In order to add/subtract Durations you can use non static methods of Duration class.

Nitriding answered 25/2, 2019 at 12:29 Comment(0)
R
0

Spring already does this but not with all the same options as you. Their implementation is

private static Duration simpleParse(String rawTime) {
    if (rawTime == null || rawTime.isEmpty())
        return null;
    if (!Character.isDigit(rawTime.charAt(0)))
        return null;

    String time = rawTime.toLowerCase();
    return tryParse(time, "ns", Duration::ofNanos)
            .orElseGet(() -> tryParse(time, "ms", Duration::ofMillis).orElseGet(
                    () -> tryParse(time, "s", Duration::ofSeconds).orElseGet(
                            () -> tryParse(time, "m", Duration::ofMinutes).orElseGet(
                                    () -> tryParse(time, "h", Duration::ofHours)
                                            .orElseGet(() -> tryParse(time, "d",
                                                    Duration::ofDays)
                                                            .orElse(null))))));
}

private static Optional<Duration> tryParse(String time, String unit,
        Function<Long, Duration> toDuration) {
    if (time.endsWith(unit)) {
        String trim = time.substring(0, time.lastIndexOf(unit)).trim();
        try {
            return Optional.of(toDuration.apply(Long.parseLong(trim)));
        }
        catch (NumberFormatException ignore) {
            return Optional.empty();
        }
    }
    return Optional.empty();
}
Racon answered 25/2, 2019 at 12:39 Comment(0)
Q
0

For Spring Boot's users:

You can use the converter StringToDurationConverter in order to parse simple inputs like 5s, 12h, 1d and so.

In its implementation, we can find the DurationStyle class.

Duration parsed = DurationStyle.detectAndParse("12h");

Also, we can see some additional examples in the unit tests.

Be aware, complex inputs are not supported!

Quaff answered 10/8, 2022 at 16:58 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.