Parsing time strings like "1h 30min"
Asked Answered
R

8

47

Anyone know of a Java library that can parse time strings such as "30min" or "2h 15min" or "2d 15h 30min" as milliseconds (or some kind of Duration object). Can Joda-Time do something like this?

(I have an ugly long method to maintain that does such parsing and would like to get rid of it / replace it with something that does a better job.)

Rundell answered 19/6, 2011 at 17:9 Comment(2)
I've seen this "Xd Yh Zm" or "Xd Yh Zmin" referred to as "JIRA notation", but I don't know if that term is widespread.Rundell
Related Gist in Kotlin: gist.github.com/OndraZizka/5fd56479ed2f6175703eb8a2e1bb1088Kettering
V
36

You'll probably have to tweak this a bit for your own format, but try something along these lines:

PeriodFormatter formatter = new PeriodFormatterBuilder()
    .appendDays().appendSuffix("d ")
    .appendHours().appendSuffix("h ")
    .appendMinutes().appendSuffix("min")
    .toFormatter();

Period p = formatter.parsePeriod("2d 5h 30min");

note that there is a appendSuffix that takes a variants parameter if you need to make it more flexible.

Update: Joda Time has since added Period.toStandardDuration(), and from there you can use getStandardSeconds() to get the elapsed time in seconds as a long.

If you're using an older version without these methods you can still calculate a timestamp yourself by assuming the standard 24/hr in a day, 60min/hr, etc. (In this case, take advantage of the constants in the DateTimeConstants class to avoid the need for magic numbers.)

