001/*
002 * ============================================================================
003 *  Copyright © 2002-2024 by Thomas Thrien.
004 *  All Rights Reserved.
005 * ============================================================================
006 *  Licensed to the public under the agreements of the GNU Lesser General Public
007 *  License, version 3.0 (the "License"). You may obtain a copy of the License at
008 *
009 *       http://www.gnu.org/licenses/lgpl.html
010 *
011 *  Unless required by applicable law or agreed to in writing, software
012 *  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
013 *  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
014 *  License for the specific language governing permissions and limitations
015 *  under the License.
016 */
017
018package org.tquadrat.foundation.util;
019
020import static java.util.Arrays.stream;
021import static java.util.function.Function.identity;
022import static java.util.stream.Collectors.toMap;
023import static org.apiguardian.api.API.Status.INTERNAL;
024import static org.apiguardian.api.API.Status.STABLE;
025import static org.tquadrat.foundation.lang.Objects.requireNonNullArgument;
026import static org.tquadrat.foundation.lang.Objects.requireNotBlankArgument;
027
028import java.time.DateTimeException;
029import java.time.ZoneId;
030import java.time.ZoneOffset;
031import java.time.temporal.TemporalAccessor;
032import java.time.zone.ZoneRulesException;
033import java.util.HashMap;
034import java.util.Map;
035import java.util.TimeZone;
036
037import org.apiguardian.api.API;
038import org.tquadrat.foundation.annotation.ClassVersion;
039import org.tquadrat.foundation.annotation.UtilityClass;
040import org.tquadrat.foundation.exception.PrivateConstructorForStaticClassCalledError;
041import org.tquadrat.foundation.lang.SoftLazy;
042
043/**
044 *  Additional helpers for the work with date/time values.
045 *
046 *  @extauthor Thomas Thrien - thomas.thrien@tquadrat.org
047 *  @version $Id: DateTimeUtils.java 1091 2024-01-25 23:10:08Z tquadrat $
048 *  @since 0.3.0
049 *
050 *  @UMLGraph.link
051 */
052@UtilityClass
053@ClassVersion( sourceVersion = "$Id: DateTimeUtils.java 1091 2024-01-25 23:10:08Z tquadrat $" )
054@API( status = STABLE, since = "0.3.0" )
055public final class DateTimeUtils
056{
057        /*------------------------*\
058    ====** Static Initialisations **===========================================
059        \*------------------------*/
060    /**
061     *  The alias map.
062     *
063     *  @since 0.3.0
064     *  @see #createZoneIdAliasMap()
065     */
066    @API( status = INTERNAL, since = "0.4.0" )
067    private static final SoftLazy<Map<String,String>> m_ZoneIdAliasMap;
068
069    /**
070     *  The cached zone ids.
071     */
072    @SuppressWarnings( "StaticCollection" )
073    private static final Map<String,ZoneId> m_ZoneIdCache = new HashMap<>();
074
075    static
076    {
077        //---* Initialise the ZoneId Alias Map *-------------------------------
078        m_ZoneIdAliasMap = SoftLazy.use( DateTimeUtils::createZoneIdAliasMap );
079    }
080
081        /*--------------*\
082    ====** Constructors **=====================================================
083        \*--------------*/
084    /**
085     *  No instance allowed for this class!
086     */
087    private DateTimeUtils() { throw new PrivateConstructorForStaticClassCalledError( DateTimeUtils.class ); }
088
089        /*---------*\
090    ====** Methods **==========================================================
091        \*---------*/
092    /**
093     *  Creates the alias map for the old (deprecated) zone ids that are used
094     *  for the call to
095     *  {@link ZoneId#of(String, java.util.Map)}
096     *  to retrieve a
097     *  {@link ZoneId}
098     *  instance for the given zone id.
099     *
100     *  @return The alias map.
101     *
102     *  @since 0.4.0
103     */
104    @API( status = STABLE, since = "0.4.0" )
105    public static final Map<String,String> createZoneIdAliasMap()
106    {
107        final var retValue = stream( TimeZone.getAvailableIDs() )
108            .filter( id -> !ZoneId.getAvailableZoneIds().contains( id ) )
109            .collect( toMap( identity(), id -> TimeZone.getTimeZone( id ).toZoneId().normalized().toString() ) );
110
111        //---* Done *----------------------------------------------------------
112        return retValue;
113    }   //  createZoneIdAliasMap()
114
115    /**
116     *  <p>{@summary Returns the alias map for the zone id, holding the
117     *  deprecated ids.} If not yet created, the alias map will be
118     *  created by a call to
119     *  {@link #createZoneIdAliasMap()}
120     *  and the result to that call will be cached for future calls.</p>
121     *
122     *  @return The alias map.
123     *
124     *  @see #createZoneIdAliasMap()
125     *
126     *  @since 0.4.0
127     */
128    @API( status = STABLE, since = "0.4.0" )
129    public static final Map<String,String> getZoneIdAliasMap() { return m_ZoneIdAliasMap.get(); }
130
131    /**
132     *  <p>{@summary Replaces the given instance of
133     *  {@link ZoneId}
134     *  by one from the cache.}</p>
135     *
136     *  @param  zoneId  The instance of {@code ZoneId} that needs to be
137     *      replaced.
138     *  @return The instance of {@code ZoneId} from the cache; this may be the
139     *      same as the argument in case the zone id was not yet in the cache.
140     *
141     *  @see #retrieveCachedZoneId(String)
142     */
143    public static final ZoneId replaceByCachedZoneId( final ZoneId zoneId )
144    {
145        final ZoneId retValue;
146        synchronized( m_ZoneIdCache )
147        {
148            retValue = m_ZoneIdCache.computeIfAbsent( requireNonNullArgument( zoneId, "zoneId" ).getId(), _ -> zoneId );
149        }
150
151        //---* Done *----------------------------------------------------------
152        return retValue;
153    }   //  replaceByCachedZoneId()
154
155    /**
156     *  <p>{@summary Retrieves a cached instance of
157     *  {@link ZoneId}.}</p>
158     *
159     *  <p>Usually, each call to
160     *  {@link ZoneId#of(String)}
161     *  returns a new instance, even if the argument remains the same. This
162     *  means that it cannot be assumed that</p>
163     *  <pre><code>ZoneId.of( "UTC" ) == ZoneId.of( "UTC" )</code></pre>
164     *  <p>returns {@code true} (although it cannot be excluded either).</p>
165     *  <p>If an application uses {@code ZoneId}s a lot, this could cause
166     *  significant memory pressure, so it would make sense to cache them.</p>
167     *  <p>This is safe because the instances of {@code ZoneId} are immutable
168     *  (the documentation says, they should be treated as
169     *  <i>ValueBased</i>).</p>
170     *  <p>As the number of distinct timezones is limited, there is no
171     *  housekeeping for the cache itself.</p>
172     *
173     *  @note The id strings are case-sensitive!
174     *
175     *  @param  id  The id for the time zone.
176     *  @return The instance of {@code ZoneId} for the given id.
177     *  @throws DateTimeException   The given id has an invalid format.
178     *  @throws ZoneRulesException  The given id is a region id that cannot be
179     *      found.
180     *
181     *  @see <a href="https://stackoverflow.com/a/77660700/1554195">stackoverflow: &quot;Many instances of java.time.ZoneRegion in Java heap&quot;</a>
182     */
183    public static final ZoneId retrieveCachedZoneId( final String id ) throws DateTimeException, ZoneRulesException
184    {
185        final ZoneId retValue;
186        synchronized( m_ZoneIdCache )
187        {
188            retValue = m_ZoneIdCache.computeIfAbsent( requireNotBlankArgument( id, "id" ), ZoneId::of );
189        }
190
191        //---* Done *----------------------------------------------------------
192        return retValue;
193    }   //  retrievedCachedZoneId()
194
195    /**
196     *  <p>{@summary Retrieves a cached instance of
197     *  {@link ZoneId}
198     *  using a map of aliases.}</p>
199     *
200     *  @param  id  The id for the time zone.
201     *  @param  aliases A map of alias zone ids (typically abbreviations) to
202     *      real zone ids.
203     *  @return The instance of {@code ZoneId} for the given id.
204     *  @throws DateTimeException   The given id has an invalid format.
205     *  @throws ZoneRulesException  The given id is a region id that cannot be
206     *      found.
207     *
208     *  @see #retrieveCachedZoneId(String)
209     *  @see ZoneId#of(String,Map)
210     */
211    public static final ZoneId retrieveCachedZoneId( final String id, final Map<String,String> aliases ) throws DateTimeException, ZoneRulesException
212    {
213        final var zoneId = ZoneId.of( requireNonNullArgument( id, "id" ), requireNonNullArgument( aliases, "aliases" ) );
214        final var retValue = replaceByCachedZoneId( zoneId );
215
216        //---* Done *----------------------------------------------------------
217        return retValue;
218    }   //  retrieveCacheZoneId()
219
220    /**
221     *  <p>{@summary Retrieves a cached instance of
222     *  {@link ZoneId}
223     *  from the given {@code temporal}.}</p>
224     *
225     *  @param  temporal    The temporal object.
226     *  @return The instance of {@code ZoneId} for the temporal.
227     *  @throws DateTimeException   The given temporal cannot be converted to a
228     *      {@code ZoneId}.
229     *
230     *  @see #retrieveCachedZoneId(String)
231     *  @see ZoneId#from(TemporalAccessor)
232     */
233    public static final ZoneId retrieveCachedZoneId( final TemporalAccessor temporal ) throws DateTimeException
234    {
235        final var zoneId = ZoneId.from( requireNonNullArgument( temporal, "temporal" ) );
236        final var retValue = replaceByCachedZoneId( zoneId );
237
238        //---* Done *----------------------------------------------------------
239        return retValue;
240    }   //  retrieveCachedZoneId()
241
242    /**
243     *  <p>{@summary Retrieves a cached instance of
244     *  {@link ZoneId}
245     *  for the given offset.}</p>
246     *
247     *  @param  prefix  One of &quot;GMT&quot;, &quot;UTC&quot;, &quot;UT&quot;
248     *      or the empty string.
249     *  @param  offset  The offset.
250     *  @return The instance of {@code ZoneId} for the arguments.
251     *  @throws IllegalArgumentException    The prefix is not one of
252     *      &quot;GMT&quot;, &quot;UTC&quot;, &quot;UT&quot; or the empty
253     *      string.
254     *
255     *  @see #retrieveCachedZoneId(String)
256     *  @see ZoneId#ofOffset(String, ZoneOffset)
257     */
258    public static final ZoneId retrieveCachedZoneId( final String prefix, final ZoneOffset offset ) throws IllegalArgumentException
259    {
260        final var zoneId = ZoneId.ofOffset( requireNonNullArgument( prefix, "prefix" ), requireNonNullArgument( offset, "offset" ) );
261        final var retValue = replaceByCachedZoneId( zoneId );
262
263        //---* Done *----------------------------------------------------------
264        return retValue;
265    }   //  retrieveCachedZoneId()
266}
267//  class DateTimeUtils
268
269/*
270 *  End of File
271 */