All Downloads are FREE. Search and download functionalities are using the official Maven repository.

jfxtras.icalendarfx.utilities.DateTimeUtilities Maven / Gradle / Ivy

There is a newer version: 17-r1
Show newest version
package jfxtras.icalendarfx.utilities;

import static java.time.temporal.ChronoField.DAY_OF_MONTH;
import static java.time.temporal.ChronoField.HOUR_OF_DAY;
import static java.time.temporal.ChronoField.MINUTE_OF_HOUR;
import static java.time.temporal.ChronoField.MONTH_OF_YEAR;
import static java.time.temporal.ChronoField.SECOND_OF_MINUTE;
import static java.time.temporal.ChronoField.YEAR;

import java.time.DateTimeException;
import java.time.DayOfWeek;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.Period;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.SignStyle;
import java.time.temporal.ChronoUnit;
import java.time.temporal.Temporal;
import java.time.temporal.TemporalAdjusters;
import java.time.temporal.TemporalAmount;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

import jfxtras.icalendarfx.components.VEvent;
import jfxtras.icalendarfx.parameters.VParameterElement;
import jfxtras.icalendarfx.properties.component.time.TimeTransparency.TimeTransparencyType;
import jfxtras.icalendarfx.utilities.DateTimeUtilities;
import jfxtras.icalendarfx.utilities.ICalendarUtilities;
import jfxtras.icalendarfx.utilities.Pair;


/**
 * Temporal date and date-time types and supporting methods for iCalendar.
 *  DATE
 *  DATE_WITH_LOCAL_TIME
 *  DATE_WITH_LOCAL_TIME_AND_TIME_ZONE
 *  DATE_WITH_UTC_TIME:
 * see iCalendar RFC 5545, page 32-33
 *
 * includes methods to format a Temporal representing a DateTimeType as a String
 * 
 * @author David Bal
 *
 */
public final class DateTimeUtilities
{
    private DateTimeUtilities() { }
    
    private static final ZoneId DEFAULT_ZONE = ZoneId.systemDefault(); // default ZoneId to convert to
    /** 
     * Default DateTimeType to use when none is specified.  For example, when a date-only component is converted to
     * a date-time one.
     */
    public static final DateTimeType DEFAULT_DATE_TIME_TYPE = DateTimeType.DATE_WITH_UTC_TIME;

    public final static DateTimeFormatter LOCAL_DATE_FORMATTER = new DateTimeFormatterBuilder()
            .appendValue(YEAR, 4, 10, SignStyle.EXCEEDS_PAD)
            .appendValue(MONTH_OF_YEAR, 2)
            .appendValue(DAY_OF_MONTH, 2)
            .toFormatter();
    final static DateTimeFormatter LOCAL_TIME_FORMATTER = new DateTimeFormatterBuilder()
            .appendValue(HOUR_OF_DAY, 2)
            .appendValue(MINUTE_OF_HOUR, 2)
            .appendValue(SECOND_OF_MINUTE, 2)
            .toFormatter();
    public final static DateTimeFormatter LOCAL_DATE_TIME_FORMATTER = new DateTimeFormatterBuilder()
            .parseCaseInsensitive()
            .append(LOCAL_DATE_FORMATTER)
            .appendLiteral('T')
            .append(LOCAL_TIME_FORMATTER)
            .toFormatter();
    public final static DateTimeFormatter ZONED_DATE_TIME_UTC_FORMATTER = new DateTimeFormatterBuilder()
            .append(LOCAL_DATE_TIME_FORMATTER)
            .appendOffsetId()
            .toFormatter();
//    @Deprecated
    public final static DateTimeFormatter ZONED_DATE_TIME_FORMATTER = new DateTimeFormatterBuilder()
            .optionalStart()
            .appendLiteral('[')
            .parseCaseInsensitive()
            .appendZoneRegionId()
            .appendLiteral(']')
            .optionalEnd()
            .append(LOCAL_DATE_TIME_FORMATTER)
            .optionalStart()
            .appendOffsetId()            
            .optionalEnd()
            .toFormatter();
    final static DateTimeFormatter ZONE_FORMATTER = new DateTimeFormatterBuilder()
            .optionalStart()
            .parseCaseInsensitive()
            .appendLiteral("TZID=")
            .appendZoneRegionId()
            .optionalEnd()
            .toFormatter();
    
