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