Inspired by bug #44958 - Record level support for Data Tables. (No formula parser...
[poi.git] / src / java / org / apache / poi / hssf / usermodel / HSSFDateUtil.java
blobbe590c3a2d18eb2b13f31c994d9c5dc02d5137a2
1 /* ====================================================================
2 Licensed to the Apache Software Foundation (ASF) under one or more
3 contributor license agreements. See the NOTICE file distributed with
4 this work for additional information regarding copyright ownership.
5 The ASF licenses this file to You under the Apache License, Version 2.0
6 (the "License"); you may not use this file except in compliance with
7 the License. You may obtain a copy of the License at
9 http://www.apache.org/licenses/LICENSE-2.0
11 Unless required by applicable law or agreed to in writing, software
12 distributed under the License is distributed on an "AS IS" BASIS,
13 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 See the License for the specific language governing permissions and
15 limitations under the License.
16 ==================================================================== */
21 * DateUtil.java
23 * Created on January 19, 2002, 9:30 AM
25 package org.apache.poi.hssf.usermodel;
27 import java.util.Calendar;
28 import java.util.Date;
29 import java.util.GregorianCalendar;
31 /**
32 * Contains methods for dealing with Excel dates.
34 * @author Michael Harhen
35 * @author Glen Stampoultzis (glens at apache.org)
36 * @author Dan Sherman (dsherman at isisph.com)
37 * @author Hack Kampbjorn (hak at 2mba.dk)
38 * @author Alex Jacoby (ajacoby at gmail.com)
39 * @author Pavel Krupets (pkrupets at palmtreebusiness dot com)
42 public class HSSFDateUtil
44 private HSSFDateUtil()
48 private static final int BAD_DATE =
49 -1; // used to specify that date is invalid
50 private static final long DAY_MILLISECONDS = 24 * 60 * 60 * 1000;
52 /**
53 * Given a Date, converts it into a double representing its internal Excel representation,
54 * which is the number of days since 1/1/1900. Fractional days represent hours, minutes, and seconds.
56 * @return Excel representation of Date (-1 if error - test for error by checking for less than 0.1)
57 * @param date the Date
59 public static double getExcelDate(Date date) {
60 return getExcelDate(date, false);
62 /**
63 * Given a Date, converts it into a double representing its internal Excel representation,
64 * which is the number of days since 1/1/1900. Fractional days represent hours, minutes, and seconds.
66 * @return Excel representation of Date (-1 if error - test for error by checking for less than 0.1)
67 * @param date the Date
68 * @param use1904windowing Should 1900 or 1904 date windowing be used?
70 public static double getExcelDate(Date date, boolean use1904windowing) {
71 Calendar calStart = new GregorianCalendar();
72 calStart.setTime(date); // If date includes hours, minutes, and seconds, set them to 0
73 return internalGetExcelDate(calStart, use1904windowing);
75 /**
76 * Given a Date in the form of a Calendar, converts it into a double
77 * representing its internal Excel representation, which is the
78 * number of days since 1/1/1900. Fractional days represent hours,
79 * minutes, and seconds.
81 * @return Excel representation of Date (-1 if error - test for error by checking for less than 0.1)
82 * @param date the Calendar holding the date to convert
83 * @param use1904windowing Should 1900 or 1904 date windowing be used?
85 public static double getExcelDate(Calendar date, boolean use1904windowing) {
86 // Don't alter the supplied Calendar as we do our work
87 return internalGetExcelDate( (Calendar)date.clone(), use1904windowing );
89 private static double internalGetExcelDate(Calendar date, boolean use1904windowing) {
90 if ((!use1904windowing && date.get(Calendar.YEAR) < 1900) ||
91 (use1904windowing && date.get(Calendar.YEAR) < 1904))
93 return BAD_DATE;
94 } else {
95 // Because of daylight time saving we cannot use
96 // date.getTime() - calStart.getTimeInMillis()
97 // as the difference in milliseconds between 00:00 and 04:00
98 // can be 3, 4 or 5 hours but Excel expects it to always
99 // be 4 hours.
100 // E.g. 2004-03-28 04:00 CEST - 2004-03-28 00:00 CET is 3 hours
101 // and 2004-10-31 04:00 CET - 2004-10-31 00:00 CEST is 5 hours
102 double fraction = (((date.get(Calendar.HOUR_OF_DAY) * 60
103 + date.get(Calendar.MINUTE)
104 ) * 60 + date.get(Calendar.SECOND)
105 ) * 1000 + date.get(Calendar.MILLISECOND)
106 ) / ( double ) DAY_MILLISECONDS;
107 Calendar calStart = dayStart(date);
109 double value = fraction + absoluteDay(calStart, use1904windowing);
111 if (!use1904windowing && value >= 60) {
112 value++;
113 } else if (use1904windowing) {
114 value--;
117 return value;
122 * Given an Excel date with using 1900 date windowing, and
123 * converts it to a java.util.Date.
125 * NOTE: If the default <code>TimeZone</code> in Java uses Daylight
126 * Saving Time then the conversion back to an Excel date may not give
127 * the same value, that is the comparison
128 * <CODE>excelDate == getExcelDate(getJavaDate(excelDate,false))</CODE>
129 * is not always true. For example if default timezone is
130 * <code>Europe/Copenhagen</code>, on 2004-03-28 the minute after
131 * 01:59 CET is 03:00 CEST, if the excel date represents a time between
132 * 02:00 and 03:00 then it is converted to past 03:00 summer time
134 * @param date The Excel date.
135 * @return Java representation of the date, or null if date is not a valid Excel date
136 * @see java.util.TimeZone
138 public static Date getJavaDate(double date) {
139 return getJavaDate(date, false);
142 * Given an Excel date with either 1900 or 1904 date windowing,
143 * converts it to a java.util.Date.
145 * NOTE: If the default <code>TimeZone</code> in Java uses Daylight
146 * Saving Time then the conversion back to an Excel date may not give
147 * the same value, that is the comparison
148 * <CODE>excelDate == getExcelDate(getJavaDate(excelDate,false))</CODE>
149 * is not always true. For example if default timezone is
150 * <code>Europe/Copenhagen</code>, on 2004-03-28 the minute after
151 * 01:59 CET is 03:00 CEST, if the excel date represents a time between
152 * 02:00 and 03:00 then it is converted to past 03:00 summer time
154 * @param date The Excel date.
155 * @param use1904windowing true if date uses 1904 windowing,
156 * or false if using 1900 date windowing.
157 * @return Java representation of the date, or null if date is not a valid Excel date
158 * @see java.util.TimeZone
160 public static Date getJavaDate(double date, boolean use1904windowing) {
161 if (isValidExcelDate(date)) {
162 int startYear = 1900;
163 int dayAdjust = -1; // Excel thinks 2/29/1900 is a valid date, which it isn't
164 int wholeDays = (int)Math.floor(date);
165 if (use1904windowing) {
166 startYear = 1904;
167 dayAdjust = 1; // 1904 date windowing uses 1/2/1904 as the first day
169 else if (wholeDays < 61) {
170 // Date is prior to 3/1/1900, so adjust because Excel thinks 2/29/1900 exists
171 // If Excel date == 2/29/1900, will become 3/1/1900 in Java representation
172 dayAdjust = 0;
174 GregorianCalendar calendar = new GregorianCalendar(startYear,0,
175 wholeDays + dayAdjust);
176 int millisecondsInDay = (int)((date - Math.floor(date)) *
177 DAY_MILLISECONDS + 0.5);
178 calendar.set(GregorianCalendar.MILLISECOND, millisecondsInDay);
179 return calendar.getTime();
181 else {
182 return null;
187 * Given a format ID and its format String, will check to see if the
188 * format represents a date format or not.
189 * Firstly, it will check to see if the format ID corresponds to an
190 * internal excel date format (eg most US date formats)
191 * If not, it will check to see if the format string only contains
192 * date formatting characters (ymd-/), which covers most
193 * non US date formats.
195 * @param formatIndex The index of the format, eg from ExtendedFormatRecord.getFormatIndex
196 * @param formatString The format string, eg from FormatRecord.getFormatString
197 * @see #isInternalDateFormat(int)
199 public static boolean isADateFormat(int formatIndex, String formatString) {
200 // First up, is this an internal date format?
201 if(isInternalDateFormat(formatIndex)) {
202 return true;
205 // If we didn't get a real string, it can't be
206 if(formatString == null || formatString.length() == 0) {
207 return false;
210 String fs = formatString;
212 // Translate \- into just -, before matching
213 fs = fs.replaceAll("\\\\-","-");
214 // And \, into ,
215 fs = fs.replaceAll("\\\\,",",");
216 // And '\ ' into ' '
217 fs = fs.replaceAll("\\\\ "," ");
219 // If it end in ;@, that's some crazy dd/mm vs mm/dd
220 // switching stuff, which we can ignore
221 fs = fs.replaceAll(";@", "");
223 // If it starts with [$-...], then could be a date, but
224 // who knows what that starting bit is all about
225 fs = fs.replaceAll("^\\[\\$\\-.*?\\]", "");
227 // If it starts with something like [Black] or [Yellow],
228 // then it could be a date
229 fs = fs.replaceAll("^\\[[a-zA-Z]+\\]", "");
231 // Otherwise, check it's only made up, in any case, of:
232 // y m d h s - / , . :
233 // optionally followed by AM/PM
234 if(fs.matches("^[yYmMdDhHsS\\-/,. :]+[ampAMP]*$")) {
235 return true;
238 return false;
242 * Given a format ID this will check whether the format represents
243 * an internal excel date format or not.
244 * @see #isADateFormat(int, java.lang.String)
246 public static boolean isInternalDateFormat(int format) {
247 boolean retval =false;
249 switch(format) {
250 // Internal Date Formats as described on page 427 in
251 // Microsoft Excel Dev's Kit...
252 case 0x0e:
253 case 0x0f:
254 case 0x10:
255 case 0x11:
256 case 0x12:
257 case 0x13:
258 case 0x14:
259 case 0x15:
260 case 0x16:
261 case 0x2d:
262 case 0x2e:
263 case 0x2f:
264 retval = true;
265 break;
267 default:
268 retval = false;
269 break;
271 return retval;
275 * Check if a cell contains a date
276 * Since dates are stored internally in Excel as double values
277 * we infer it is a date if it is formatted as such.
278 * @see #isADateFormat(int, String)
279 * @see #isInternalDateFormat(int)
281 public static boolean isCellDateFormatted(HSSFCell cell) {
282 if (cell == null) return false;
283 boolean bDate = false;
285 double d = cell.getNumericCellValue();
286 if ( HSSFDateUtil.isValidExcelDate(d) ) {
287 HSSFCellStyle style = cell.getCellStyle();
288 int i = style.getDataFormat();
289 String f = style.getDataFormatString(cell.getBoundWorkbook());
290 bDate = isADateFormat(i, f);
292 return bDate;
295 * Check if a cell contains a date, checking only for internal
296 * excel date formats.
297 * As Excel stores a great many of its dates in "non-internal"
298 * date formats, you will not normally want to use this method.
299 * @see #isADateFormat(int,String)
300 * @see #isInternalDateFormat(int)
302 public static boolean isCellInternalDateFormatted(HSSFCell cell) {
303 if (cell == null) return false;
304 boolean bDate = false;
306 double d = cell.getNumericCellValue();
307 if ( HSSFDateUtil.isValidExcelDate(d) ) {
308 HSSFCellStyle style = cell.getCellStyle();
309 int i = style.getDataFormat();
310 bDate = isInternalDateFormat(i);
312 return bDate;
317 * Given a double, checks if it is a valid Excel date.
319 * @return true if valid
320 * @param value the double value
323 public static boolean isValidExcelDate(double value)
325 return (value > -Double.MIN_VALUE);
329 * Given a Calendar, return the number of days since 1900/12/31.
331 * @return days number of days since 1900/12/31
332 * @param cal the Calendar
333 * @exception IllegalArgumentException if date is invalid
336 static int absoluteDay(Calendar cal, boolean use1904windowing)
338 return cal.get(Calendar.DAY_OF_YEAR)
339 + daysInPriorYears(cal.get(Calendar.YEAR), use1904windowing);
343 * Return the number of days in prior years since 1900
345 * @return days number of days in years prior to yr.
346 * @param yr a year (1900 < yr < 4000)
347 * @param use1904windowing
348 * @exception IllegalArgumentException if year is outside of range.
351 private static int daysInPriorYears(int yr, boolean use1904windowing)
353 if ((!use1904windowing && yr < 1900) || (use1904windowing && yr < 1900)) {
354 throw new IllegalArgumentException("'year' must be 1900 or greater");
357 int yr1 = yr - 1;
358 int leapDays = yr1 / 4 // plus julian leap days in prior years
359 - yr1 / 100 // minus prior century years
360 + yr1 / 400 // plus years divisible by 400
361 - 460; // leap days in previous 1900 years
363 return 365 * (yr - (use1904windowing ? 1904 : 1900)) + leapDays;
366 // set HH:MM:SS fields of cal to 00:00:00:000
367 private static Calendar dayStart(final Calendar cal)
369 cal.get(Calendar
370 .HOUR_OF_DAY); // force recalculation of internal fields
371 cal.set(Calendar.HOUR_OF_DAY, 0);
372 cal.set(Calendar.MINUTE, 0);
373 cal.set(Calendar.SECOND, 0);
374 cal.set(Calendar.MILLISECOND, 0);
375 cal.get(Calendar
376 .HOUR_OF_DAY); // force recalculation of internal fields
377 return cal;
380 // ---------------------------------------------------------------------------------------------------------