    /** Compares two temporals of the same type */
    public final static Comparator TEMPORAL_COMPARATOR = (t1, t2) -> 
    {
        if (t1.getClass().equals(t2.getClass()))
        {
            return getTemporalComparator(t1).compare(t1, t2);
        } else
        {
            throw new DateTimeException("For comparision, Temporal classes must be equal (" + t1.getClass().getSimpleName() + ", " + t2.getClass().getSimpleName() + ")");
        }
    };
    
    /** Compares two temporals of the LocalDate, LocalDateTime and ZonedDateTime
     * Makes LocalDate at start of day.  Add system default ZoneId to LocalDate and LocalDateTime */
    public final static Comparator TEMPORAL_COMPARATOR2 = (t1, t2) -> 
    {
        ZonedDateTime z1 = (ZonedDateTime) DateTimeType.DATE_WITH_UTC_TIME.from(t1);
        ZonedDateTime z2 = (ZonedDateTime) DateTimeType.DATE_WITH_UTC_TIME.from(t2);
        return z1.compareTo(z2);
    };
    
    /** Returns correct comparator based on Temporal parameter */
    public final static Comparator getTemporalComparator(Temporal t) 
    {
        if (t instanceof LocalDate)
        {
            return (t1, t2) -> ((LocalDate) t1).compareTo((LocalDate) t2);
        } else if (t instanceof LocalDateTime)
        {
            return (t1, t2) -> ((LocalDateTime) t1).compareTo((LocalDateTime) t2);            
        } else if (t instanceof ZonedDateTime)
        {
            return (t1, t2) -> ((ZonedDateTime) t1).compareTo((ZonedDateTime) t2);
        } else
        {
            throw new DateTimeException("Unsupported Temporal type:" + t.getClass().getSimpleName());
        }
    }
    
    /** Determines if Temporal is before t2
     * Works for LocalDate, LocalDateTime and ZonedDateTime
     * 
     * @param t1 first Temporal
     * @param t2 second Temporal (to compare with t1)
     * @return true if t1 is before t2, false otherwise
     */
    public static boolean isBefore(Temporal t1, Temporal t2)
    {
        if (t1.getClass().equals(t2.getClass()))
        {
            if (t1 instanceof LocalDate)
            {
                return ((LocalDate) t1).isBefore((LocalDate) t2);
            } else if (t1 instanceof LocalDateTime)
            {
                return ((LocalDateTime) t1).isBefore((LocalDateTime) t2);
            } else if (t1 instanceof ZonedDateTime)
            {
                return ((ZonedDateTime) t1).isBefore((ZonedDateTime) t2);
            } else
            {
                throw new DateTimeException("Unsupported Temporal class: " + t1.getClass());
            }
        }
        throw new DateTimeException("For comparision, Temporal classes must be equal (" + t1.getClass().getSimpleName() + ", " + t2.getClass().getSimpleName() + ")");
    }

    /** Determines if Temporal is after t2
     * Works for LocalDate, LocalDateTime and ZonedDateTime
     * 
     * @param t1 first Temporal
     * @param t2 second Temporal (to compare with t1)
     * @return true if t1 is after t2, false otherwise
     */
    public static boolean isAfter(Temporal t1, Temporal t2)
    {
        if (t1.getClass().equals(t2.getClass()))
        {
            if (t1 instanceof LocalDate)
            {
                return ((LocalDate) t1).isAfter((LocalDate) t2);
            } else if (t1 instanceof LocalDateTime)
            {
                return ((LocalDateTime) t1).isAfter((LocalDateTime) t2);
            } else if (t1 instanceof ZonedDateTime)
            {
                return ((ZonedDateTime) t1).isAfter((ZonedDateTime) t2);
            } else
            {
                throw new DateTimeException("Unsupported Temporal class: " + t1.getClass());
            }
        } throw new DateTimeException("For comparision, Temporal classes must be equal (" + t1.getClass().getSimpleName() + ", " + t2.getClass().getSimpleName() + ")");
    }
    
