Reputation: 81771
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
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
Reputation: 79115
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
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
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
}
Taken from this Gist: https://gist.github.com/OndraZizka/5fd56479ed2f6175703eb8a2e1bb1088
Upvotes: 0
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
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
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
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
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