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.iso;
020
021import java.text.DateFormat;
022import java.text.SimpleDateFormat;
023import java.time.Duration;
024import java.util.*;
025
026/**
027 * provides various parsing and format functions used
028 * by the ISO 8583 specs.
029 *
030 * @author apr@cs.com.uy
031 * @author Hani S. Kirollos
032 * @version $Id$
033 * @see ISOUtil
034 */
035public class ISODate {
036    private ISODate() {
037        throw new AssertionError();
038    }
039
040    public static final long ONE_YEAR = 365L*86400L*1000L;
041   /**
042    * Formats a date object, using the default time zone for this host
043    *
044    * WARNING: See <a href="https://jpos.org/faq/isodate_pattern.html">important issue</a> related to date pattern.
045    *
046    * @param d date object to be formatted
047    * @param pattern to be used for formatting
048    */
049    public static String formatDate (Date d, String pattern) {
050        return formatDate(d, pattern, TimeZone.getDefault());
051    }
052    /**
053     * You should use this version of formatDate() if you want a specific 
054     * timeZone to calculate the date on.
055     *
056     * WARNING: See <a href="https://jpos.org/faq/isodate_pattern.html">important issue</a> related to date pattern.
057     *
058     * @param d date object to be formatted
059     * @param pattern to be used for formatting
060     * @param timeZone for GMT for example, use TimeZone.getTimeZone("GMT")
061     *        and for Uruguay use TimeZone.getTimeZone("GMT-03:00")
062     */
063    public static String formatDate (Date d, String pattern, TimeZone timeZone) {
064        SimpleDateFormat df =
065            (SimpleDateFormat) DateFormat.getDateTimeInstance();
066        df.setTimeZone(timeZone);
067        df.applyPattern(pattern);
068        return df.format(d);
069    }
070    /**
071     * converts a string in DD/MM/YY format to a Date object
072     * Warning: return null on invalid dates (prints Exception to console)
073     * Uses default time zone for this host
074     * @return parsed Date (or null)
075     */
076    public static Date parse(String s) {
077        return parse(s, TimeZone.getDefault());
078    }
079    /**
080     * converts a string in DD/MM/YY format to a Date object
081     * Warning: return null on invalid dates (prints Exception to console)
082     * @param s String in DD/MM/YY recorded in timeZone
083     * @param timeZone for GMT for example, use TimeZone.getTimeZone("GMT")
084     *        and for Uruguay use TimeZone.getTimeZone("GMT-03:00")
085     * @return parsed Date (or null)
086     */
087    public static Date parse(String s, TimeZone timeZone) {
088        Date d = null;
089        SimpleDateFormat df =
090            (SimpleDateFormat) DateFormat.getDateInstance(
091                DateFormat.SHORT, Locale.UK);
092        df.setTimeZone (timeZone);
093        try {
094            d = df.parse (s);
095        } catch (java.text.ParseException e) {
096        }
097        return d;
098    }
099    /**
100     * converts a string in DD/MM/YY HH:MM:SS format to a Date object
101     * Warning: return null on invalid dates (prints Exception to console)
102     * Uses default time zone for this host
103     * @return parsed Date (or null)
104     */
105    public static Date parseDateTime(String s) {
106        return parseDateTime(s, TimeZone.getDefault());
107    }
108    /**
109     * converts a string in DD/MM/YY HH:MM:SS format to a Date object
110     * Warning: return null on invalid dates (prints Exception to console)
111     * @param s string in DD/MM/YY HH:MM:SS format recorded in timeZone
112     * @param timeZone for GMT for example, use TimeZone.getTimeZone("GMT")
113     *        and for Uruguay use TimeZone.getTimeZone("GMT-03:00")
114     * @return parsed Date (or null)
115     */
116    public static Date parseDateTime(String s, TimeZone timeZone) {
117        Date d = null;
118        SimpleDateFormat df =
119            new SimpleDateFormat("dd/MM/yy hh:mm:ss", Locale.UK);
120
121        df.setTimeZone (timeZone);
122        try {
123            d = df.parse (s);
124        } catch (java.text.ParseException e) { }
125        return d;
126    }
127
128    /**
129     * try to find out suitable date given [YY[YY]]MMDDhhmmss format<br>
130     * (difficult thing being finding out appropiate year)
131     * @param d date formated as [YY[YY]]MMDDhhmmss, typical field 13 + field 12
132     * @return Date
133     */
134    public static Date parseISODate (String d) {
135        return parseISODate (d, System.currentTimeMillis());
136    }
137
138    /**
139     * try to find out suitable date given [YY[YY]]MMDDhhmmss format<br>
140     * (difficult thing being finding out appropiate year)
141     * @param d date formated as [YY[YY]]MMDDhhmmss, typical field 13 + field 12
142     * @param timeZone for GMT for example, use TimeZone.getTimeZone("GMT")
143     *        and for Uruguay use TimeZone.getTimeZone("GMT-03:00")
144     * @return Date
145     */
146    public static Date parseISODate (String d, TimeZone timeZone) {
147        return parseISODate (d, System.currentTimeMillis(), timeZone);
148    }
149
150    /**
151     * try to find out suitable date given [YY[YY]]MMDDhhmmss format<br>
152     * (difficult thing being finding out appropiate year)
153     * @param d date formated as [YY[YY]]MMDDhhmmss, typical field 13 + field 12
154     * @param currentTime currentTime in millis
155     * @return Date
156     */
157    public static Date parseISODate (String d, long currentTime) {
158        return parseISODate (d, currentTime, TimeZone.getDefault() );
159    }
160    /**
161     * try to find out suitable date given [YY[YY]]MMDDhhmmss format<br>
162     * (difficult thing being finding out appropiate year)
163     * @param d date formated as [YY[YY]]MMDDhhmmss, typical field 13 + field 12
164     * @param currentTime currentTime in millis
165     * @param timeZone for GMT for example, use TimeZone.getTimeZone("GMT")
166     *        and for Uruguay use TimeZone.getTimeZone("GMT-03:00")
167     * @return Date
168     */
169    public static Date parseISODate (String d, long currentTime, TimeZone timeZone) {
170        int YY = 0;
171
172        Calendar cal = new GregorianCalendar();
173        cal.setTimeZone(timeZone);
174        Date now = new Date(currentTime);
175        cal.setTime (now);
176
177        if (d.length() == 14) {
178            YY = Integer.parseInt(d.substring (0, 4));
179            d = d.substring (4);
180        }
181        else if (d.length() == 12) {
182            YY = calculateNearestFullYear(Integer.parseInt(d.substring(0, 2)), cal);
183            d = d.substring (2);
184        } 
185        int MM = Integer.parseInt(d.substring (0, 2))-1;
186        int DD = Integer.parseInt(d.substring (2, 4));
187        int hh = Integer.parseInt(d.substring (4, 6));
188        int mm = Integer.parseInt(d.substring (6, 8));
189        int ss = Integer.parseInt(d.substring (8,10));
190        
191        cal.set (Calendar.MONTH, MM);
192        cal.set (Calendar.DATE, DD);
193        cal.set (Calendar.HOUR_OF_DAY, hh);
194        cal.set (Calendar.MINUTE, mm);
195        cal.set (Calendar.SECOND, ss);
196        cal.set (Calendar.MILLISECOND, 0);
197
198        if (YY != 0) {
199            cal.set (Calendar.YEAR, YY);
200            return cal.getTime();
201        } 
202        else {
203            Date thisYear = cal.getTime();
204            cal.set (Calendar.YEAR, cal.get (Calendar.YEAR)-1);
205            Date previousYear = cal.getTime();
206            cal.set (Calendar.YEAR, cal.get (Calendar.YEAR)+2);
207            Date nextYear = cal.getTime();
208            if (Math.abs (now.getTime() - previousYear.getTime()) <
209                Math.abs (now.getTime() - thisYear.getTime())) 
210            {
211                thisYear = previousYear;
212            } else if (Math.abs (now.getTime() - thisYear.getTime()) >
213                       Math.abs (now.getTime() - nextYear.getTime()))
214            {
215                thisYear = nextYear;
216            }
217            return thisYear;
218        }
219    }
220
221    /**
222     * @return date in MMddHHmmss format suitable for FIeld 7
223     */
224    public static String getDateTime (Date d) {
225        return formatDate (d, "MMddHHmmss");
226    }
227        /**
228         * @param d date object to be formatted
229     * @param timeZone for GMT for example, use TimeZone.getTimeZone("GMT")
230     *        and for Uruguay use TimeZone.getTimeZone("GMT-03:00")
231     * @return date in MMddHHmmss format suitable for FIeld 7
232     */
233    public static String getDateTime (Date d, TimeZone timeZone) {
234        return formatDate (d, "MMddHHmmss", timeZone);
235    }
236    /**
237     * @return date in HHmmss format - suitable for field 12
238     */
239    public static String getTime (Date d) {
240        return formatDate (d, "HHmmss");
241    }
242    /**
243         * @param d date object to be formatted
244     * @param timeZone for GMT for example, use TimeZone.getTimeZone("GMT")
245     *        and for Uruguay use TimeZone.getTimeZone("GMT-03:00")
246     * @return date in HHmmss format - suitable for field 12
247     */
248    public static String getTime (Date d, TimeZone timeZone) {
249        return formatDate (d, "HHmmss", timeZone);
250    }
251    /**
252     * @return date in MMdd format - suitable for field 13
253     */
254    public static String getDate(Date d) {
255        return formatDate (d, "MMdd");
256    }
257    /**
258         * @param d date object to be formatted
259     * @param timeZone for GMT for example, use TimeZone.getTimeZone("GMT")
260     *        and for Uruguay use TimeZone.getTimeZone("GMT-03:00")
261     * @return date in MMdd format - suitable for field 13
262     */
263    public static String getDate(Date d, TimeZone timeZone) {
264        return formatDate (d, "MMdd", timeZone);
265    }
266    /**
267     * @return date in yyMMdd format - suitable for ANSI field 8
268     */
269    public static String getANSIDate(Date d) {
270        return formatDate (d, "yyMMdd");
271    }
272    /**
273         * @param d date object to be formatted
274     * @param timeZone for GMT for example, use TimeZone.getTimeZone("GMT")
275     *        and for Uruguay use TimeZone.getTimeZone("GMT-03:00")
276     * @return date in yyMMdd format - suitable for ANSI field 8
277     */
278    public static String getANSIDate(Date d, TimeZone timeZone) {
279        return formatDate (d, "yyMMdd", timeZone);
280    }
281    public static String getEuropeanDate(Date d) {
282        return formatDate (d, "ddMMyy");
283    }
284    public static String getEuropeanDate(Date d, TimeZone timeZone) {
285        return formatDate (d, "ddMMyy", timeZone);
286    }
287    /**
288     * @return date in yyMM format - suitable for field 14
289     */
290    public static String getExpirationDate(Date d) {
291        return formatDate (d, "yyMM");
292    }
293    /**
294         * @param d date object to be formatted
295     * @param timeZone for GMT for example, use TimeZone.getTimeZone("GMT")
296     *        and for Uruguay use TimeZone.getTimeZone("GMT-03:00")
297     * @return date in yyMM format - suitable for field 14
298     */
299    public static String getExpirationDate(Date d, TimeZone timeZone) {
300        return formatDate (d, "yyMM", timeZone);
301    }
302
303    /**
304     * @param d date object to be formatted
305     * @return date in YDDD format suitable for bit 31 or 37
306     * depending on interchange
307     */
308    public static String getJulianDate(Date d) {
309      String day = formatDate(d, "DDD", TimeZone.getDefault());
310      String year = formatDate(d, "yy", TimeZone.getDefault());
311      year = year.substring(1);
312      return year + day;
313    }
314
315    /**
316     * @param d date object to be formatted
317     * @param timeZone for GMT for example, use TimeZone.getTimeZone("GMT")
318     * and for Uruguay use TimeZone.getTimeZone("GMT-03:00")
319     * @return date in YDDD format suitable for bit 31 or 37
320     * depending on interchange
321     */
322    public static String getJulianDate(Date d, TimeZone timeZone) {
323      String day = formatDate(d, "DDD", timeZone);
324      String year = formatDate(d, "yy", timeZone);
325      year = year.substring(1);
326      return year + day;
327    }
328
329    /**
330     * Calculates the closest year in full YYYY format based on a two-digit year input.
331     * The closest year is determined in relation to the current year provided by the Calendar instance.
332     *
333     * @param year The two-digit year to be converted (e.g., 23 for 2023 or 2123).
334     * @param now  The current date provided as a Calendar instance used for reference.
335     * @return The closest full year in YYYY format.
336     * @throws IllegalArgumentException if the input year is not between 0 and 99.
337     */
338    private static int calculateNearestFullYear(int year, Calendar now) {
339        if (year < 0 || year > 99) {
340            throw new IllegalArgumentException("Year must be between 0 and 99");
341        }
342
343        int currentYear = now.get(Calendar.YEAR); // e.g., 2023
344        int currentCentury = currentYear - currentYear % 100; // e.g., 2000 for 2023
345        int possibleYear = currentCentury + year; // e.g., 2023 for year 23
346
347        // Adjust to the closest century if needed
348        if (Math.abs(year - currentYear % 100) > 50) {
349            possibleYear += (year > currentYear % 100) ? -100 : 100;
350        }
351        return possibleYear;
352    }
353
354    public static String formatDuration(Duration d) {
355        long days = d.toDays();
356        long hours = d.toHoursPart();
357        long minutes = d.toMinutesPart();
358        long seconds = d.toSecondsPart();
359        StringJoiner sj = new StringJoiner(", ");
360        if (days > 0)
361            sj.add(days + "d");
362        if (hours > 0)
363            sj.add(hours + "h");
364        if (minutes > 0)
365            sj.add(minutes + "m");
366        if (seconds > 0 || sj.length() == 0)
367            sj.add(seconds + "s");
368        return sj.toString();
369    }
370}