    private final static int CONFLICT_CHECK_QUANTITY = 400;
    /** Check if schedule conflict exists for {@link TimeTransparencyType.OPAQUE OPAQUE} events 
     * using default check quantity
     * 
     * @param vEvent  event to test
     * @param vEvents  existing events
     * @return  UID and start of recurrence of conflicting event, null otherwise
     */
    public static String checkScheduleConflict(VEvent vEvent, List vEvents)
    {
        return checkScheduleConflict(vEvent, vEvents, CONFLICT_CHECK_QUANTITY);
    }
    
    /** Check if schedule conflict exists for {@link TimeTransparencyType.OPAQUE OPAQUE} events.
     * 
     * @param vEvent  event to test
     * @param vEvents  existing events
     * @param checkQuantity  amount of recurrences to be tested
     * @return  UID and start of recurrence of conflicting event, null otherwise
     */
    // TODO - Should this method compare FreeBusy times too?
    public static String checkScheduleConflict(VEvent vEvent, List vEvents, int checkQuantity)
    {
        // must be opaque to cause conflict, opaque is default
        TimeTransparencyType newTransparency = (vEvent.getTimeTransparency() == null) ? TimeTransparencyType.OPAQUE : vEvent.getTimeTransparency().getValue();
        if (newTransparency == TimeTransparencyType.TRANSPARENT)
        {
            return null;
        }
        
        /*
         * Make list of recurrence start date/times for VEvent to be tested
         */
        LocalDate dtstart = LocalDate.from(vEvent.getDateTimeStart().getValue());
        TemporalAmount duration = vEvent.getActualDuration();
        List newStarts = vEvent.streamRecurrences().limit(checkQuantity).collect(Collectors.toList());
        Temporal lastStart = newStarts.get(newStarts.size()-1);

        /*
         * Make sorted list of events before lastStart from all VEvents
         */
        List eventTimes = vEvents.stream()
            .filter(v -> 
            { // only keep OPAQUE events
                TimeTransparencyType myTransparency = (v.getTimeTransparency() == null) ? TimeTransparencyType.OPAQUE : v.getTimeTransparency().getValue();
                return myTransparency == TimeTransparencyType.OPAQUE;
            })
            .flatMap(v ->
            {
                TemporalAmount actualDuration = v.getActualDuration();
                Temporal myDTStart = v.getDateTimeStart().getValue().with(dtstart);
                return v.streamRecurrences(myDTStart)
                    .limit(checkQuantity)
                    .filter(t -> ! DateTimeUtilities.isAfter(t, lastStart))
                    .map(t -> 
                    {
                        String uid = (v.getUniqueIdentifier() != null) ? v.getUniqueIdentifier().getValue() : null;
                        return new Triple(t, t.plus(actualDuration), uid);
                    });
            })
            .sorted((t1, t2) -> DateTimeUtilities.TEMPORAL_COMPARATOR2.compare(t1.start, t2.start))
            .collect(Collectors.toList());

        /*
         *  Search for a conflict
         *  1.  New start before existing end, new end after existing start
         */
        for (Temporal newStart : newStarts)
        {
            Temporal newEnd = newStart.plus(duration);
            Triple firstConflict = eventTimes.stream()
                .filter(triple ->
                {
                    boolean newStartBeforeEnd = DateTimeUtilities.isBefore(newStart, triple.end);
                    boolean newEndAfterStart = DateTimeUtilities.isAfter(newEnd, triple.start);
                    return (newStartBeforeEnd && newEndAfterStart);
                })
                .findAny()
                .orElseGet(() -> null);
            if (firstConflict != null)
            {
                String uid = (firstConflict.uid != null) ? firstConflict.uid + ", " : "";
                return (firstConflict == null) ? null : uid + DateTimeUtilities.temporalToString(firstConflict.start);
            }
        }
        return null; // no conflicts found
    }
    // Triple class for checkScheduleConflict
    private static class Triple
    {
        public Triple(Temporal start, Temporal end, String uid)
        {
            this.start = start;
            this.end = end;
            this.uid = uid;
        }
        Temporal start;
        Temporal end;
        String uid;
    }
    
//    /** Check if schedule conflict exists for {@link TimeTransparencyType.OPAQUE OPAQUE} events.
//     * Finds conflict faster, but not necessarily the first conflict that occurs
//     * 
//     * @param vEvent  event to test
//     * @param vEvents  existing events
//     * @param checkQuantity  amount of recurrences to be tested
//     * @return  UID and start of recurrence of conflicting event, null otherwise
//     */
//    // TODO - DOESN'T WORK, FIX IF WANTED
//    public static String checkScheduleConflict2(VEvent vEvent, List vEvents, int checkQuantity)
//    {
//        // must be opaque to cause conflict, opaque is default
//        TimeTransparencyType newTransparency = (vEvent.getTimeTransparency() == null) ? TimeTransparencyType.OPAQUE : vEvent.getTimeTransparency().getValue();
//        if (newTransparency == TimeTransparencyType.TRANSPARENT)
//        {
//            return null;
//        }
//        
//        LocalDate dtstart = LocalDate.from(vEvent.getDateTimeStart().getValue());
//        TemporalAmount duration = vEvent.getActualDuration();
//        List newStarts = vEvent.streamRecurrences().limit(checkQuantity).collect(Collectors.toList());
//        Temporal lastStart = newStarts.get(newStarts.size()-1);
//                   
//        /*
//         *  Search for a conflict
//         *  Note: The first conflict found may not be the first occurring conflict.
//         *  Make list of Pairs containing start and end temporals
//         */
//        for (VEvent v : vEvents)
//        {
//            // can only conflict with opaque events, opaque is default
//            TimeTransparencyType myTransparency = (v.getTimeTransparency() == null) ? TimeTransparencyType.OPAQUE : v.getTimeTransparency().getValue();
//            if (myTransparency == TimeTransparencyType.OPAQUE)
//            {
//                List> eventTimes = new ArrayList<>();
//                Temporal myDTStart = v.getDateTimeStart().getValue().with(dtstart);
//                TemporalAmount actualDuration = v.getActualDuration();
//                Iterator existingStartIterator = v.streamRecurrences(myDTStart).limit(checkQuantity).iterator();
//                while (existingStartIterator.hasNext())
//                {
//                    Temporal t = existingStartIterator.next();
//                    if (DateTimeUtilities.isAfter(t, lastStart)) break; // quit adding if after last new start temporal
//                    eventTimes.add(new Pair<>(t, t.plus(actualDuration)));
//                }
//                
//                for (Temporal newStart : newStarts)
//                {
//                    Temporal newEnd = newStart.plus(duration);
//                    Pair conflict = eventTimes.stream()
//                        .filter(p ->
//                        {
//                            Temporal existingStart = p.getKey();
//                            Temporal existingEnd = p.getValue();
//                            // test start
//                            boolean isOnOrAfter = ! DateTimeUtilities.isBefore(newStart, existingStart);
//                            if (isOnOrAfter)
//                            {
//                                return ! DateTimeUtilities.isAfter(newStart, existingEnd);
//                            }
//                            // test end
//                            boolean isAfter = DateTimeUtilities.isAfter(newEnd, existingStart);
//                            if (isAfter)
//                            {
//                                return DateTimeUtilities.isBefore(newEnd, existingEnd);
//                            }
//                            return false;
//                        })
//                        .findAny()
//                        .orElseGet(() -> null);
//                    if (conflict != null)
//                    {
//                        String uid = (v.getUniqueIdentifier() != null) ? v.getUniqueIdentifier().getValue() + ", " : "";
//                        return (conflict == null) ? null : uid + conflict.getKey();
//                    }
//                }
//            }
//        }
//        return null; // no conflicts found
//    }
    
