001/*
002 * jPOS Project [http://jpos.org]
003 * Copyright (C) 2000-2026 jPOS Software SRL
004 *
005 * This program is free software: you can redistribute it and/or modify
006 * it under the terms of the GNU Affero General Public License as
007 * published by the Free Software Foundation, either version 3 of the
008 * License, or (at your option) any later version.
009 *
010 * This program is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
013 * GNU Affero General Public License for more details.
014 *
015 * You should have received a copy of the GNU Affero General Public License
016 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
017 */
018
019package org.jpos.util;
020
021import java.text.DateFormat;
022import java.text.ParseException;
023import java.text.SimpleDateFormat;
024import java.util.Calendar;
025import java.util.Date;
026import java.util.GregorianCalendar;
027import java.util.Locale;
028import java.util.TimeZone;
029
030/**
031 * Convenience helpers for parsing and formatting dates and times in a small
032 * set of jPOS-specific patterns. Methods that take {@code null} return
033 * {@code null} so call sites do not need to guard themselves.
034 */
035public class DateUtil {
036    /** Utility class; instances carry no state. */
037    public DateUtil() {}
038    static SimpleDateFormat dfDate = (SimpleDateFormat) DateFormat.getDateInstance(DateFormat.SHORT, Locale.US);
039    static SimpleDateFormat dfDateTime = (SimpleDateFormat) DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM, Locale.US);
040
041    static SimpleDateFormat dfDate_mmddyyyy = new SimpleDateFormat("MM/dd/yyyy");
042    static SimpleDateFormat dfDate_yyyymmdd = new SimpleDateFormat("yyyyMMdd");
043    static SimpleDateFormat dfDateTime_mmddyyyy = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss");
044
045    static SimpleDateFormat dfDate_mmddyy = new SimpleDateFormat("MM/dd/yy");
046    /**
047     * Parses {@code s} using the US short date format.
048     *
049     * @param s date string, or {@code null}
050     * @return parsed date, or {@code null} when {@code s} is {@code null}
051     * @throws ParseException if the string cannot be parsed
052     */
053    public static Date parseDate (String s) throws ParseException {
054        if (s == null)
055            return null;
056        return dfDate.parse (s);
057    }
058    /**
059     * Parses {@code s} using the {@code MM/dd/yyyy} pattern.
060     *
061     * @param s date string, or {@code null}
062     * @return parsed date, or {@code null} when {@code s} is {@code null}
063     * @throws ParseException if the string cannot be parsed
064     */
065    public static Date parseDate_mmddyyyy (String s) throws ParseException {
066        if (s == null)
067            return null;
068        return dfDate_mmddyyyy.parse (s);
069    }
070    /**
071     * Parses {@code s} using the {@code yyyyMMdd} pattern.
072     *
073     * @param s date string, or {@code null}
074     * @return parsed date, or {@code null} when {@code s} is {@code null}
075     * @throws ParseException if the string cannot be parsed
076     */
077    public static Date parseDate_yyyymmdd (String s) throws ParseException {
078        if (s == null)
079            return null;
080        return dfDate_yyyymmdd.parse (s);
081    }
082    /**
083     * Parses {@code s} using the {@code MM/dd/yy} pattern.
084     *
085     * @param s date string, or {@code null}
086     * @return parsed date, or {@code null} when {@code s} is {@code null}
087     * @throws ParseException if the string cannot be parsed
088     */
089    public static Date parseDate_mmddyy (String s) throws ParseException {
090        if (s == null)
091            return null;
092        return dfDate_mmddyy.parse (s);
093    }
094
095    /**
096     * Parses {@code s} using the US short date / medium time format.
097     *
098     * @param s date-time string, or {@code null}
099     * @return parsed date, or {@code null} when {@code s} is {@code null}
100     * @throws ParseException if the string cannot be parsed
101     */
102    public static Date parseDateTime (String s) throws ParseException {
103        if (s == null)
104            return null;
105        return dfDateTime.parse (s);
106    }
107    /**
108     * Parses {@code s} using the {@code MM/dd/yyyy HH:mm:ss} pattern.
109     *
110     * @param s date-time string, or {@code null}
111     * @return parsed date, or {@code null} when {@code s} is {@code null}
112     * @throws ParseException if the string cannot be parsed
113     */
114    public static Date parseDateTime_mmddyyyy (String s) throws ParseException {
115        if (s == null)
116            return null;
117        return dfDateTime_mmddyyyy.parse (s);
118    }
119    /**
120     * Parses {@code s} using the {@code MM/dd/yyyy HH:mm:ss} pattern.
121     *
122     * @param s timestamp string, or {@code null}
123     * @return parsed date, or {@code null} when {@code s} is {@code null}
124     * @throws ParseException if the string cannot be parsed
125     */
126    public static Date parseTimestamp (String s) throws ParseException {
127        if (s == null)
128            return null;
129        return dfDateTime_mmddyyyy.parse (s);
130    }
131
132    /**
133     * Parses {@code s} using the {@code MM/dd/yyyy HH:mm:ss} pattern in the
134     * given time zone.
135     *
136     * @param s        date-time string, or {@code null}
137     * @param tzString time-zone identifier accepted by {@link TimeZone#getTimeZone(String)};
138     *                 {@code null} keeps the JVM default zone
139     * @return parsed date, or {@code null} when {@code s} is {@code null}
140     * @throws ParseException if the string cannot be parsed
141     */
142    public static Date parseDateTime_mmddyyyy (String s, String tzString)
143        throws ParseException
144    {
145        if (s == null)
146            return null;
147        DateFormat df = (DateFormat) dfDateTime_mmddyyyy.clone();
148        if (tzString != null)
149            df.setTimeZone (TimeZone.getTimeZone (tzString));
150        return df.parse (s);
151    }
152
153    /**
154     * Formats {@code d} using the US short date format.
155     *
156     * @param d date, or {@code null}
157     * @return formatted string, or {@code null} when {@code d} is {@code null}
158     */
159    public static String dateToString (Date d) {
160        if (d == null)
161            return null;
162        return dfDate.format (d);
163    }
164    /**
165     * Formats {@code d} using the {@code MM/dd/yyyy} pattern.
166     *
167     * @param d date, or {@code null}
168     * @return formatted string, or {@code null} when {@code d} is {@code null}
169     */
170    public static String dateToString_mmddyyyy (Date d) {
171        if (d == null)
172            return null;
173        return dfDate_mmddyyyy.format (d);
174    }
175
176    /**
177     * Formats {@code d} using the US short date / medium time format.
178     *
179     * @param d date, or {@code null}
180     * @return formatted string, or {@code null} when {@code d} is {@code null}
181     */
182    public static String dateTimeToString (Date d) {
183        if (d == null)
184            return null;
185        return dfDateTime.format (d);
186    }
187    /**
188     * Formats {@code d} using the US short date / medium time format in the
189     * given time zone.
190     *
191     * @param d        date, or {@code null}
192     * @param tzString time-zone identifier; {@code null} keeps the JVM default zone
193     * @return formatted string, or {@code null} when {@code d} is {@code null}
194     */
195    public static String dateTimeToString (Date d, String tzString) {
196        if (d == null)
197            return null;
198        DateFormat df = (DateFormat) dfDateTime.clone();
199        if (tzString != null)
200            df.setTimeZone (TimeZone.getTimeZone (tzString));
201        return df.format (d);
202    }
203    /**
204     * Formats {@code d} using the {@code MM/dd/yyyy HH:mm:ss} pattern.
205     *
206     * @param d date, or {@code null}
207     * @return formatted string, or {@code null} when {@code d} is {@code null}
208     */
209    public static String dateTimeToString_mmddyyyy (Date d) {
210        if (d == null)
211            return null;
212        return dfDateTime_mmddyyyy.format (d);
213    }
214    /**
215     * Formats {@code d} using the {@code MM/dd/yyyy HH:mm:ss} pattern.
216     *
217     * @param d date, or {@code null}
218     * @return formatted string, or {@code null} when {@code d} is {@code null}
219     */
220    public static String timestamp (Date d) {
221        if (d == null)
222            return null;
223        return dfDateTime_mmddyyyy.format (d);
224    }
225    /**
226     * Formats {@code d} using the {@code MM/dd/yyyy} pattern.
227     *
228     * @param d date, or {@code null}
229     * @return formatted string, or {@code null} when {@code d} is {@code null}
230     */
231    public static String postdate (Date d) {
232        if (d == null)
233            return null;
234        return dfDate_mmddyyyy.format (d);
235    }
236
237   /**
238    * Parses an {@code MMDDYY} date and {@code HHMMSS} time, choosing the
239    * century closest to "now" so two-digit years near a century boundary
240    * round to the correct year.
241    *
242    * @param d date in {@code MMDDYY}
243    * @param t time in {@code HHMMSS}
244    * @return parsed date
245    */
246    public static Date parseDateTime (String d, String t) {
247        Calendar cal = new GregorianCalendar();
248        Date now = new Date ();
249        cal.setTime (now);
250
251        int YY = Integer.parseInt (d.substring (4));
252        int MM = Integer.parseInt (d.substring (0, 2))-1;
253        int DD = Integer.parseInt (d.substring (2, 4));
254        int hh = Integer.parseInt (t.substring (0, 2));
255        int mm = Integer.parseInt (t.substring (2, 4));
256        int ss = Integer.parseInt (t.substring (4));
257        int century = cal.get (Calendar.YEAR) / 100;
258
259        cal.set (Calendar.YEAR, (century * 100) + YY);
260        cal.set (Calendar.MONTH, MM);
261        cal.set (Calendar.DATE, DD);
262        cal.set (Calendar.HOUR_OF_DAY, hh);
263        cal.set (Calendar.MINUTE, mm);
264        cal.set (Calendar.SECOND, ss);
265
266        //
267        // I expect this program to continue running by 2099 ... --apr@jpos.org
268        //
269        Date thisCentury = cal.getTime();
270        cal.set (Calendar.YEAR, (--century * 100) + YY);
271        Date previousCentury = cal.getTime();
272
273        if (Math.abs (now.getTime() - previousCentury.getTime()) <
274            Math.abs (now.getTime() - thisCentury.getTime()) )
275            thisCentury = previousCentury;
276        return thisCentury;
277    }
278   /**
279    * Parses an {@code HHMM} or {@code HHMMSS} time using today as the date.
280    *
281    * @param t time string
282    * @return parsed date with today's year/month/day
283    */
284    public static Date parseTime (String t) {
285        return parseTime (t, new Date());
286    }
287    /**
288     * Parses an {@code HHMM} or {@code HHMMSS} time using {@code now} as the
289     * date portion.
290     *
291     * @param t   time string
292     * @param now date supplying the year/month/day
293     * @return parsed date with the supplied date and the parsed time
294     */
295    public static Date parseTime (String t, Date now) {
296        Calendar cal = new GregorianCalendar();
297        cal.setTime (now);
298
299        int hh = Integer.parseInt (t.substring (0, 2));
300        int mm = Integer.parseInt (t.substring (2, 4));
301        int ss = t.length() > 4 ? Integer.parseInt (t.substring (4)) : 0;
302
303        cal.set (Calendar.HOUR_OF_DAY, hh);
304        cal.set (Calendar.MINUTE, mm);
305        cal.set (Calendar.SECOND, ss);
306
307        return cal.getTime();
308    }
309
310    /**
311     * Formats {@code d} using the JVM default date format in the given time zone.
312     *
313     * @param d        date
314     * @param tzString time-zone identifier; {@code null} keeps the JVM default zone
315     * @return formatted date string
316     */
317    public static String dateToString (Date d, String tzString) {
318        DateFormat df = (DateFormat) DateFormat.getDateInstance().clone();
319        if (tzString != null)
320            df.setTimeZone (TimeZone.getTimeZone (tzString));
321        return df.format (d);
322    }
323    /**
324     * Formats the time portion of {@code d} using the JVM short time format
325     * in the given time zone.
326     *
327     * @param d        date
328     * @param tzString time-zone identifier; {@code null} keeps the JVM default zone
329     * @return formatted time string
330     */
331    public static String timeToString (Date d, String tzString) {
332        DateFormat df = (DateFormat)
333            DateFormat.getTimeInstance(DateFormat.SHORT).clone();
334        if (tzString != null)
335            df.setTimeZone (TimeZone.getTimeZone (tzString));
336        return df.format (d);
337    }
338    /**
339     * Formats the time portion of {@code d} using the JVM short time format.
340     *
341     * @param d date
342     * @return formatted time string
343     */
344    public static String timeToString (Date d) {
345        return timeToString (d, null);
346    }
347    /**
348     * Renders a duration in milliseconds as a compact {@code 1h2m3s}-style string.
349     *
350     * @param period duration in milliseconds
351     * @return human-readable duration; always includes a seconds component,
352     *         and includes hours/minutes only when non-zero
353     */
354    public static String toDays (long period) {
355        StringBuffer sb = new StringBuffer();
356        long hours = period / 3600000L;
357        if (hours > 0) {
358            sb.append (hours);
359            sb.append ("h");
360            period -= (hours * 3600000L);
361        }
362        long mins = period / 60000L;
363        if (mins > 0) {
364            sb.append (mins);
365            sb.append ("m");
366            period -= (mins * 60000L);
367        }
368        long secs = period / 1000L;
369        sb.append (secs);
370        sb.append ("s");
371        return sb.toString();
372    }
373}
374