1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package dev.metaschema.core.metapath.item.atomic;
7   
8   import java.time.LocalDate;
9   import java.time.ZoneOffset;
10  import java.time.ZonedDateTime;
11  import java.time.temporal.ChronoUnit;
12  
13  import dev.metaschema.core.datatype.adapter.MetaschemaDataTypeProvider;
14  import dev.metaschema.core.datatype.object.AmbiguousDate;
15  import dev.metaschema.core.datatype.object.AmbiguousDateTime;
16  import dev.metaschema.core.metapath.function.DateTimeFunctionException;
17  import dev.metaschema.core.metapath.function.InvalidValueForCastFunctionException;
18  import dev.metaschema.core.metapath.item.atomic.impl.DateWithoutTimeZoneItemImpl;
19  import dev.metaschema.core.metapath.type.IAtomicOrUnionType;
20  import dev.metaschema.core.metapath.type.InvalidTypeMetapathException;
21  import dev.metaschema.core.util.ObjectUtils;
22  import edu.umd.cs.findbugs.annotations.NonNull;
23  import edu.umd.cs.findbugs.annotations.Nullable;
24  
25  /**
26   * An atomic Metapath item representing a date value in the Metapath system with
27   * or without an explicit time zone.
28   * <p>
29   * This interface provides functionality for handling date/time values with or
30   * without time zone information, supporting parsing, casting, and comparison
31   * operations. It works in conjunction with {@link ZonedDateTime} to handle time
32   * zone ambiguity.
33   */
34  public interface IDateItem extends ICalendarTemporalItem {
35    /**
36     * Get the type information for this item.
37     *
38     * @return the type information
39     */
40    @NonNull
41    static IAtomicOrUnionType<IDateItem> type() {
42      return MetaschemaDataTypeProvider.DATE.getItemType();
43    }
44  
45    @Override
46    default IAtomicOrUnionType<? extends IDateItem> getType() {
47      return type();
48    }
49  
50    /**
51     * Construct a new date item using the provided string {@code value}.
52     *
53     * @param value
54     *          a string representing a date
55     * @return the new item
56     */
57    @NonNull
58    static IDateItem valueOf(@NonNull String value) {
59      try {
60        return valueOf(MetaschemaDataTypeProvider.DATE.parse(value));
61      } catch (IllegalArgumentException ex) {
62        throw new InvalidTypeMetapathException(
63            null,
64            String.format("Invalid date value '%s'. %s",
65                value,
66                ex.getLocalizedMessage()),
67            ex);
68      }
69    }
70  
71    /**
72     * Construct a new date item using the provided {@code value}.
73     * <p>
74     * This method handles recording if an explicit timezone was provided using the
75     * {@code hasTimeZone} parameter. The {@link AmbiguousDate#hasTimeZone()} method
76     * can be called to determine if timezone information is present.
77     *
78     * @param value
79     *          a date, without time zone information
80     * @param hasTimeZone
81     *          {@code true} if the date/time is intended to have an associated time
82     *          zone or {@code false} otherwise
83     * @return the new item
84     * @see AmbiguousDateTime for more details on timezone handling
85     */
86    @NonNull
87    static IDateItem valueOf(@NonNull ZonedDateTime value, boolean hasTimeZone) {
88      ZonedDateTime truncated = ObjectUtils.notNull(value.truncatedTo(ChronoUnit.DAYS));
89      return hasTimeZone
90          ? IDateWithTimeZoneItem.valueOf(truncated)
91          : valueOf(new AmbiguousDate(truncated, false));
92    }
93  
94    /**
95     * Construct a new date item using the provided {@code value}.
96     * <p>
97     * This method handles recording that the timezone is implicit.
98     *
99     * @param value
100    *          a date, without time zone information
101    * @return the new item
102    * @see AmbiguousDateTime for more details on timezone handling
103    */
104   @NonNull
105   static IDateItem valueOf(@NonNull LocalDate value) {
106     return valueOf(ObjectUtils.notNull(value.atStartOfDay(ZoneOffset.UTC)), false);
107   }
108 
109   /**
110    * Construct a new date item using the provided {@code value}.
111    *
112    * @param value
113    *          an ambiguous date with time zone information already identified
114    * @return the new item
115    */
116   @NonNull
117   static IDateItem valueOf(@NonNull AmbiguousDate value) {
118     return value.hasTimeZone()
119         ? IDateWithTimeZoneItem.valueOf(value.getValue())
120         : new DateWithoutTimeZoneItemImpl(value);
121   }
122 
123   @Override
124   default boolean hasDate() {
125     return true;
126   }
127 
128   @Override
129   default boolean hasTime() {
130     return false;
131   }
132 
133   @Override
134   default int getYear() {
135     return asZonedDateTime().getYear();
136   }
137 
138   @Override
139   default int getMonth() {
140     return asZonedDateTime().getMonthValue();
141   }
142 
143   @Override
144   default int getDay() {
145     return asZonedDateTime().getDayOfMonth();
146   }
147 
148   @Override
149   default int getHour() {
150     return 0;
151   }
152 
153   @Override
154   default int getMinute() {
155     return 0;
156   }
157 
158   @Override
159   default int getSecond() {
160     return 0;
161   }
162 
163   @Override
164   default int getNano() {
165     return 0;
166   }
167 
168   /**
169    * Get this date as a date/time value at the start of the day.
170    *
171    * @return the date time value
172    */
173   @NonNull
174   default IDateTimeItem asDateTime() {
175     return IDateTimeItem.valueOf(this);
176   }
177 
178   @Override
179   @NonNull
180   ZonedDateTime asZonedDateTime();
181 
182   /**
183    * Get the date as a {@link LocalDate}.
184    *
185    * @return the date
186    */
187   @NonNull
188   default LocalDate asLocalDate() {
189     return ObjectUtils.notNull(asZonedDateTime().toLocalDate());
190   }
191 
192   /**
193    * Adjusts an xs:dateTime value to a specific timezone, or to no timezone at
194    * all.
195    * <p>
196    * This method does one of the following things based on the arguments.
197    * <ol>
198    * <li>If the provided offset is {@code null} and the provided date/time value
199    * has a timezone, the timezone is maked absent.
200    * <li>If the provided offset is {@code null} and the provided date/time value
201    * has an absent timezone, the date/time value is returned.
202    * <li>If the provided offset is not {@code null} and the provided date/time
203    * value has an absent timezone, the date/time value is returned with the new
204    * timezone applied.
205    * <li>Otherwise, the provided timezone is applied to the date/time value
206    * adjusting the time instant.
207    * </ol>
208    * <p>
209    * Implements the XPath 3.1 <a href=
210    * "https://www.w3.org/TR/xpath-functions-31/#func-adjust-dateTime-to-timezone">fn:adjust-dateTime-to-timezone</a>
211    * function.
212    *
213    * @param offset
214    *          the timezone offset to use or {@code null}
215    * @return the adjusted date/time value
216    * @throws DateTimeFunctionException
217    *           with code
218    *           {@link DateTimeFunctionException#INVALID_TIME_ZONE_VALUE_ERROR} if
219    *           the offset is &lt; -PT14H or &gt; PT14H
220    */
221   @Override
222   default IDateItem replaceTimezone(@Nullable IDayTimeDurationItem offset) {
223     return offset == null
224         ? hasTimezone()
225             ? valueOf(ObjectUtils.notNull(asZonedDateTime().withZoneSameLocal(ZoneOffset.UTC)), false)
226             : this
227         : hasTimezone()
228             ? valueOf(IDateTimeItem.valueOf(this).replaceTimezone(offset).asZonedDateTime(),
229                 true)
230             : valueOf(
231                 ObjectUtils.notNull(asZonedDateTime().withZoneSameLocal(offset.asZoneOffset())),
232                 true);
233   }
234 
235   /**
236    * Cast the provided type to this item type.
237    *
238    * @param item
239    *          the item to cast
240    * @return the original item if it is already this type, otherwise a new item
241    *         cast to this type
242    * @throws InvalidValueForCastFunctionException
243    *           if the provided {@code item} cannot be cast to this type
244    */
245   @NonNull
246   static IDateItem cast(@NonNull IAnyAtomicItem item) {
247     IDateItem retval;
248     if (item instanceof IDateItem) {
249       retval = (IDateItem) item;
250     } else if (item instanceof IDateTimeItem) {
251       retval = ((IDateTimeItem) item).asDate();
252     } else if (item instanceof IStringItem || item instanceof IUntypedAtomicItem) {
253       try {
254         retval = valueOf(item.asString());
255       } catch (IllegalStateException | InvalidTypeMetapathException ex) {
256         // asString can throw IllegalStateException exception
257         throw new InvalidValueForCastFunctionException(ex);
258       }
259     } else {
260       throw new InvalidValueForCastFunctionException(
261           String.format("Unsupported item type '%s'", item.getClass().getName()));
262     }
263     return retval;
264   }
265 
266   @Override
267   default IDateItem castAsType(IAnyAtomicItem item) {
268     return cast(item);
269   }
270 }