I would like to parse any duration/period string literal (in the format shown in the examples below) into a Period/Duration instance in Java.
By duration/period, I mean an amount of time that contains both date-based and time-based quantities. In Java, Duration alone only works for time-based quantities like seconds, minutes, and hours, while Period alone only works for date-based quantities like years, months, and days.
The string to parse won't always contain all quantities.
Examples:
29 days 23 hours 59 minutes 20 seconds
4 years 10 months 10 seconds
10 days 9 minutes
1 month
How would I do that, if possible?
Convert your string to ISO 8601 format and parse into PeriodDuration
My idea is to use the PeriodDuration class of the ThreeTen Extra project. It combines the Period and Duration classes that you already mention. A limitation is that its parse method only accepts ISO 8601 format, so we need to convert your string into that format first. ISO 8601 format is like P10DT9M for 10 days 9 minutes. The P is fixed. The T marks the beginning of the time-based part if there is one (hours, minutes, seconds) to separate it from the date-based part (years, months, weeks, days). A nice consequence is that we can always tell whether M means months or minutes: If M comes before T or there is no T, it means months. If M comes after T, it means minutes. My first code snippet converts your string to ISO 8601.
String[] examples = {
"29 days 23 hours 59 minutes 20 seconds",
"4 years 10 months 10 seconds",
"10 days 9 minutes",
"1 month"
};
for (String pds : examples) {
// If hours, minutes or seconds are present, put a T before them
String temp = pds.replaceFirst("(\\d+ *(?:hour|minute|second))", " T $1");
// Abbreviate all units to 1 letter; remove spaces
temp = temp.replaceAll("([ymwdhms])[a-z]*", "$1").replace(" ", "");
// Prepend P
String iso = "P" + temp;
System.out.println(iso);
}
Output so far is:
P29dT23h59m20s
P4y10mT10s
P10dT9m
P1m
After this we should be able to parse:
PeriodDuration pd = PeriodDuration.parse(iso);
I have not installed ThreeTen Extra, so I have not tried this last line before posting it. The OP reported in a comment that it works like a charm on ThreeTen Extra 1.6.0. The only caveat is that the units have to be in descending order when parsing the string, otherwise it throws a DateTimeParseException.
Don’t worry that ISO 8601 format is usually given in upper case and your units are in lower case. The documentation of the parse method states:
The sections have suffixes in ASCII of "Y" for years, "M" for months,
"W" for weeks, "D" for days, "H" for hours, "M" for minutes, "S" for
seconds, accepted in upper or lower case.
Edit: With thanks to Arvind Kumar Avinash for the comment, if you need to accept upper case letters too, add the (?i) flag expression in your regular expression to enable case-insensitive matching:
temp = temp.replaceAll("(?i)([ymwdhms])[a-z]*", "$1").replace(" ", "");
Links
ThreeTen Extra project home
Documentation of PeriodDuration.parse()
Wikipedia article: ISO 8601
Related
Part of my current project is to convert mm:ss to seconds...but the user has the option to enter x:xx or xx:xx.
For example, if someone wanted to enter one minute and thirty seconds into the program, they have the option to write it as either "01:30" or "1:30". And the output of both would be 90 seconds.
This is my current code.
System.out.print("Time (mm:ss): ")
String time = scan.nextLine();
int min = Integer.parseInt(time.substring(0, time.indexOf(':'))) * 60;
int sec = Integer.parseInt(time.substring(3, time.length()));
int duration = (min + sec);
System.out.println("Seconds: " + duration)
It works whenever I enter xx:xx, but fails when I enter x:xx.
I am not sure how to only read the characters after ":" . If I start the substring at ":" (I have it at 3 now), it can't convert to int because it reads the ":".
I have looked all over Google and my textbook, but have not found anything. I assume I am just using the wrong technique. The code needs to stay within the parameters of basic beginner String methods. Thank you!
This answer probably does not stay within the parameters of basic beginner String methods as requested. I think it will be useful for other readers of your question who don’t have the same limitation.
java.time.Duration
The Duration class is the class to use for an amount of time like 1 minute 30 seconds. Unfortunately, the Duration class can only parse strings in ISO 8601 format (link below), but the string conversion isn’t hard with a regular expression. And fortunately in ISO 8601 format leading zeroes don’t matter. The Duration class is part of java.time, the modern Java date and time API.
ISO 8601 format for a duration is like PT01M30S. Think of it as a period of time of 01 minute 30 seconds. If the format feels unusual at first, it is straightforward. So let’s convert to it. The following method accepts your user’s format, converts and returns a Duration.
public static Duration parseDuration(String durStr) {
String isoString = durStr.replaceFirst("^(\\d{1,2}):(\\d{2})$", "PT$1M$2S");
return Duration.parse(isoString);
}
Duration has a toSeconds method for converting to seconds. So let’s try the whole thing out:
System.out.println(parseDuration("01:30").toSeconds());
System.out.println(parseDuration("1:30").toSeconds());
Output is the expected:
90
90
Consider whether you need to convert to seconds at all, though. Keeping the Duration objects as they are will probably make your code more self-explanatory.
Links
Oracle tutorial: Date Time explaining how to use java.time.
Wikipedia article: ISO 8601
int sec = Integer.parseInt(time.substring(time.indexOf(':') + 1), time.length()));
Thanks guys!
Here I have simplified your code.
String time = "1:30";
String[] timeUnit = time.split(":");
int totalSeconds = 60 * Integer.parseInt(timeUnit[0]) + Integer.parseInt(timeUnit[1]);
System.out.println("Seconds "+totalSeconds);
I am trying to use the Duration class instead of long.
It has superior literal syntax. I like its flexibility, though it looks weird.
"PT10S" means 10 seconds, what is the problem to accept "10 seconds"?!
Okay never mind.
I am just curious why PT prefix has been chosen (not "DU" e.g.) and why any prefix is better here rather than nothing?
As can be found on the page Jesper linked to (ISO-8601 - Data elements and interchange formats – Information interchange – Representation of dates and times)
P is the duration designator (for period) placed at the start of the duration representation.
Y is the year designator that follows the value for the number of years.
M is the month designator that follows the value for the number of months.
W is the week designator that follows the value for the number of weeks.
D is the day designator that follows the value for the number of days.
T is the time designator that precedes the time components of the representation.
So P means 'Period' and because there are no date-components it only has a 'Time'.
You could interpret this as 'Period of Time'
The 'why' this was chosen, you have to ask the ISO members that wrote the standard, but my guess is that it is easier to parse. (short and unambigious)
The details for the time component are:
H is the hour designator that follows the value for the number of hours.
M is the minute designator that follows the value for the number of minutes.
S is the second designator that follows the value for the number of seconds.
The value of PT20S then parses to:
Period
Time
20
Seconds
So, a duration of 20 seconds.
More examples can be found in the javadoc: https://docs.oracle.com/javase/8/docs/api/java/time/Duration.html#parse-java.lang.CharSequence-
Java has taken a subset of the ISO 8601 standard format for a duration. So the “why” is why the standard was written the way it is, and it’s a guessing game. My go is:
P for period was chosen so that you can distinguish a duration from a date and/or time. Especially since a period may also be written in the same format as a local date-time, for example P0003-06-04T12:30:05 for 3 years 6 months 4 days 12 hours 30 minutes 5 seconds, the P can be necessary to distinguish. The P also gives a little but quick and convenient bit of validation in case you happen to pass a completely different string in a place where a duration was expected. And yes, PT10S looks weird, but once you get accustomed to it, you recognize it immediately as a duration, which can be practical.
T for time between the date part and the time part was chosen for two reasons:
For consistency with date-time strings that have T in the same place, for example 2018-07-04T15:00 for July 4, 2018 at 15:00 hours.
To disambiguate the otherwise ambiguous M for either months or minutes: P3M unambiguously means 3 months while PT3M means 3 minutes.
Actually if go on Duration API developed in Java since 1.8, they have gone with standard ISO 8601:
with java doc as below :
/**
* Applies an ISO 8601 Duration to a {#link ZonedDateTime}.
*
* <p>Since the JDK defined different types for the different parts of a Duration
* specification, this utility method is needed when a full Duration is to be applied to a
* {#link ZonedDateTime}. See {#link Period} and {#link Duration}.
*
* <p>All date-based parts of a Duration specification (Year, Month, Day or Week) are parsed
* using {#link Period#parse(CharSequence)} and added to the time. The remaining parts (Hour,
* Minute, Second) are parsed using {#link Duration#parse(CharSequence)} and added to the time.
*
* #param time A zoned date time to apply the offset to
* #param offset The offset in ISO 8601 Duration format
* #return A zoned date time with the offset applied
*/
public static ZonedDateTime addOffset(ZonedDateTime time, String offset) { }
Obtains a Duration from a text string of pattern: PnDTnHnMn.nS, where
nD = number of days,
nH = number of hours,
nM = number of minutes,
n.nS = number of seconds, the decimal point may be either a dot or a comma.
T = must be used before the part consisting of nH, nM, n.nS
Example of implementation with java as
import java.time.Duration;
public class ParseExample {
public static void main(String... args) {
parse("PT20S");//T must be at the beginning to time part
parse("P2D");//2 day
parse("-P2D");//minus 2 days
parse("P-2DT-20S");//S for seconds
parse("PT20H");//H for hours
parse("PT220H");
parse("PT20M");//M for minutes
parse("PT20.3S");//second can be in fraction like 20.3
parse("P4DT12H20M20.3S");
parse("P-4DT-12H-20M-20.3S");
parse("-P4DT12H20M20.3S");
}
private static void parse(String pattern) {
Duration d = Duration.parse(pattern);
System.out.println("Pattern: %s => %s%n", pattern, d);
}
}
Am using joda-time-2.5 in Android Studio project.
I am not able to work out what I missing to be able to correctly format a String with years and/or months.
The Period calculates correctly - but will not go beyond "Weeks" eg. 1000000 minutes is correctly formatted to "99wks, 1day, 10hrs + 40mins". But not as months/years format eg. "1year, 10months, 3weeks, 1day, 10hrs + 40mins" etc
I have tried all kinds of variations of
Period pA = new Period(mA);
Period pA = new Period(mA, PeriodType.standard());
Period pA = new Period(mA, PeriodType.yearMonthDay());
etc but these made no difference.
I have tried adding/removing various .appends/years/months/printZero - this made no difference.
I have tried changing the period units : if I use Months or Years it will work eg
Months mA = Months.months(15);
Period pA = new Period(mA, PeriodType.standard());
Correctly produces "1year, 3months".
I understand that 'years' and 'months' are not precise (and approximate would actually be fine in this case), but I thought that's what the PeriodTypes/yearMonthDay or standard took care of?
I also tried PeriodFormat.getDefault().print(period) without success.
Please find code below:
private String formatTimeStr(int minutes){
Minutes mA = Minutes.minutes(minutes);
Period pA = new Period(mA);
PeriodFormatter dhm = new PeriodFormatterBuilder()
.printZeroNever()
.appendYears()
.appendSuffix("year","years")
.appendSeparator(", ")
.appendMonths()
.appendSuffix("mnth", "mnths")
.appendSeparator(", ")
.appendWeeks()
.appendSuffix("wk", "wks")
.appendSeparator(", ")
.appendDays()
.appendSuffix("day", "days")
.appendSeparator(", ")
.appendHours()
.appendSuffix("hr", "hrs")
.appendSeparator(" & ")
.appendMinutes()
.appendSuffix("min", "mins")
.toFormatter();
String formattedTimeStr = dhm.print(pA.normalizedStandard());
return formattedTimeStr;
}
Fact is as you already have recognized that minutes are not convertible to months in a strict sense. The Joda-Time-documentation speaks it out, too.
If the period contains years or months, then the months will be
normalized to be between 0 and 11. The days field and below will be
normalized as necessary, however this will not overflow into the
months field. Thus a period of 1 year 15 months will normalize to 2
years 3 months. But a period of 1 month 40 days will remain as 1 month
40 days.
Technically, there are two options for conversion.
a) You define a reference timestamp. Then you can do following:
LocalDateTime tsp1 = new LocalDateTime(); // now as example
LocalDateTime tsp2 = tsp1.plusMinutes(minutes);
Period p = new Period(tsp1, tsp2, PeriodType.standard()); // or other period type?
b) You find and develop yourself a rounding algorithm based on the estimated length of time units. For example you might be willing to accept a rounded month length of 30.5 days or similar. This can be considered as fair solution if the use-case does not require absolute precision as this is often true in social media scenarios. As far as I know, Joda-Time does not support such a rounding feature out of the box (in contrast to other libraries).
I need to represent a time interval as localized string like this: 10 hours 25 minutes 1 second depending on Locale.
It is pretty easy to realize by hand in English:
String hourStr = hours == 1 ? "hour" : "hours" etc.
But I need some "out-of-the-box" Java (maybe Java8) mechanism according to rules of different languages.
Does Java have it, or I need to realize it for each Locale used in app by myself?
Look at Joda-Time. It supports the languages English, Danish, Dutch, French, German, Japanese, Polish, Portuguese and Spanish with version 2.5.
Period period = new Period(new LocalDate(2013, 4, 11), LocalDate.now());
PeriodFormatter formatter = PeriodFormat.wordBased(Locale.GERMANY);
System.out.println(formatter.print(period)); // output: 1 Jahr, 2 Monate und 3 Wochen
formatter = formatter.withLocale(Locale.ENGLISH);
System.out.println(formatter.print(period)); // output: 1 Jahr, 2 Monate und 3 Wochen (bug???)
formatter = PeriodFormat.wordBased(Locale.ENGLISH);
System.out.println(formatter.print(period)); // output: 1 year, 2 months and 3 weeks
You might to adjust the interpunctuation chars however. To do this you might need to copy and edit the messages-resource-files in your classpath which have this format (here english variant):
PeriodFormat.space=\
PeriodFormat.comma=,
PeriodFormat.commandand=,and
PeriodFormat.commaspaceand=, and
PeriodFormat.commaspace=,
PeriodFormat.spaceandspace=\ and
PeriodFormat.year=\ year
PeriodFormat.years=\ years
PeriodFormat.month=\ month
PeriodFormat.months=\ months
PeriodFormat.week=\ week
PeriodFormat.weeks=\ weeks
PeriodFormat.day=\ day
PeriodFormat.days=\ days
PeriodFormat.hour=\ hour
PeriodFormat.hours=\ hours
PeriodFormat.minute=\ minute
PeriodFormat.minutes=\ minutes
PeriodFormat.second=\ second
PeriodFormat.seconds=\ seconds
PeriodFormat.millisecond=\ millisecond
PeriodFormat.milliseconds=\ milliseconds
Since version 2.5 it might be also possible to apply complex regular expressions to model more complex plural rules. Personally I see it as user-unfriendly, and regular expressions might not be sufficient for languages like Arabic (my first impression). There are also other limitations with localization, see this pull request in debate.
Side notice: Java 8 is definitely not able to do localized duration formatting.
UPDATE from 2015-08-26:
With the version of my library Time4J-v4.3 (available in Maven Central) following more powerful solution is possible which supports currently 45 languages:
import static net.time4j.CalendarUnit.*;
import static net.time4j.ClockUnit.*;
// the input for creating the duration (in Joda-Time called Period)
IsoUnit[] units = {YEARS, MONTHS, DAYS, HOURS, MINUTES, SECONDS};
PlainTimestamp start = PlainDate.of(2013, 4, 11).atTime(13, 45, 21);
PlainTimestamp end = SystemClock.inLocalView().now();
// create the duration
Duration<?> duration = Duration.in(units).between(start, end);
// print the duration (here not abbreviated, but with full unit names)
String s = PrettyTime.of(Locale.US).print(duration, TextWidth.WIDE);
System.out.println(s);
// example output: 1 year, 5 months, 7 days, 3 hours, 25 minutes, and 49 seconds
Why is Time4J better for your problem?
It has a more expressive way to say in which units a duration should be calculated.
It supports 45 languages.
It supports the sometimes complex plural rules of languages inclusive right-to-left scripts like in Arabic without any need for manual configuration
It supports locale-dependent list patterns (usage of comma, space or words like "and")
It supports 3 different text widths: WIDE, ABBREVIATED (SHORT) and NARROW
The interoperability with Java-8 is better because Java-8-types like java.time.Period or java.time.Duration are understood by Time4J.
We have intervals (elapsed time between two oracle timestamps) stored in our database as seconds and we format them at front end with Java.
What we would to achieve on the reports is a format of the form "HH:MM" or "HH:MM:SS", with the time separator ":" localized as it happens with dates and time information, i.e '.' for Italian and ':' for English.
Unfortunately the date-related formatting classes, like SimpleDateFormat, do not work** because we can expect durations above the 24 hours.
We don't want to employ 3rdy party library as well.
Do you know how we can solve this problem?
TIA
If you want hours of more than 24 you can print this separately.
int hour = time / 3600000;
String duration = time + new SimpleDateFormat(":mm:ss").format(new Date(time));
To support other locales you could do this more complicated example.
int hour = time / 3600000;
String duration = hour
+ DateFormat.getDateInstance(DateFormat.MEDIUM, locale)
.format(new Date(time % 3600000).substring(1);
What this will do is use the locale specific format for the last digit of the hour + mins + secs and prepend the additional digits of the hours. Note: this will not work for negative times.
You can construct a date object, say "2000-01-01", then add your seconds to it. Then you can use this to extract the localized version of the time:
DateFormat format = DateFormat.getTimeInstance(DateFormat.MEDIUM, locale);
String formatted_time_part = format.format(some_date_object);
Finally you still need to add elapsed days! I'm affraid that localization of long intervals (days, months, years, centuries) has no corresponding API in Java but I might be wrong. So you will have to figure out that for yourself.