Virulence answered 19/6, 2011 at 17:29 Comment(2)
Thanks! With little tweaks I get this to do mostly what I want, and it is quite a bit nicer than the 150-line regex / state machine monster I was confronted with. (Btw, for the Period -> millis conversion, Joda's DateTimeConstants contains useful constants such as MILLIS_PER_DAY.)Rundell
For one approach of using java.time to handle the exact format mentioned in the question, see this answer. Though I wonder if there's anything similar to PeriodFormatterBuilder in java.time, for a similar, simpler solution as in this answer.Rundell
R
36

Duration parsing is now included in Java 8. Use standard ISO 8601 format with Duration.parse.

Duration d = Duration.parse("PT1H30M")

You can convert this duration to the total length in milliseconds. Beware that Duration has a resolution of nanoseconds, so you may have data loss going from nanoseconds to milliseconds.

long milliseconds = d.toMillis();

The format is slightly different than what you describe but could be easily translated from one to another.

Resolute answered 17/3, 2014 at 15:22 Comment(5)
This also adds the benefit of supporting ISO8601 standard for duration. The ISO string must match the form P[yY][mM][dD][T[hH][mM][s[.s]S]]. It uses standard Java classes in Java 8 and there are also backports for Java 6 & 7 see: threeten.org/threetenbpNonresistance
I'm not sure it supports the whole ISO8601 standard, specifically a string with weeks: P2W for instance is not parsable, but I believe is valid per the standard. The docs for Duration say: "Obtains a Duration from a text string such as PnDTnHnMn.nS."Vinegarette
How does this help when the question was about 'such as "30min" or "2h 15min" or "2d 15h 30min"'?Empoverish
... It seems to work for some examples... too bad it does not for the example mentioned on the wikipedia article for ISO 8601 !... ( "P3Y6M4DT12H30M5S") - (Java.io.Duration version55.0, Java 11 )Criminology
Worth mentioning: if you are working with time periods such as "P3Y6M4D", where the actual value added to a point in time depends on the point in time (so that adding 1 month would yield the same date in the next month instead of a standard amount of days), you can use Period.parse instead. But then you don't get support for smaller time fields than days.Receivable
S
15

I wanted to make the day, hour and minute optional and this seems to work to do that. Note that the appendSuffix() calls do not have a space after the character.

Using Joda 2.3.

PeriodParser parser = new PeriodFormatterBuilder()
        .appendDays().appendSuffix("d").appendSeparatorIfFieldsAfter(" ")
        .appendHours().appendSuffix("h").appendSeparatorIfFieldsAfter(" ")
        .appendMinutes().appendSuffix("min")
        .toParser();

The above code passes these tests.

@Test
public void testConvert() {
    DurationConverter c = new DurationConverter();

    Duration d;
    Duration expected;

    d = c.convert("1d");
    expected = Duration.ZERO
            .withDurationAdded(Duration.standardDays(1),1);
    assertEquals(d, expected);

    d = c.convert("1d 1h 1min");
    expected = Duration.ZERO
            .withDurationAdded(Duration.standardDays(1),1)
            .withDurationAdded(Duration.standardHours(1),1)
            .withDurationAdded(Duration.standardMinutes(1),1);
    assertEquals(d, expected);


    d = c.convert("1h 1min");
    expected = Duration.ZERO
            .withDurationAdded(Duration.standardHours(1),1)
            .withDurationAdded(Duration.standardMinutes(1),1);
    assertEquals(d, expected);

    d = c.convert("1h");
    expected = Duration.ZERO
            .withDurationAdded(Duration.standardHours(1),1);
    assertEquals(d, expected);

    d = c.convert("1min");
    expected = Duration.ZERO
            .withDurationAdded(Duration.standardMinutes(1),1);
    assertEquals(d, expected);

}
Squadron answered 7/2, 2014 at 15:23 Comment(3)
PeriodParser is primarily meant as an internal interface. PeriodFormatter is the user-facing API.Chiton
where is DurationConverter defined?Luthuli
Although I originally thought this useful, It doesn't correctly handle invalid input. Or at least it may, but that would be in the implementation of DurationConverter which we can't see.Blau
D
8

FYI, Just wrote this for hour+ periods, only uses java.time.*, pretty simple to understand and customize for any need;

This version works with strings like; 3d12h, 2y, 9m10d, etc.

import java.time.Duration;
import java.time.Instant;
import java.time.Period;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.Locale;
private static final Pattern periodPattern = Pattern.compile("([0-9]+)([hdwmy])");

public static Long parsePeriod(String period){
    if(period == null) return null;
    period = period.toLowerCase(Locale.ENGLISH);
    Matcher matcher = periodPattern.matcher(period);
    Instant instant=Instant.EPOCH;
    while(matcher.find()){
        int num = Integer.parseInt(matcher.group(1));
        String typ = matcher.group(2);
        switch (typ) {
            case "h":
                instant=instant.plus(Duration.ofHours(num));
                break;
            case "d":
                instant=instant.plus(Duration.ofDays(num));
                break;
            case "w":
                instant=instant.plus(Period.ofWeeks(num));
                break;
            case "m":
                instant=instant.plus(Period.ofMonths(num));
                break;
            case "y":
                instant=instant.plus(Period.ofYears(num));
                break;
        }
    }
    return instant.toEpochMilli();
}

Doty answered 31/5, 2019 at 13:25 Comment(3)
Yeah, nice answerBaguette
Yes: Need to mention that DateTimeFormatter doesn't apply on TemporalAmount, so it's not implemented in java.time API, and the standard Duration parsing is not configurable (yet) Also Duration implementation rely on this pattern (final private not modifiable ...) : ([-+]?)P(?:([-+]?[0-9]+)D)?(T(?:([-+]?[0-9]+)H)?(?:([-+]?[0-9]+)M)?(?:([-+]?[0-9]+)(?:[.,]([0-9]{0,9}))?S)?)?Hawthorne
The code needs more work. Buggy. Parsing "1m" throws "Unsupported unit: Months" in Java8+. There is no error detection, no validation. Parsing "10sec" happily returns 0. Parsing "foo bar" happily returns 0.Calista
P
2

java.time

Quoted below is a notice from the home page of Joda-Time:

Note that from Java SE 8 onwards, users are asked to migrate to java.time (JSR-310) - a core part of the JDK which replaces this project.

Solution using java.time, the modern Date-Time API:

You can convert the input string into ISO_8601#Duration format and then parse the same into java.time.Duration which was introduced with Java-8 as part of JSR-310 implementation.

Demo:

import java.time.Duration;
import java.util.stream.Stream;

public class Main {
    public static void main(String[] args) {
        // Test
        Stream.of(
                "30min", 
                "2h 15min", 
                "2d 15h 30min"
        ).forEach(s -> System.out.println(s + " => " + toMillis(s) + "ms"));
    }

    static long toMillis(String strDuration) {
        strDuration = strDuration.replaceAll("\\s+", "").replaceFirst("(\\d+d)", "P$1T");
        strDuration = strDuration.charAt(0) != 'P' ? "PT" + strDuration.replace("min", "m")
                : strDuration.replace("min", "m");
        Duration duration = Duration.parse(strDuration);
        return duration.toMillis();
    }
}

Output:

30min => 1800000ms
2h 15min => 8100000ms
2d 15h 30min => 228600000ms

ONLINE DEMO

Learn more about the modern Date-Time API* from Trail: Date Time.


* If you are working for an Android project and your Android API level is still not compliant with Java-8, check Java 8+ APIs available through desugaring. Note that Android 8.0 Oreo already provides support for java.time.

Phenomenon answered 29/10, 2021 at 18:45 Comment(0)
B
0

No, Joda defaults to taking only Durations, Instant intervals, and objects. For the latter it accepts things like Dates or SOAP ISO format. You can add you own converter here for the Duration class, and admittedly that would hide all your ugly code.

Bituminous answered 19/6, 2011 at 17:22 Comment(0)
A
0

I realise this thread is a few years old but it keeps popping up at the top of a Google search so I thought we'd share this.

We needed this capability as well on a couple of projects, so we created this small library for it: https://github.com/blueanvil/kotlin-duration-string

It's written in/for Kotlin, but you can use it from Java code as well (see JavaTest.java):

Duration duration = toDuration("24d 3h 45m");
...
toHumanReadableString(duration);
Aric answered 2/8, 2020 at 14:48 Comment(0)
G
0

Not exactly Java - rather Kotlin, and not using Joda but JDK's java.time:

val RELAXED_FORMATTER = DateTimeFormatter.ofPattern("yyyy[-MM[-dd[' 'HH:mm[:ss[.SSS]]]]]")

fun parseTemporalInput(input: String): LocalDateTime? {
    var result = LocalDateTime.MAX.withNano(0)

    if (input.lowercase() == "now")
        return LocalDateTime.now()

    try {
        val parsed = RELAXED_FORMATTER.parse(input)
        for (field in listOf(YEAR, MONTH_OF_YEAR, DAY_OF_MONTH, HOUR_OF_DAY, MINUTE_OF_HOUR, SECOND_OF_MINUTE)) {
            try {
                result = result.with(field, parsed.getLong(field))
            } catch (ex: UnsupportedTemporalTypeException) {
                result = result.with(field, if (field.isDateBased) 1 else 0)
            }
        }
        return result
    } 
    catch (parseEx: DateTimeParseException) {
        try {
            val inputToIso8601 = "P" + input.uppercase().replace("-","").replace(" ", "").replace("D", "DT").removeSuffix("T")
            // Expected format:  "PnDTnHnMn.nS"
            val duration = Duration.parse(inputToIso8601)

            val base = LocalDateTime.now().let {
                if (!inputToIso8601.contains("D")) it
                else it.truncatedTo(ChronoUnit.DAYS)
            }

            return base.minus(duration)
        }
        catch (ex: DateTimeParseException) {
            return null
        }
    }
    return null
}
  • Only supports days (since the underlying ISO 8601 does not standardize weeks, months etc.)
  • Does not handle relative in the future, although easy to add.
  • Is not meant as a full-blown implementation of duration parsing, so take it as that.

Taken from this Gist: https://gist.github.com/OndraZizka/5fd56479ed2f6175703eb8a2e1bb1088

Gorman answered 29/10, 2021 at 17:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.