001/*
002 * Copyright (c) 2010-2024 Mark Allen, Norbert Bartels.
003 *
004 * Permission is hereby granted, free of charge, to any person obtaining a copy
005 * of this software and associated documentation files (the "Software"), to deal
006 * in the Software without restriction, including without limitation the rights
007 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
008 * copies of the Software, and to permit persons to whom the Software is
009 * furnished to do so, subject to the following conditions:
010 *
011 * The above copyright notice and this permission notice shall be included in
012 * all copies or substantial portions of the Software.
013 *
014 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
015 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
016 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
017 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
018 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
019 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
020 * THE SOFTWARE.
021 */
022package com.restfb.util;
023
024import static com.restfb.logging.RestFBLogger.UTILS_LOGGER;
025
026import java.text.ParseException;
027import java.util.Date;
028import java.util.Optional;
029
030/**
031 * A collection of date-handling utility methods.
032 * 
033 * @author <a href="http://restfb.com">Mark Allen</a>
034 * @since 1.6
035 */
036public final class DateUtils {
037  /**
038   * Facebook "long" date format (IETF RFC 3339). Example: {@code 2010-02-28T16:11:08+0000}
039   */
040  public static final String FACEBOOK_LONG_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ssZ";
041
042  /**
043   * Facebook "long" date format (IETF RFC 3339) without a timezone component. Example: {@code 2010-02-28T16:11:08}
044   */
045  public static final String FACEBOOK_LONG_DATE_FORMAT_WITHOUT_TIMEZONE = "yyyy-MM-dd'T'HH:mm:ss";
046
047  /**
048   * Facebook "long" date format (IETF RFC 3339) without a timezone or seconds component. Example:
049   * {@code 2010-02-28T16:11}
050   */
051  public static final String FACEBOOK_LONG_DATE_FORMAT_WITHOUT_TIMEZONE_OR_SECONDS = "yyyy-MM-dd'T'HH:mm";
052
053  /**
054   * Facebook short date format. Example: {@code 04/15/1984}
055   */
056  public static final String FACEBOOK_SHORT_DATE_FORMAT = "MM/dd/yyyy";
057
058  /**
059   * Facebook alternate short date format. Example: {@code 2012-09-15}
060   */
061  public static final String FACEBOOK_ALTERNATE_SHORT_DATE_FORMAT = "yyyy-MM-dd";
062
063  /**
064   * Facebook month-year only date format. Example: {@code Example: 2007-03}
065   */
066  public static final String FACEBOOK_MONTH_YEAR_DATE_FORMAT = "yyyy-MM";
067
068  /**
069   * DateFormatStrategy (default: SimpleDateFormat).
070   */
071  private static DateFormatStrategy strategy = new SimpleDateFormatStrategy();
072
073  /**
074   * Prevents instantiation.
075   */
076  private DateUtils() {
077    throw new IllegalStateException("DateUtils must not be instantiated");
078  }
079
080  /**
081   * Returns a Java representation of a Facebook "long" {@code date} string, or the number of seconds since the epoch.
082   * <p>
083   * Supports dates with or without timezone information.
084   * 
085   * @param date
086   *          Facebook {@code date} string.
087   * @return Java date representation of the given Facebook "long" {@code date} string or {@code null} if {@code date}
088   *         is {@code null} or invalid.
089   */
090  public static Date toDateFromLongFormat(String date) {
091    if (isNull(date)) {
092      return null;
093    }
094
095    // Is this an all-digit date? Then assume it's the "seconds since epoch"
096    // variant
097    if (date.trim().matches("\\d+")) {
098      return new Date(Long.parseLong(date) * 1000L);
099    }
100
101    Date parsedDate = toDateWithFormatString(date, FACEBOOK_LONG_DATE_FORMAT);
102
103    // Fall back to variant without timezone if the initial parse fails
104    if (isNull(parsedDate)) {
105      parsedDate = toDateWithFormatString(date, FACEBOOK_LONG_DATE_FORMAT_WITHOUT_TIMEZONE);
106    }
107
108    // Fall back to variant without seconds if secondary parse fails
109    if (isNull(parsedDate)) {
110      parsedDate = toDateWithFormatString(date, FACEBOOK_LONG_DATE_FORMAT_WITHOUT_TIMEZONE_OR_SECONDS);
111    }
112
113    // fallback if we assume long, but it is a short format
114    if (isNull(parsedDate)) {
115      parsedDate = toDateFromShortFormat(date);
116    }
117
118    return parsedDate;
119  }
120
121  private static boolean isNull(Object date) {
122    return date == null;
123  }
124
125  /**
126   * Returns a Java representation of a Facebook "short" {@code date} string.
127   * 
128   * @param date
129   *          Facebook {@code date} string.
130   * @return Java date representation of the given Facebook "short" {@code date} string or {@code null} if {@code date}
131   *         is {@code null} or invalid.
132   */
133  public static Date toDateFromShortFormat(String date) {
134    if (isNull(date)) {
135      return null;
136    }
137
138    Date parsedDate = toDateWithFormatString(date, FACEBOOK_SHORT_DATE_FORMAT);
139
140    // Fall back to variant if initial parse fails
141    if (isNull(parsedDate)) {
142      parsedDate = toDateWithFormatString(date, FACEBOOK_ALTERNATE_SHORT_DATE_FORMAT);
143    }
144
145    return parsedDate;
146  }
147
148  /**
149   * Returns a Java representation of a Facebook "month-year" {@code date} string.
150   * 
151   * @param date
152   *          Facebook {@code date} string.
153   * @return Java date representation of the given Facebook "month-year" {@code date} string or {@code null} if
154   *         {@code date} is {@code null} or invalid.
155   */
156  public static Date toDateFromMonthYearFormat(String date) {
157    if (isNull(date)) {
158      return null;
159    }
160
161    if ("0000-00".equals(date)) {
162      return null;
163    }
164
165    return toDateWithFormatString(date, FACEBOOK_MONTH_YEAR_DATE_FORMAT);
166  }
167
168  /**
169   * Returns a String representation of a {@code date} object
170   * 
171   * @param date
172   *          as Date
173   * @return String representation of a {@code date} object. The String is in the form {@code 2010-02-28T16:11:08}
174   */
175  public static String toLongFormatFromDate(Date date) {
176    return Optional.ofNullable(date).map(strategy.formatFor(FACEBOOK_LONG_DATE_FORMAT_WITHOUT_TIMEZONE)::format).orElse(null);
177  }
178
179  /**
180   * Returns a <strong>short</strong> String representation of a {@code date} object
181   *
182   * @param date
183   *          as Date
184   * @return String representation of a {@code date} object. The String is in the form {@code 2019-06-14}
185   */
186  public static String toShortFormatFromDate(Date date) {
187    return Optional.ofNullable(date).map(strategy.formatFor(FACEBOOK_ALTERNATE_SHORT_DATE_FORMAT)::format).orElse(null);
188  }
189
190  /**
191   * Returns a Java representation of a {@code date} string.
192   * 
193   * @param date
194   *          Date in string format.
195   * @return Java date representation of the given {@code date} string or {@code null} if {@code date} is {@code null}
196   *         or invalid.
197   */
198  private static Date toDateWithFormatString(String date, String format) {
199    if (isNull(date)) {
200      return null;
201    }
202
203    try {
204      return strategy.formatFor(format).parse(date);
205    } catch (ParseException e) {
206      UTILS_LOGGER.trace("Unable to parse date '{}' using format string '{}': {}", date, format, e);
207      return null;
208    }
209  }
210
211  /**
212   * get the current DateFormatStrategy.
213   * 
214   * @return the current DateFormatStrategy
215   */
216  public static DateFormatStrategy getDateFormatStrategy() {
217    return strategy;
218  }
219
220  /**
221   * set the {@link DateFormatStrategy}.
222   * <p>
223   * default value: {@link SimpleDateFormatStrategy}
224   * 
225   * @param dateFormatStrategy
226   *          the used {@link DateFormatStrategy}
227   * 
228   */
229  public static void setDateFormatStrategy(DateFormatStrategy dateFormatStrategy) {
230    strategy = dateFormatStrategy;
231  }
232}