    /**
     * returns week of month.
     * For example, a LocalDate representing March 10, 2016 returns 2, for the 2nd Thursday.
     * 
     * @param dateBasedTemporal - date based Temporal, such as LocalDate
     * @return - ordinal week in month, such as 2nd (as in 2nd Wednesday in month)
     */
    public static int weekOrdinalInMonth(Temporal dateBasedTemporal)
    {
        Temporal start = dateBasedTemporal.with(TemporalAdjusters.firstDayOfMonth());
        int ordinalWeekNumber = 0;
        while (! DateTimeUtilities.isBefore(dateBasedTemporal, start))
        {
            ordinalWeekNumber++;
            start = start.plus(1, ChronoUnit.WEEKS);
        }
        return ordinalWeekNumber;
    }
    
    /**
     * Calculate TemporalAmount between two Temporals.
     * Both temporals must be the same type and representations of a DateTimeType.
     * 
     * @param startInclusive - the start temporal, not null
     * @param endExclusive - the end temporal, not null
     * @return - Period for DATE, Duration for all other DateTimeTypes
     */
    public static TemporalAmount temporalAmountBetween(Temporal startInclusive, Temporal endExclusive)
    {
        final TemporalAmount duration;
        if (startInclusive instanceof LocalDate || endExclusive instanceof LocalDate)
        {
            duration = Period.between(LocalDate.from(startInclusive), LocalDate.from(endExclusive));
        } else
        {
            duration = Duration.between(startInclusive, endExclusive);
        }
        return duration;
    }
    
