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