5

I am trying to replace org.joda.time.Period with java.time.

We have the following values stored in DB.

P1M, P1Y, P1D, PT1H, PT1M

Just to parse this value:

Period monthly = ISOPeriodFormat.standard().parsePeriod(<One of the above value from DB>);

This is simple and getting Period as expected. But, now replacing with java.time is troublesome.

Because, P1D, P1M, P1Y should be parsed using the below code:

java.time.Period period = java.time.Period.parse("P1M");

And, P1H and P1D should be parsed using the below code.

Duration dailyD = Duration.parse("P1D");

So, I might need some string check also like:

if(value.startsWith("PT")) {
   // Use java.time.Duration
} else {
   // Use java.time.Period
}

Is there any better code for this logic?

Also, Finally, I will have to find the number of occurances from some startTime to till date based on the above java.time.Period or java.time.Duration.

Like, if startDateTime is 01/01/2015 08:30:

LocalDateTime startDateTime = // the above startDateTime ..

    if(value.startsWith("PT")) {
       // Use java.time.Duration
     ChronoUnit.SECONDS.between(startDateTime,curentDate)/(duration.toMillis()/1000)
    } else {

 if(value.endsWith("Y")) {
       // Use java.time.Period
ChronoUnit.YEARS.between(startDateTime,curentDate)/(period.getYears()/1000)
} else if(value.endsWith("M")){
 ChronoUnit.MONTHS.between(startDateTime,curentDate)/(period.getMonths()/1000)
}

Is there any other better way without this value parsing?

My input could have P2M, P10M, P2Y, PT15M, PT10M. It won't have the combination of both Period and time like P1MT10M. But any number of months, years or days possible.

halfer
  • 19,824
  • 17
  • 99
  • 186
user1578872
  • 7,808
  • 29
  • 108
  • 206
  • Is that useful for you? https://github.com/meetup/timeywimey/blob/master/timeywimey-core/src/main/java/com/meetup/timeywimey/JodaConverters.java – Lefteris Bab Nov 10 '17 at 07:01
  • I don’t think you meant to divide by 1000 the last two times you did it in the last code snippet, with the years and with the months. – Ole V.V. Nov 10 '17 at 07:43
  • You may want to look into [`org.threeten.extra.PeriodDuration`](http://www.threeten.org/threeten-extra/apidocs/org/threeten/extra/PeriodDuration.html) in the ThreeTen Extra project. – Ole V.V. Nov 10 '17 at 08:58
  • 1
    I think (like you?) I would prefer to leave all parsing to the library methods. One option is to try parsing both as a `Period` and as a `Duration` and take the one that works. – Ole V.V. Nov 10 '17 at 08:59
  • 1
    Are you sure your period is always a `Period` or a `Duration`? Or put the opposite way, could something like `P1MT1H` be present in your data? – Ole V.V. Nov 10 '17 at 09:01
  • If you parse to a `java.time.Duration` then the original information if it was one day or 86400 seconds is lost because that type only stores seconds and nanoseconds. And about `java.time.Period`, it automatically convert a week to seven days which might not be desirable for you. IMHO I would avoid the conversion because the `java.time`-types are too limited here, compared with JodaTime. – Meno Hochschild Nov 10 '17 at 09:30

1 Answers1

3

Java-8 does not have such a complex duration type like the class org.joda.time.Period. But you can create your own implementation of the interface TemporalAmount in a straight-forward way:

import java.time.DateTimeException;    
import java.time.temporal.ChronoUnit;
import java.time.temporal.Temporal;
import java.time.temporal.TemporalAmount;
import java.time.temporal.TemporalUnit;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import static java.time.temporal.ChronoUnit.*;

public class SimpleDuration implements TemporalAmount {

    private static final List<TemporalUnit> SUPPORTED_UNITS =
        Collections.unmodifiableList(Arrays.asList(YEARS, MONTHS, DAYS, HOURS, MINUTES));

    private final int amount;
    private final ChronoUnit unit;

    private SimpleDuration(int amount, ChronoUnit unit) {
        this.amount = amount;
        this.unit = unit;
    }

    public static SimpleDuration of(int amount, ChronoUnit unit) {
        if (SUPPORTED_UNITS.contains(unit)) {
            return new SimpleDuration(amount, unit);
        } else {
            throw new IllegalArgumentException("Not supported: " + unit);
        }
    }

    @Override
    public long get(TemporalUnit unit) {
        if (this.unit.equals(unit)) {
          return this.amount;
        }
        return 0;
    }

    @Override
    public List<TemporalUnit> getUnits() {
        return SUPPORTED_UNITS;
    }

    @Override
    public Temporal addTo(Temporal temporal) {
        return temporal.plus(this.amount, this.unit);
    }

    @Override
    public Temporal subtractFrom(Temporal temporal) {
        return temporal.minus(this.amount, this.unit);
    }

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof SimpleDuration) {
            SimpleDuration that = (SimpleDuration) obj;
            return this.unit == that.unit && this.amount == that.amount;
        } else {
            return false;
        }
    }

    @Override
    public int hashCode() {
        return this.unit.hashCode() + 37 * Integer.hashCode(this.amount);
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        if (this.amount < 0) {
            sb.append('-');
        }
        sb.append('P');
        if (this.unit.isTimeBased()) {
            sb.append('T');
        }
        sb.append(Math.abs(this.amount));
        switch (this.unit) {

            case YEARS :
                sb.append('Y');
                break;

            case MONTHS :
                sb.append('M');
                break;

            case DAYS :
                sb.append('D');
                break;

            case HOURS :
                sb.append('H');
                break;

            case MINUTES :
                sb.append('M');
                break;

            default :
                throw new UnsupportedOperationException(this.unit.name());
        }
        return sb.toString();
    }

    public static SimpleDuration parse(String input) {
        int len = input.length();
        int index = 0;
        boolean negative = false;
        if (len > 0 && input.charAt(0) == '-') {
            negative = true; // for XML-Schema (not for ISO-8601)
            index++;
        }
        if (len >= 3 && input.charAt(index) == 'P') {
            boolean timeBased = (input.charAt(index + 1) == 'T');
            char last = input.charAt(len - 1);
            ChronoUnit unit;
            switch (last) {

                case 'Y' :
                    unit = YEARS;
                    break;

                case 'M' :
                    unit = (timeBased ? MINUTES : MONTHS);
                    break;

                case 'D' :
                    unit = DAYS;
                    break;

                case 'H' :
                    unit = HOURS;
                    break;

                default :
                    throw new DateTimeException(
                        "Unknown unit symbol found at last position: " + input
                    );
            }
            if (unit.isTimeBased() != timeBased) {
                throw new DateTimeException("Invalid XML-Schema-format: " + input);
            }
            try {
              int amount =
                Integer.parseInt(
                  input.substring(index).substring(timeBased ? 2 : 1, len - 1 - index));
              if (amount < 0) {
                throw new DateTimeException(
                  "XML-Schema does not allow negative amounts inside: " + input);
              }
              return SimpleDuration.of(negative ? -amount : amount, unit);
            } catch (NumberFormatException ex) {
                throw new DateTimeException("Invalid XML-Schema-format: " + input, ex);
            }
        }
        throw new DateTimeException("Cannot parse: " + input);
    }

    public static void main(String... args) {
        System.out.println(parse("-PT10M")); // -PT10M
    }
}
Meno Hochschild
  • 42,708
  • 7
  • 104
  • 126
  • Thanks for the input. But, my input could have P2M, P10M, P2Y, PT15M, PT10M. It wont have the combination of both Period and time like P1MT10M. But any number of months, years or days possible. – user1578872 Nov 10 '17 at 14:47
  • @user1578872 Ah okay, that is something you have told us differently in your original question. I will soon adjust my answer for your new requirement. – Meno Hochschild Nov 10 '17 at 14:56
  • @user1578872 Have now adjusted my answer. The suggested code covers a simplified variant of XML-schema suitable for the representations you mentioned inclusive a possible leading minus sign for negative durations. – Meno Hochschild Nov 10 '17 at 15:33
  • Awesome. Hope, ChronoException can be replaced with RuntimeException as I dont want to import net.time4j.engine. – user1578872 Nov 10 '17 at 16:18
  • This doesnt work as expected. SimpleDuration simpleDuration = SimpleDuration.parse("PT10M"); // It returns 10 as opposed to return 0 System.out.println("YEARS "+simpleDuration.get(ChronoUnit.YEARS)); – user1578872 Nov 10 '17 at 16:36
  • @user1578872 I have fixed the import for the exception class (a faux-pas because I rather love to work with my own time library). About your counter-example: Sorry, I have now fixed the getter-code. – Meno Hochschild Nov 10 '17 at 16:59
  • Nice work but I wonder if it wouldn't make more sense to copy the code from threetenextra. – assylias Nov 10 '17 at 19:57
  • @assylias Why from Threeten-Extra? ;-) I have exercise by implementing a much more featured `net.time4j.Duration`-class in my lib Time4J where I had also to write an [internal implementation](http://time4j.net/javadoc-en/net/time4j/Duration.html#toTemporalAmount--) of `TemporalAmount` for conversion purposes. And the sign-handling here is different (more suitable for XML-schema). – Meno Hochschild Nov 10 '17 at 22:41
  • I'm not questioning your ability to do it properly, I'm just saying that if the user only needs a PeriodDuration class, there is one in threetenextra that's been tested and can be copied/pasted as is. – assylias Nov 11 '17 at 10:37
  • 2
    @assylias `PeriodDuration`of Threeten-extra does not the same (not suitable for XML-schema in case of negative durations), and `net.time4j.Duration` has even different arithmetic behaviour for negative durations and has extra formatting capabilities (missing in threeten-extra) so we talk here about apples and oranges, not really comparable or exchangeable classes representing a temporal amount. And the OP has explicitly asked for a Java-8-solution, not for 3rd-party-libraries. – Meno Hochschild Nov 11 '17 at 15:42