    /**
     * produced ISO.8601 date and date-time string for given Temporal of type
     * LocalDate, LocalDateTime or ZonedDateTime
     * 
     * @param temporal
     * @return
     */
    public static String temporalToString(Temporal temporal)
    {
        if (temporal instanceof ZonedDateTime)
        {
            ZonedDateTime value = (ZonedDateTime) temporal;
            ZoneId z = value.getZone();
            if (z.normalized().equals(ZoneOffset.UTC))
            {
                return DateTimeUtilities.ZONED_DATE_TIME_UTC_FORMATTER.format(value);
            } else
            {
                return DateTimeUtilities.LOCAL_DATE_TIME_FORMATTER.format(value); // Time zone is added through TimeZoneIdentifier parameter
            }
        } else if (temporal instanceof LocalDateTime)
        {
            LocalDateTime value = (LocalDateTime) temporal;
            return DateTimeUtilities.LOCAL_DATE_TIME_FORMATTER.format(value);
        } else if (temporal instanceof LocalDate)
        {
            return DateTimeUtilities.LOCAL_DATE_FORMATTER.format(temporal);
        } else if (temporal != null)
        {
            throw new DateTimeException("Unsuported Date-Time class:" + temporal.getClass().getSimpleName());
        }
        return null;
    }
    
    /**
     * Parse ISO.8601 string into LocalDate, LocalDateTime or ZonedDateTime Temporal object.
     */ 
    public static Temporal temporalFromString(String string)
    {
        List> list = ICalendarUtilities.parseInlineElementsToListPair(string);
        Map map = list.stream().collect(Collectors.toMap(p -> p.getKey(), p -> p.getValue()));
//        Map map = ICalendarUtilities.propertyLineToParameterMap(string);
        StringBuilder builder = new StringBuilder(50);
        String value = map.get(ICalendarUtilities.PROPERTY_VALUE_KEY);
        if (map.get(VParameterElement.TIME_ZONE_IDENTIFIER.toString()) != null)
        {
            if (value.charAt(value.length()-1) != 'Z')
            {
                builder.append("[");
                builder.append(map.get(VParameterElement.TIME_ZONE_IDENTIFIER.toString()));
                builder.append("]");
            }
        }
        builder.append(value);
        String string2 = builder.toString();

        final String form0 = "^[0-9]{8}";
        final String form1 = "^[0-9]{8}T([0-9]{6})";
        final String form2 = "^[0-9]{8}T([0-9]{6})Z";
        final String form3 = "^(\\[.*/.*\\])[0-9]{8}T([0-9]{6}Z?)";
        if (string2.matches(form0))
        {
            return LocalDate.parse(string2, DateTimeUtilities.LOCAL_DATE_FORMATTER);                                                
        } else if (string2.matches(form1))
        {
            return LocalDateTime.parse(string2, DateTimeUtilities.LOCAL_DATE_TIME_FORMATTER);                                                
        } else if (string2.matches(form2))
        {
            return ZonedDateTime.parse(string2, DateTimeUtilities.ZONED_DATE_TIME_UTC_FORMATTER);                                                
        } else if (string2.matches(form3))
        {
            return ZonedDateTime.parse(string2, DateTimeUtilities.ZONED_DATE_TIME_FORMATTER);                                                
        } else
        {
            throw new DateTimeException("Can't parse date-time string:" + string);                        
        }
    }
    
    
    /**
     * Convert 2-character string to DayOfWeek following below convention:
     * weekday     = "SU" / "MO" / "TU" / "WE" / "TH" / "FR" / "SA"
     * Corresponding to SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, and SATURDAY days of the week.
     * 
     * @param dayOfWeekAbbreviation - 2 character abbreviation representing day of the week
     * @return - associated DayOfWeek
     */
    public static DayOfWeek dayOfWeekFromAbbreviation(String dayOfWeekAbbreviation)
    {
        Optional optional = Arrays.stream(DayOfWeek.values())
                .filter(d -> d.toString().substring(0, 2).equals(dayOfWeekAbbreviation.toUpperCase()))
                .findAny();
        return (optional.isPresent()) ? optional.get() : null;
            
    }
    
