Jonik
Jonik

Reputation: 81771

Parsing time strings like "1h 30min"

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.)

Upvotes: 48

Views: 41127

Answers (8)

Cosmin Marginean
Cosmin Marginean

Reputation: 131

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.

Here's a complete example written in Kotlin


import java.time.Duration
import java.util.regex.Pattern


fun Duration.toHumanReadableString(): String {
    val duration = this.truncatedTo(ChronoUnit.MILLIS)

    if (duration == Duration.ZERO) {
        return "0ms"
    }

    val elements = listOf(
        "${duration.toDaysPart()}d",
        "${duration.toHoursPart()}h",
        "${duration.toMinutesPart()}m",
        "${duration.toSecondsPart()}s",
        "${duration.toMillisPart()}ms"
    )
    val start = elements.indexOfFirst { it[0] != '0' }
    val end = elements.indexOfLast { it[0] != '0' }
    return elements.subList(start, end + 1).joinToString(" ")
}


internal val REGEX_DURATION = Pattern.compile("""
    ^(
       (
         ((?<days>\d+)d)?(\s*)
         ((?<hours>(0*(2[0-3]|[0-1]?[0-9])))h)?(\s*)
         ((?<minutes>(0*([0-5]?[0-9])))m)?(\s*)
         ((?<seconds>(0*([0-5]?[0-9])))s)?(\s*)
         ((?<milliseconds>(0*([0-9]|[1-9][0-9]|[1-9][0-9][0-9])))ms)?
      )
    | (
         ((?<days2>\d+)d)
        |((?<hours2>\d+)h)
        |((?<minutes2>\d+)m)
        |((?<seconds2>\d+)s)
        |((?<milliseconds2>\d+)ms)
      )
    )$
""".replace("\\s+".toRegex(), "")
        .trim())

internal const val MS = 1L
internal const val SEC = 1000 * MS
internal const val MIN = 60 * SEC
internal const val HOUR = 60 * MIN
internal const val DAY = 24 * HOUR

internal val GROUP_UNIT_CONVERTERS = mapOf(
        "days" to DAY,
        "hours" to HOUR,
        "minutes" to MIN,
        "seconds" to SEC,
        "milliseconds" to MS,
        "days2" to DAY,
        "hours2" to HOUR,
        "minutes2" to MIN,
        "seconds2" to SEC,
        "milliseconds2" to MS
)

fun String.toDuration(): Duration? {
    val matcher = REGEX_DURATION.matcher(this.trim())
    return if (matcher.matches()) {

        var durationNs = 0L

        GROUP_UNIT_CONVERTERS.forEach { (groupName, multiplier) ->
            val amount = matcher.group(groupName)?.toLong()
            if (amount != null)
                durationNs += amount * multiplier
        }
        Duration.ofMillis(durationNs)
    } else {
        null
    }
}

Upvotes: 0

Arvind Kumar Avinash
Arvind Kumar Avinash

Reputation: 79115

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",
                "30sec",
                "2h 15min 10sec",
                "2day 15hour 30min",
                "2 days 15 hours 30 mins",
                "2 Days 15 Hours 30 Minutes",
                "2days 15hrs 30mins",
                "2 Hours 15 Minutes 10 Seconds"
        ).forEach(s -> System.out.println(s + " => " + toMillis(s) + "ms"));
    }

    static long toMillis(String strDuration) {
        strDuration = strDuration.toUpperCase()
                .replaceAll("\\s+", "")
                .replaceAll("DAYS?", "D")
                .replaceAll("(?:HOURS?)|(?:HRS?)", "H")
                .replaceAll("(?:MINUTES?)|(?:MINS?)", "M")
                .replaceAll("(?:SECONDS?)|(?:SECS?)", "S")
                .replaceAll("(\\d+D)", "P$1T");
        strDuration = strDuration.charAt(0) != 'P' ? "PT" + strDuration : strDuration;
        // System.out.println(strDuration);
        Duration duration = Duration.parse(strDuration);
        return duration.toMillis();
    }
}

Output:

30min => 1800000ms
2h 15min => 8100000ms
2d 15h 30min => 228600000ms
30sec => 30000ms
2h 15min 10sec => 8110000ms
2day 15hour 30min => 228600000ms
2 days 15 hours 30 mins => 228600000ms
2 Days 15 Hours 30 Minutes => 228600000ms
2days 15hrs 30mins => 228600000ms
2 Hours 15 Minutes 10 Seconds => 8110000ms

Online Demo

Regex 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.

Upvotes: 3

Ondra Žižka
Ondra Žižka

Reputation: 46836

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

Upvotes: 0

bekce
bekce

Reputation: 4310

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();
}

Upvotes: 8

Andrejs
Andrejs

Reputation: 27697

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.

Upvotes: 37

Victor
Victor

Reputation: 3475

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);

}

Upvotes: 15

Brad Mace
Brad Mace

Reputation: 27886

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.)

Upvotes: 37

MJB
MJB

Reputation: 9399

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.

Upvotes: 0

Related Questions