/* * SPDX-License-Identifier: Apache-2.0 * * The OpenSearch Contributors require contributions made to * this file be licensed under the Apache-2.0 license or a * compatible open source license. */ /* * Licensed to Elasticsearch under one or more contributor * license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch licenses this file to you under * the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /* * Modifications Copyright OpenSearch Contributors. See * GitHub history for details. */ package org.opensearch.common.time; import org.opensearch.common.logging.DeprecationLogger; import org.joda.time.DateTimeZone; import java.time.Clock; import java.time.Duration; import java.time.Instant; import java.time.ZoneId; import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Set; import static org.opensearch.common.time.DateUtilsRounding.getMonthOfYear; import static org.opensearch.common.time.DateUtilsRounding.getTotalMillisByYearMonth; import static org.opensearch.common.time.DateUtilsRounding.getYear; import static org.opensearch.common.time.DateUtilsRounding.utcMillisAtStartOfYear; /** * Date utilities. * * @opensearch.internal */ public class DateUtils { public static DateTimeZone zoneIdToDateTimeZone(ZoneId zoneId) { if (zoneId == null) { return null; } if (zoneId instanceof ZoneOffset) { // the id for zoneoffset is not ISO compatible, so cannot be read by ZoneId.of return DateTimeZone.forOffsetMillis(((ZoneOffset) zoneId).getTotalSeconds() * 1000); } return DateTimeZone.forID(zoneId.getId()); } private static final DeprecationLogger deprecationLogger = DeprecationLogger.getLogger(DateUtils.class); // pkg private for tests static final Map DEPRECATED_SHORT_TIMEZONES; public static final Set DEPRECATED_SHORT_TZ_IDS; static { Map tzs = new HashMap<>(); tzs.put("EST", "-05:00"); // eastern time without daylight savings tzs.put("HST", "-10:00"); tzs.put("MST", "-07:00"); tzs.put("ROC", "Asia/Taipei"); tzs.put("Eire", "Europe/London"); DEPRECATED_SHORT_TIMEZONES = Collections.unmodifiableMap(tzs); DEPRECATED_SHORT_TZ_IDS = tzs.keySet(); } // Map of deprecated timezones and their recommended new counterpart public static final Map DEPRECATED_LONG_TIMEZONES; static { Map tzs = new HashMap<>(); tzs.put("Africa/Asmera", "Africa/Nairobi"); tzs.put("Africa/Timbuktu", "Africa/Abidjan"); tzs.put("America/Argentina/ComodRivadavia", "America/Argentina/Catamarca"); tzs.put("America/Atka", "America/Adak"); tzs.put("America/Buenos_Aires", "America/Argentina/Buenos_Aires"); tzs.put("America/Catamarca", "America/Argentina/Catamarca"); tzs.put("America/Coral_Harbour", "America/Atikokan"); tzs.put("America/Cordoba", "America/Argentina/Cordoba"); tzs.put("America/Ensenada", "America/Tijuana"); tzs.put("America/Fort_Wayne", "America/Indiana/Indianapolis"); tzs.put("America/Indianapolis", "America/Indiana/Indianapolis"); tzs.put("America/Jujuy", "America/Argentina/Jujuy"); tzs.put("America/Knox_IN", "America/Indiana/Knox"); tzs.put("America/Louisville", "America/Kentucky/Louisville"); tzs.put("America/Mendoza", "America/Argentina/Mendoza"); tzs.put("America/Montreal", "America/Toronto"); tzs.put("America/Porto_Acre", "America/Rio_Branco"); tzs.put("America/Rosario", "America/Argentina/Cordoba"); tzs.put("America/Santa_Isabel", "America/Tijuana"); tzs.put("America/Shiprock", "America/Denver"); tzs.put("America/Virgin", "America/Port_of_Spain"); tzs.put("Antarctica/South_Pole", "Pacific/Auckland"); tzs.put("Asia/Ashkhabad", "Asia/Ashgabat"); tzs.put("Asia/Calcutta", "Asia/Kolkata"); tzs.put("Asia/Chongqing", "Asia/Shanghai"); tzs.put("Asia/Chungking", "Asia/Shanghai"); tzs.put("Asia/Dacca", "Asia/Dhaka"); tzs.put("Asia/Harbin", "Asia/Shanghai"); tzs.put("Asia/Kashgar", "Asia/Urumqi"); tzs.put("Asia/Katmandu", "Asia/Kathmandu"); tzs.put("Asia/Macao", "Asia/Macau"); tzs.put("Asia/Rangoon", "Asia/Yangon"); tzs.put("Asia/Saigon", "Asia/Ho_Chi_Minh"); tzs.put("Asia/Tel_Aviv", "Asia/Jerusalem"); tzs.put("Asia/Thimbu", "Asia/Thimphu"); tzs.put("Asia/Ujung_Pandang", "Asia/Makassar"); tzs.put("Asia/Ulan_Bator", "Asia/Ulaanbaatar"); tzs.put("Atlantic/Faeroe", "Atlantic/Faroe"); tzs.put("Atlantic/Jan_Mayen", "Europe/Oslo"); tzs.put("Australia/ACT", "Australia/Sydney"); tzs.put("Australia/Canberra", "Australia/Sydney"); tzs.put("Australia/LHI", "Australia/Lord_Howe"); tzs.put("Australia/NSW", "Australia/Sydney"); tzs.put("Australia/North", "Australia/Darwin"); tzs.put("Australia/Queensland", "Australia/Brisbane"); tzs.put("Australia/South", "Australia/Adelaide"); tzs.put("Australia/Tasmania", "Australia/Hobart"); tzs.put("Australia/Victoria", "Australia/Melbourne"); tzs.put("Australia/West", "Australia/Perth"); tzs.put("Australia/Yancowinna", "Australia/Broken_Hill"); tzs.put("Brazil/Acre", "America/Rio_Branco"); tzs.put("Brazil/DeNoronha", "America/Noronha"); tzs.put("Brazil/East", "America/Sao_Paulo"); tzs.put("Brazil/West", "America/Manaus"); tzs.put("Canada/Atlantic", "America/Halifax"); tzs.put("Canada/Central", "America/Winnipeg"); tzs.put("Canada/East-Saskatchewan", "America/Regina"); tzs.put("Canada/Eastern", "America/Toronto"); tzs.put("Canada/Mountain", "America/Edmonton"); tzs.put("Canada/Newfoundland", "America/St_Johns"); tzs.put("Canada/Pacific", "America/Vancouver"); tzs.put("Canada/Yukon", "America/Whitehorse"); tzs.put("Chile/Continental", "America/Santiago"); tzs.put("Chile/EasterIsland", "Pacific/Easter"); tzs.put("Cuba", "America/Havana"); tzs.put("Egypt", "Africa/Cairo"); tzs.put("Eire", "Europe/Dublin"); tzs.put("Europe/Belfast", "Europe/London"); tzs.put("Europe/Tiraspol", "Europe/Chisinau"); tzs.put("GB", "Europe/London"); tzs.put("GB-Eire", "Europe/London"); tzs.put("Greenwich", "Etc/GMT"); tzs.put("Hongkong", "Asia/Hong_Kong"); tzs.put("Iceland", "Atlantic/Reykjavik"); tzs.put("Iran", "Asia/Tehran"); tzs.put("Israel", "Asia/Jerusalem"); tzs.put("Jamaica", "America/Jamaica"); tzs.put("Japan", "Asia/Tokyo"); tzs.put("Kwajalein", "Pacific/Kwajalein"); tzs.put("Libya", "Africa/Tripoli"); tzs.put("Mexico/BajaNorte", "America/Tijuana"); tzs.put("Mexico/BajaSur", "America/Mazatlan"); tzs.put("Mexico/General", "America/Mexico_City"); tzs.put("NZ", "Pacific/Auckland"); tzs.put("NZ-CHAT", "Pacific/Chatham"); tzs.put("Navajo", "America/Denver"); tzs.put("PRC", "Asia/Shanghai"); tzs.put("Pacific/Johnston", "Pacific/Honolulu"); tzs.put("Pacific/Ponape", "Pacific/Pohnpei"); tzs.put("Pacific/Samoa", "Pacific/Pago_Pago"); tzs.put("Pacific/Truk", "Pacific/Chuuk"); tzs.put("Pacific/Yap", "Pacific/Chuuk"); tzs.put("Poland", "Europe/Warsaw"); tzs.put("Portugal", "Europe/Lisbon"); tzs.put("ROC", "Asia/Taipei"); tzs.put("ROK", "Asia/Seoul"); tzs.put("Singapore", "Asia/Singapore"); tzs.put("Turkey", "Europe/Istanbul"); tzs.put("UCT", "Etc/UCT"); tzs.put("US/Alaska", "America/Anchorage"); tzs.put("US/Aleutian", "America/Adak"); tzs.put("US/Arizona", "America/Phoenix"); tzs.put("US/Central", "America/Chicago"); tzs.put("US/East-Indiana", "America/Indiana/Indianapolis"); tzs.put("US/Eastern", "America/New_York"); tzs.put("US/Hawaii", "Pacific/Honolulu"); tzs.put("US/Indiana-Starke", "America/Indiana/Knox"); tzs.put("US/Michigan", "America/Detroit"); tzs.put("US/Mountain", "America/Denver"); tzs.put("US/Pacific", "America/Los_Angeles"); tzs.put("US/Samoa", "Pacific/Pago_Pago"); tzs.put("Universal", "Etc/UTC"); tzs.put("W-SU", "Europe/Moscow"); tzs.put("Zulu", "Etc/UTC"); DEPRECATED_LONG_TIMEZONES = Collections.unmodifiableMap(tzs); } public static ZoneId dateTimeZoneToZoneId(DateTimeZone timeZone) { if (timeZone == null) { return null; } if (DateTimeZone.UTC.equals(timeZone)) { return ZoneOffset.UTC; } return of(timeZone.getID()); } public static ZoneId of(String zoneId) { String deprecatedId = DEPRECATED_SHORT_TIMEZONES.get(zoneId); if (deprecatedId != null) { deprecationLogger.deprecate( "timezone_" + zoneId, "Use of short timezone id " + zoneId + " is deprecated. Use " + deprecatedId + " instead" ); return ZoneId.of(deprecatedId); } return ZoneId.of(zoneId).normalized(); } static final Instant MAX_NANOSECOND_INSTANT = Instant.parse("2262-04-11T23:47:16.854775807Z"); static final long MAX_NANOSECOND_IN_MILLIS = MAX_NANOSECOND_INSTANT.toEpochMilli(); /** * convert a java time instant to a long value which is stored in lucene * the long value resembles the nanoseconds since the epoch * * @param instant the instant to convert * @return the nano seconds and seconds as a single long */ public static long toLong(Instant instant) { if (instant.isBefore(Instant.EPOCH)) { throw new IllegalArgumentException( "date[" + instant + "] is before the epoch in 1970 and cannot be " + "stored in nanosecond resolution" ); } if (instant.isAfter(MAX_NANOSECOND_INSTANT)) { throw new IllegalArgumentException( "date[" + instant + "] is after 2262-04-11T23:47:16.854775807 and cannot be " + "stored in nanosecond resolution" ); } return instant.getEpochSecond() * 1_000_000_000 + instant.getNano(); } /** * Returns an instant that is with valid nanosecond resolution. If * the parameter is before the valid nanosecond range then this returns * the minimum {@linkplain Instant} valid for nanosecond resultion. If * the parameter is after the valid nanosecond range then this returns * the maximum {@linkplain Instant} valid for nanosecond resolution. *

* Useful for checking if all values for the field are within some range, * even if the range's endpoints are not valid nanosecond resolution. */ public static Instant clampToNanosRange(Instant instant) { if (instant.isBefore(Instant.EPOCH)) { return Instant.EPOCH; } if (instant.isAfter(MAX_NANOSECOND_INSTANT)) { return MAX_NANOSECOND_INSTANT; } return instant; } /** * convert a long value to a java time instant * the long value resembles the nanoseconds since the epoch * * @param nanoSecondsSinceEpoch the nanoseconds since the epoch * @return the instant resembling the specified date */ public static Instant toInstant(long nanoSecondsSinceEpoch) { if (nanoSecondsSinceEpoch < 0) { throw new IllegalArgumentException( "nanoseconds [" + nanoSecondsSinceEpoch + "] are before the epoch in 1970 and cannot " + "be processed in nanosecond resolution" ); } if (nanoSecondsSinceEpoch == 0) { return Instant.EPOCH; } long seconds = nanoSecondsSinceEpoch / 1_000_000_000; long nanos = nanoSecondsSinceEpoch % 1_000_000_000; return Instant.ofEpochSecond(seconds, nanos); } /** * Convert a nanosecond timestamp in milliseconds * * @param milliSecondsSinceEpoch the millisecond since the epoch * @return the nanoseconds since the epoch */ public static long toNanoSeconds(long milliSecondsSinceEpoch) { if (milliSecondsSinceEpoch < 0) { throw new IllegalArgumentException( "milliSeconds [" + milliSecondsSinceEpoch + "] are before the epoch in 1970 and cannot " + "be converted to nanoseconds" ); } else if (milliSecondsSinceEpoch > MAX_NANOSECOND_IN_MILLIS) { throw new IllegalArgumentException( "milliSeconds [" + milliSecondsSinceEpoch + "] are after 2262-04-11T23:47:16.854775807 " + "and cannot be converted to nanoseconds" ); } return milliSecondsSinceEpoch * 1_000_000; } /** * Convert a nanosecond timestamp in milliseconds * * @param nanoSecondsSinceEpoch the nanoseconds since the epoch * @return the milliseconds since the epoch */ public static long toMilliSeconds(long nanoSecondsSinceEpoch) { if (nanoSecondsSinceEpoch < 0) { throw new IllegalArgumentException( "nanoseconds are [" + nanoSecondsSinceEpoch + "] are before the epoch in 1970 and cannot " + "be converted to milliseconds" ); } if (nanoSecondsSinceEpoch == 0) { return 0; } return nanoSecondsSinceEpoch / 1_000_000; } /** * Rounds the given utc milliseconds sicne the epoch down to the next unit millis * * Note: This does not check for correctness of the result, as this only works with units smaller or equal than a day * In order to ensure the performance of this methods, there are no guards or checks in it * * @param utcMillis the milliseconds since the epoch * @param unitMillis the unit to round to * @return the rounded milliseconds since the epoch */ public static long roundFloor(long utcMillis, final long unitMillis) { if (utcMillis >= 0) { return utcMillis - utcMillis % unitMillis; } else { utcMillis += 1; return utcMillis - utcMillis % unitMillis - unitMillis; } } /** * Round down to the beginning of the quarter of the year of the specified time * @param utcMillis the milliseconds since the epoch * @return The milliseconds since the epoch rounded down to the quarter of the year */ public static long roundQuarterOfYear(final long utcMillis) { int year = getYear(utcMillis); int month = getMonthOfYear(utcMillis, year); int firstMonthOfQuarter = (((month - 1) / 3) * 3) + 1; return DateUtils.of(year, firstMonthOfQuarter); } /** * Round down to the beginning of the month of the year of the specified time * @param utcMillis the milliseconds since the epoch * @return The milliseconds since the epoch rounded down to the month of the year */ public static long roundMonthOfYear(final long utcMillis) { int year = getYear(utcMillis); int month = getMonthOfYear(utcMillis, year); return DateUtils.of(year, month); } /** * Round down to the beginning of the year of the specified time * @param utcMillis the milliseconds since the epoch * @return The milliseconds since the epoch rounded down to the beginning of the year */ public static long roundYear(final long utcMillis) { int year = getYear(utcMillis); return utcMillisAtStartOfYear(year); } /** * Round down to the beginning of the week based on week year of the specified time * @param utcMillis the milliseconds since the epoch * @return The milliseconds since the epoch rounded down to the beginning of the week based on week year */ public static long roundWeekOfWeekYear(final long utcMillis) { return roundFloor(utcMillis + 3 * 86400 * 1000L, 604800000) - 3 * 86400 * 1000L; } /** * Return the first day of the month * @param year the year to return * @param month the month to return, ranging from 1-12 * @return the milliseconds since the epoch of the first day of the month in the year */ private static long of(final int year, final int month) { long millis = utcMillisAtStartOfYear(year); millis += getTotalMillisByYearMonth(year, month); return millis; } /** * Returns the current UTC date-time with milliseconds precision. * In Java 9+ (as opposed to Java 8) the {@code Clock} implementation uses system's best clock implementation (which could mean * that the precision of the clock can be milliseconds, microseconds or nanoseconds), whereas in Java 8 * {@code System.currentTimeMillis()} is always used. To account for these differences, this method defines a new {@code Clock} * which will offer a value for {@code ZonedDateTime.now()} set to always have milliseconds precision. * * @return {@link ZonedDateTime} instance for the current date-time with milliseconds precision in UTC */ public static ZonedDateTime nowWithMillisResolution() { return nowWithMillisResolution(Clock.systemUTC()); } public static ZonedDateTime nowWithMillisResolution(Clock clock) { Clock millisResolutionClock = Clock.tick(clock, Duration.ofMillis(1)); return ZonedDateTime.now(millisResolutionClock); } }