    /**
     * Returns LocalDateTime from Temporal that is an instance of either LocalDate, LocalDateTime or ZonedDateTime
     * If the parameter is type LocalDate the returned LocalDateTime is atStartofDay.
     * If the parameter is type ZonedDateTime the zoneID is changed to ZoneId.systemDefault() before taking the
     * LocalDateTime.
     */
    @Deprecated
    public static LocalDateTime toLocalDateTime(Temporal temporal)
    {
        return (LocalDateTime) DateTimeType.DATE_WITH_LOCAL_TIME.from(temporal);
    }
    
    public enum DateTimeType
    {
//        DATE ("^(VALUE=DATE:)?[0-9]{8}")
        DATE ("^[0-9]{8}")
        {
            @Override
            public Temporal from(Temporal temporal, ZoneId zone)
            {
                return this.from(temporal);
            }
    
            @Override
            public Temporal from(Temporal temporal)
            {
                switch(DateTimeType.of(temporal))
                {
                case DATE:
                    return temporal; // do nothing
                case DATE_WITH_LOCAL_TIME:
                    return LocalDate.from(temporal);
                case DATE_WITH_LOCAL_TIME_AND_TIME_ZONE:
                case DATE_WITH_UTC_TIME:
                    return ZonedDateTime.from(temporal).withZoneSameInstant(DEFAULT_ZONE).toLocalDate();
                default:
                    throw new DateTimeException("Unsupported Temporal class:" + temporal.getClass().getSimpleName());
                }
            }

            @Override
            boolean is(Temporal temporal)
            {
                return temporal instanceof LocalDate;
            }

            @Override
            public Temporal parse(String temporalString, ZoneId zone)
            {
                if (temporalString.matches(getPattern()))
                {
                    return LocalDate.parse(temporalString, LOCAL_DATE_FORMATTER);                    
                }
                return null;
            }
        }
      , DATE_WITH_LOCAL_TIME ("^[0-9]{8}T([0-9]{6})")
        {
            @Override
            public Temporal from(Temporal temporal, ZoneId zone)
            {
                return this.from(temporal);
            }
    
            @Override
            public Temporal from(Temporal temporal)
            {
                switch(DateTimeType.of(temporal))
                {
                case DATE:
                    return LocalDate.from(temporal).atStartOfDay();
                case DATE_WITH_LOCAL_TIME:
                    return temporal;  // do nothing
                case DATE_WITH_LOCAL_TIME_AND_TIME_ZONE:
                case DATE_WITH_UTC_TIME:
                    return ZonedDateTime.from(temporal).withZoneSameInstant(DEFAULT_ZONE).toLocalDateTime();
                default:
                    throw new DateTimeException("Unsupported Temporal class:" + temporal.getClass().getSimpleName());
                }
            }

            @Override
            boolean is(Temporal temporal)
            {
                return temporal instanceof LocalDateTime;
            }

            @Override
            public Temporal parse(String temporalString, ZoneId zone)
            {
                boolean isTzidEmpty = zone == null;
                boolean isPatternMatch = temporalString.matches(getPattern());
                if (isTzidEmpty && isPatternMatch)
                {
                    return LocalDateTime.parse(temporalString, LOCAL_DATE_TIME_FORMATTER);
                }
                return null;
            }
        }
      , DATE_WITH_UTC_TIME ("^[0-9]{8}T([0-9]{6})Z")
        {
            @Override
            public Temporal from(Temporal temporal, ZoneId zone)
            {
                return this.from(temporal);
            }
    
            @Override
            public Temporal from(Temporal temporal)
            {
                switch(DateTimeType.of(temporal))
                {
                case DATE:
                    return LocalDate.from(temporal).atStartOfDay().atZone(ZoneId.of("Z"));
                case DATE_WITH_LOCAL_TIME:
                    return LocalDateTime.from(temporal).atZone(DEFAULT_ZONE).withZoneSameInstant(ZoneId.of("Z"));
                case DATE_WITH_LOCAL_TIME_AND_TIME_ZONE:
                    return ZonedDateTime.from(temporal).withZoneSameInstant(ZoneId.of("Z"));
                case DATE_WITH_UTC_TIME:
                    return temporal;  // do nothing
                default:
                    throw new DateTimeException("Unsupported Temporal class:" + temporal.getClass().getSimpleName());
                }
            }

            @Override
            boolean is(Temporal temporal)
            {
                if (temporal instanceof ZonedDateTime)
                {
                    ZoneId z = ((ZonedDateTime) temporal).getZone();
                    return z == ZoneId.of("Z");
                }
                return false;
            }

            @Override
            public Temporal parse(String temporalString, ZoneId zone)
            {
                boolean isPatternMatch = temporalString.matches(getPattern());
                if (isPatternMatch)
                {
                    return ZonedDateTime.parse(temporalString, ZONED_DATE_TIME_UTC_FORMATTER);
                }
                return null;
            }
        }
      , DATE_WITH_LOCAL_TIME_AND_TIME_ZONE ("^[0-9]{8}T([0-9]{6})Z?")
        {
            @Override
            public Temporal from(Temporal temporal, ZoneId zone)
            {
                switch(DateTimeType.of(temporal))
                {
                case DATE:
                    return LocalDate.from(temporal).atStartOfDay().atZone(zone);
                case DATE_WITH_LOCAL_TIME:
                    return LocalDateTime.from(temporal).atZone(zone);
                case DATE_WITH_LOCAL_TIME_AND_TIME_ZONE:
                case DATE_WITH_UTC_TIME:
                    return ZonedDateTime.from(temporal).withZoneSameInstant(zone);
                default:
                    throw new DateTimeException("Unsupported Temporal class:" + temporal.getClass().getSimpleName());
                }
            }
    
            @Override
            public Temporal from(Temporal temporal)
            {
                throw new DateTimeException("Can't make DATE_WITH_LOCAL_TIME_AND_TIME_ZONE without time zone.  Use from(Temporal temporal, ZoneId zone) instead");
            }

            @Override
            boolean is(Temporal temporal)
            {
                return temporal instanceof ZonedDateTime;
            }

            @Override
            public Temporal parse(String temporalString, ZoneId zone)
            {
                boolean isTzidEmpty = zone == null;
                boolean isPatternMatch = temporalString.matches(getPattern());
                if (! isTzidEmpty && isPatternMatch)
                {
                    LocalDateTime localDateTime = LocalDateTime.parse(temporalString, LOCAL_DATE_TIME_FORMATTER);
                    return localDateTime.atZone(zone);
                }
                return null;
            }
        };
        
        private String pattern;
        public String getPattern() { return pattern; }

        DateTimeType(String pattern)
        {
            this.pattern = pattern;
        }

        /** Find DateTimeType of Temporal parameter temporal */
        public static DateTimeType of(Temporal temporal)
        {
            return Arrays.stream(DateTimeType.values())
                    .filter(d -> d.is(temporal))
                    .findFirst()
                    .get();
        }
        
        /**
         * returns true if temporal is of the DateTimeType, false otherwise
         */
        abstract boolean is(Temporal temporal);
        
        public abstract Temporal parse(String value, ZoneId zone);
        
        /** Convert temporal to new DateTimeType - for DATE_WITH_LOCAL_TIME_AND_TIME_ZONE */
        public abstract Temporal from(Temporal temporal, ZoneId zone);
        
        /** Convert temporal to new DateTimeType  - for all types, but DATE_WITH_LOCAL_TIME_AND_TIME_ZONE */
        public abstract Temporal from(Temporal temporal);
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy