/* ====================================================================
Licensed to the Apache Software Foundation (ASF) Under one or more
contributor license agreements. See the NOTICE file distributed with
this work for Additional information regarding copyright ownership.
The ASF licenses this file to You Under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed Under the License is distributed on an "AS Is" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations Under the License.
==================================================================== */
namespace HH.WMS.Utils.NPOI.SS.UserModel
{
using System.Globalization;
using System;
using System.Text.RegularExpressions;
using System.Text;
///
/// Contains methods for dealing with Excel dates.
/// @author Michael Harhen
/// @author Glen Stampoultzis (glens at apache.org)
/// @author Dan Sherman (dsherman at Isisph.com)
/// @author Hack Kampbjorn (hak at 2mba.dk)
/// @author Alex Jacoby (ajacoby at gmail.com)
/// @author Pavel Krupets (pkrupets at palmtreebusiness dot com)
///
public class DateUtil
{
private static int SECONDS_PER_MINUTE = 60;
private static int MINUTES_PER_HOUR = 60;
private static int HOURS_PER_DAY = 24;
private static int SECONDS_PER_DAY = (HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE);
private static int BAD_DATE =
-1; // used to specify that date Is invalid
private static long DAY_MILLISECONDS = 24 * 60 * 60 * 1000;
private static char[] TIME_SEPARATOR_PATTERN = new char[] { ':' };
///
/// Given a Calendar, return the number of days since 1899/12/31.
///
/// the date
/// if set to true [use1904windowing].
/// number of days since 1899/12/31
public static int AbsoluteDay(DateTime cal, bool use1904windowing)
{
int daynum = (cal - new DateTime(1899, 12, 31)).Days;
if (cal > new DateTime(1900, 3, 1) && use1904windowing)
{
daynum++;
}
return daynum;
}
///
/// Given a Date, Converts it into a double representing its internal Excel representation,
/// which Is the number of days since 1/1/1900. Fractional days represent hours, minutes, and seconds.
///
/// Excel representation of Date (-1 if error - test for error by Checking for less than 0.1)
/// the Date
public static double GetExcelDate(DateTime date)
{
return GetExcelDate(date, false);
}
///
/// Gets the excel date.
///
/// The year.
/// The month.
/// The day.
/// The hour.
/// The minute.
/// The second.
/// Should 1900 or 1904 date windowing be used?
///
public static double GetExcelDate(int year, int month, int day, int hour, int minute, int second, bool use1904windowing)
{
if ((!use1904windowing && year < 1900) //1900 date system must bigger than 1900
|| (use1904windowing && year < 1904)) //1904 date system must bigger than 1904
{
return BAD_DATE;
}
DateTime startdate;
if (use1904windowing)
{
startdate = new DateTime(1904, 1, 1);
}
else
{
startdate = new DateTime(1900, 1, 1);
}
int nextyearmonth = 0;
if (month > 12)
{
nextyearmonth = month - 12;
month = 12;
}
int nextmonthday = 0;
if ((month == 1 || month == 3 || month == 5 || month == 7 || month == 8 || month == 10 || month == 12))
{
//big month
if (day > 31)
{
nextmonthday = day - 31;
day = 31;
}
}
else if ((month == 4 || month == 6 || month == 9 || month == 11))
{
//small month
if (day > 30)
{
nextmonthday = day - 30;
day = 30;
}
}
else if (DateTime.IsLeapYear(year))
{
//Feb. with leap year
if (day > 29)
{
nextmonthday = day - 29;
day = 29;
}
}
else
{
//Feb without leap year
if (day > 28)
{
nextmonthday = day - 28;
day = 28;
}
}
if(day<=0)
{
nextmonthday = day - 1;
day = 1;
}
DateTime date = new DateTime(year, month, day, hour, minute, second);
date = date.AddMonths(nextyearmonth);
date = date.AddDays(nextmonthday);
double value = (date - startdate).TotalDays + 1;
if (!use1904windowing && value >= 60)
{
value++;
}
else if (use1904windowing)
{
value--;
}
return value;
}
///
/// Given a Date, Converts it into a double representing its internal Excel representation,
/// which Is the number of days since 1/1/1900. Fractional days represent hours, minutes, and seconds.
///
/// The date.
/// Should 1900 or 1904 date windowing be used?
/// Excel representation of Date (-1 if error - test for error by Checking for less than 0.1)
public static double GetExcelDate(DateTime date, bool use1904windowing)
{
if ((!use1904windowing && date.Year < 1900) //1900 date system must bigger than 1900
|| (use1904windowing && date.Year < 1904)) //1904 date system must bigger than 1904
{
return BAD_DATE;
}
DateTime startdate;
if (use1904windowing)
{
startdate = new DateTime(1904, 1, 1);
}
else
{
startdate = new DateTime(1900, 1, 1);
}
double value = (date - startdate).TotalDays + 1;
if (!use1904windowing && value >= 60)
{
value++;
}
else if (use1904windowing)
{
value--;
}
return value;
}
///
/// Given an Excel date with using 1900 date windowing, and
/// Converts it to a Date.
///
/// The Excel date.
/// Java representation of the date, or null if date Is not a valid Excel date
///
/// NOTE: If the default
/// TimeZone
/// in Java uses Daylight
/// Saving Time then the conversion back to an Excel date may not give
/// the same value, that Is the comparison
/// excelDate == GetExcelDate(GetJavaDate(excelDate,false))
/// Is not always true. For example if default timezone Is
/// Europe/Copenhagen
/// , on 2004-03-28 the minute after
/// 01:59 CET Is 03:00 CEST, if the excel date represents a time between
/// 02:00 and 03:00 then it Is Converted to past 03:00 summer time
///
public static DateTime GetJavaDate(double date)
{
return GetJavaDate(date, false);
}
/**
* Given an Excel date with either 1900 or 1904 date windowing,
* Converts it to a Date.
*
* NOTE: If the default TimeZone in Java uses Daylight
* Saving Time then the conversion back to an Excel date may not give
* the same value, that Is the comparison
* excelDate == GetExcelDate(GetJavaDate(excelDate,false))
* Is not always true. For example if default timezone Is
* Europe/Copenhagen, on 2004-03-28 the minute after
* 01:59 CET Is 03:00 CEST, if the excel date represents a time between
* 02:00 and 03:00 then it Is Converted to past 03:00 summer time
*
* @param date The Excel date.
* @param use1904windowing true if date uses 1904 windowing,
* or false if using 1900 date windowing.
* @return Java representation of the date, or null if date Is not a valid Excel date
* @see TimeZone
*/
public static DateTime GetJavaDate(double date, bool use1904windowing)
{
if (!IsValidExcelDate(date))
{
throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, "Invalid Excel date double value: {0}", date));
}
int startYear = 1900;
int dayAdjust = -1; // Excel thinks 2/29/1900 Is a valid date, which it Isn't
int wholeDays = (int)Math.Floor(date);
if (use1904windowing)
{
startYear = 1904;
dayAdjust = 1; // 1904 date windowing uses 1/2/1904 as the first day
}
else if (wholeDays < 61)
{
// Date Is prior to 3/1/1900, so adjust because Excel thinks 2/29/1900 exists
// If Excel date == 2/29/1900, will become 3/1/1900 in Java representation
dayAdjust = 0;
}
DateTime startdate = new DateTime(startYear, 1, 1);
startdate = startdate.AddDays(wholeDays + dayAdjust - 1);
double millisecondsInDay = (int)((date - wholeDays) *
DAY_MILLISECONDS + 0.5);
return startdate.AddMilliseconds(millisecondsInDay);
}
///
/// Converts a string of format "HH:MM" or "HH:MM:SS" to its (Excel) numeric equivalent
///
/// The time STR.
/// a double between 0 and 1 representing the fraction of the day
public static double ConvertTime(String timeStr)
{
try
{
return ConvertTimeInternal(timeStr);
}
catch (FormatException e)
{
String msg = "Bad time format '" + timeStr
+ "' expected 'HH:MM' or 'HH:MM:SS' - " + e.Message;
throw new ArgumentException(msg);
}
}
///
/// Converts the time internal.
///
/// The time STR.
///
private static double ConvertTimeInternal(String timeStr)
{
int len = timeStr.Length;
if (len < 4 || len > 8)
{
throw new FormatException("Bad length");
}
String[] parts = timeStr.Split(TIME_SEPARATOR_PATTERN);
String secStr;
switch (parts.Length)
{
case 2: secStr = "00"; break;
case 3: secStr = parts[2]; break;
default:
throw new FormatException("Expected 2 or 3 fields but got (" + parts.Length + ")");
}
String hourStr = parts[0];
String minStr = parts[1];
int hours = ParseInt(hourStr, "hour", HOURS_PER_DAY);
int minutes = ParseInt(minStr, "minute", MINUTES_PER_HOUR);
int seconds = ParseInt(secStr, "second", SECONDS_PER_MINUTE);
double totalSeconds = seconds + (minutes + (hours) * 60) * 60;
return totalSeconds / (SECONDS_PER_DAY);
}
///
/// Given a format ID and its format String, will Check to see if the
/// format represents a date format or not.
/// Firstly, it will Check to see if the format ID corresponds to an
/// internal excel date format (eg most US date formats)
/// If not, it will Check to see if the format string only Contains
/// date formatting Chars (ymd-/), which covers most
/// non US date formats.
///
/// The index of the format, eg from ExtendedFormatRecord.GetFormatIndex
/// The format string, eg from FormatRecord.GetFormatString
///
/// true if [is A date format] [the specified format index]; otherwise, false.
///
public static bool IsADateFormat(int formatIndex, String formatString)
{
// First up, Is this an internal date format?
if (IsInternalDateFormat(formatIndex))
{
return true;
}
// If we didn't Get a real string, it can't be
if (formatString == null || formatString.Length == 0)
{
return false;
}
String fs = formatString;
// Translate \- into just -, before matching
fs = Regex.Replace(fs, "\\\\-", "-");
// And \, into ,
fs = Regex.Replace(fs, "\\\\,", ",");
// And \. into .
fs = Regex.Replace(fs,"\\\\.", ".");
// And '\ ' into ' '
fs = Regex.Replace(fs, "\\\\ ", " ");
// If it end in ;@, that's some crazy dd/mm vs mm/dd
// switching stuff, which we can ignore
fs = Regex.Replace(fs, ";@", "");
StringBuilder sb = new StringBuilder(fs.Length);
for (int i = 0; i < fs.Length; i++)
{
char c = fs[i];
if (i < fs.Length - 1)
{
char nc = fs[i + 1];
if (c == '\\')
{
switch (nc)
{
case '-':
case ',':
case '.':
case ' ':
case '\\':
// skip current '\' and continue to the next char
continue;
}
}
else if (c == ';' && nc == '@')
{
i++;
// skip ";@" duplets
continue;
}
}
sb.Append(c);
}
fs = sb.ToString();
// short-circuit if it indicates elapsed time: [h], [m] or [s]
if (Regex.IsMatch(fs, "^\\[([hH]+|[mM]+|[sS]+)\\]"))
{
return true;
}
// If it starts with [$-...], then could be a date, but
// who knows what that starting bit Is all about
fs = Regex.Replace(fs, "^\\[\\$\\-.*?\\]", "");
// If it starts with something like [Black] or [Yellow],
// then it could be a date
fs = Regex.Replace(fs, "^\\[[a-zA-Z]+\\]", "");
// You're allowed something like dd/mm/yy;[red]dd/mm/yy
// which would place dates before 1900/1904 in red
// For now, only consider the first one
if (fs.IndexOf(';') > 0 && fs.IndexOf(';') < fs.Length - 1)
{
fs = fs.Substring(0, fs.IndexOf(';'));
}
// Otherwise, Check it's only made up, in any case, of:
// y m d h s - / , . :
// optionally followed by AM/PM
if (Regex.IsMatch(fs, @"^[\[\]yYmMdDhHsS\-/,. :\""\\]+0*[ampAMP/]*$"))
{
return true;
}
return false;
}
///
/// Converts a string of format "YYYY/MM/DD" to its (Excel) numeric equivalent
///
/// The date STR.
/// a double representing the (integer) number of days since the start of the Excel epoch
public static DateTime ParseYYYYMMDDDate(String dateStr)
{
try
{
return ParseYYYYMMDDDateInternal(dateStr);
}
catch (FormatException e)
{
String msg = "Bad time format " + dateStr
+ " expected 'YYYY/MM/DD' - " + e.Message;
throw new ArgumentException(msg);
}
}
///
/// Parses the YYYYMMDD date internal.
///
/// The time string.
///
private static DateTime ParseYYYYMMDDDateInternal(String timeStr)
{
if (timeStr.Length != 10)
{
throw new FormatException("Bad length");
}
String yearStr = timeStr.Substring(0, 4);
String monthStr = timeStr.Substring(5, 2);
String dayStr = timeStr.Substring(8, 2);
int year = ParseInt(yearStr, "year", short.MinValue, short.MaxValue);
int month = ParseInt(monthStr, "month", 1, 12);
int day = ParseInt(dayStr, "day", 1, 31);
DateTime cal = new DateTime(year, month, day, 0, 0, 0);
return cal;
}
///
/// Parses the int.
///
/// The string value.
/// Name of the field.
/// The range max.
///
private static int ParseInt(String strVal, String fieldName, int rangeMax)
{
return ParseInt(strVal, fieldName, 0, rangeMax - 1);
}
///
/// Parses the int.
///
/// The STR val.
/// Name of the field.
/// The lower limit.
/// The upper limit.
///
private static int ParseInt(String strVal, String fieldName, int lowerLimit, int upperLimit)
{
int result;
try
{
result = int.Parse(strVal, CultureInfo.InvariantCulture);
}
catch (FormatException)
{
throw new FormatException("Bad int format '" + strVal + "' for " + fieldName + " field");
}
if (result < lowerLimit || result > upperLimit)
{
throw new FormatException(fieldName + " value (" + result
+ ") is outside the allowable range(0.." + upperLimit + ")");
}
return result;
}
///
/// Given a format ID this will Check whether the format represents an internal excel date format or not.
///
/// The format.
public static bool IsInternalDateFormat(int format)
{
bool retval = false;
switch (format)
{
// Internal Date Formats as described on page 427 in
// Microsoft Excel Dev's Kit...
case 0x0e:
case 0x0f:
case 0x10:
case 0x11:
case 0x12:
case 0x13:
case 0x14:
case 0x15:
case 0x16:
case 0x2d:
case 0x2e:
case 0x2f:
retval = true;
break;
default:
retval = false;
break;
}
return retval;
}
///
/// Check if a cell Contains a date
/// Since dates are stored internally in Excel as double values
/// we infer it Is a date if it Is formatted as such.
///
/// The cell.
public static bool IsCellDateFormatted(ICell cell)
{
if (cell == null) return false;
bool bDate = false;
double d = cell.NumericCellValue;
if (DateUtil.IsValidExcelDate(d))
{
ICellStyle style = cell.CellStyle;
if (style == null)
return false;
int i = style.DataFormat;
String f = style.GetDataFormatString();
bDate = IsADateFormat(i, f);
}
return bDate;
}
///
/// Check if a cell contains a date, Checking only for internal excel date formats.
/// As Excel stores a great many of its dates in "non-internal" date formats, you will not normally want to use this method.
///
/// The cell.
public static bool IsCellInternalDateFormatted(ICell cell)
{
if (cell == null) return false;
bool bDate = false;
double d = cell.NumericCellValue;
if (DateUtil.IsValidExcelDate(d))
{
ICellStyle style = cell.CellStyle;
int i = style.DataFormat;
bDate = IsInternalDateFormat(i);
}
return bDate;
}
///
/// Given a double, Checks if it Is a valid Excel date.
///
/// the double value.
///
/// true if [is valid excel date] [the specified value]; otherwise, false.
///
public static bool IsValidExcelDate(double value)
{
//return true;
return value > -Double.Epsilon;
}
}
}