diff options
author | Eike Rathke <erack@redhat.com> | 2016-07-08 17:08:47 +0200 |
---|---|---|
committer | Eike Rathke <erack@redhat.com> | 2016-07-08 20:41:02 +0000 |
commit | 6d4f2dcc7cbba771e9d9b00de50368db4a88ef1b (patch) | |
tree | 0301896941b955ffa79ef3e96b874ebdad78662c /tools | |
parent | 06287b9c348281612854d67c4eb2e7a38dc722ca (diff) |
Resolves: tdf#100452 class Date full (BCE,CE) proleptic Gregorian calendar
... implementing signed years with year 0 gap.
Date(31,12,-1) last day BCE
Date(1,1,1) first day CE
New class Date member functions:
* AddYears(sal_Int16) to be used instead of
aDate.SetYear(aDate.GetYear()+sal_Int16) to handle year 0 gap.
* convenience GetNextYear() to be used insted of GetYear()+1
* convenience GetPrevYear() to be used insted of GetYear()-1
* AddMonths(sal_Int32)
* operator=(const css::util::Date&)
New class DateTime member functions:
* operator=(const css::util::DateTime&)
Made some conversion ctors explicit, specifically Date(sal_Int32)
Adapted hopefully all places that used a sal_uInt16 year to use
sal_Int16 where appropriate.
Eliminated some quirks in date handling found on the fly.
Added era handling to i18npool icu calendar setting interface, which
missing was responsible for 0001-01-01 entered in Calc being set as
-0001-01-01, hence subtracting one day resulted in -0002-12-31.
Change-Id: I77b39fba9599ebd5067d7864f6c9ebe01f6f578f
Reviewed-on: https://gerrit.libreoffice.org/27049
Reviewed-by: Eike Rathke <erack@redhat.com>
Tested-by: Jenkins <ci@libreoffice.org>
Diffstat (limited to 'tools')
-rw-r--r-- | tools/CppunitTest_tools_test.mk | 1 | ||||
-rw-r--r-- | tools/qa/cppunit/test_date.cxx | 76 | ||||
-rw-r--r-- | tools/source/datetime/datetime.cxx | 7 | ||||
-rw-r--r-- | tools/source/datetime/tdate.cxx | 261 |
4 files changed, 280 insertions, 65 deletions
diff --git a/tools/CppunitTest_tools_test.mk b/tools/CppunitTest_tools_test.mk index e90b52457244..aced4bf6698a 100644 --- a/tools/CppunitTest_tools_test.mk +++ b/tools/CppunitTest_tools_test.mk @@ -15,6 +15,7 @@ $(eval $(call gb_CppunitTest_use_external,tools_test,boost_headers)) $(eval $(call gb_CppunitTest_add_exception_objects,tools_test, \ tools/qa/cppunit/test_bigint \ + tools/qa/cppunit/test_date \ tools/qa/cppunit/test_fract \ tools/qa/cppunit/test_inetmime \ tools/qa/cppunit/test_pathutils \ diff --git a/tools/qa/cppunit/test_date.cxx b/tools/qa/cppunit/test_date.cxx new file mode 100644 index 000000000000..0ee30089b7b5 --- /dev/null +++ b/tools/qa/cppunit/test_date.cxx @@ -0,0 +1,76 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include <cppunit/TestFixture.h> +#include <cppunit/extensions/HelperMacros.h> + +#include <tools/date.hxx> + +namespace tools +{ + +class DateTest : public CppUnit::TestFixture +{ +public: + void testDate(); + + CPPUNIT_TEST_SUITE(DateTest); + CPPUNIT_TEST(testDate); + CPPUNIT_TEST_SUITE_END(); +}; + +void DateTest::testDate() +{ + const Date aCE(1,1,1); // first day CE + const Date aBCE(31,12,-1); // last day BCE + const Date aMin(1,1,-32768); // minimum date + const Date aMax(31,12,32767); // maximum date + Date aDate(Date::EMPTY); + const long kMinDays = -11968265; + const long kMaxDays = 11967900; + + // Last day BCE to first day CE is 1 day difference. + CPPUNIT_ASSERT_EQUAL( static_cast<long>(1), aCE - aBCE); + CPPUNIT_ASSERT_EQUAL( static_cast<long>(-1), aBCE - aCE); + aDate = aBCE; + CPPUNIT_ASSERT_EQUAL( aCE.GetDate(), (aDate += 1).GetDate()); + aDate = aCE; + CPPUNIT_ASSERT_EQUAL( aBCE.GetDate(), (aDate -= 1).GetDate()); + + // The entire BCE and CE ranges cover that many days. Day 0 is -0001-12-31 + CPPUNIT_ASSERT_EQUAL( kMaxDays, aMax - aBCE); + CPPUNIT_ASSERT_EQUAL( kMinDays, aMin - aBCE); + + // Truncate at limits, not under-/overflow or wrap. + aDate = aMin; + CPPUNIT_ASSERT_EQUAL( aMin.GetDate(), (aDate -= 1).GetDate()); + aDate = aMax; + CPPUNIT_ASSERT_EQUAL( aMax.GetDate(), (aDate += 1).GetDate()); + aDate = aBCE; + CPPUNIT_ASSERT_EQUAL( aMin.GetDate(), (aDate += (kMinDays-10)).GetDate()); + aDate = aBCE; + CPPUNIT_ASSERT_EQUAL( aMax.GetDate(), (aDate += (kMaxDays+10)).GetDate()); + + // Year -1 is a leap year. + aDate = Date(28,2,-1); + CPPUNIT_ASSERT_EQUAL( Date(29,2,-1).GetDate(), (aDate += 1).GetDate()); + aDate = Date(1,3,-1); + CPPUNIT_ASSERT_EQUAL( Date(29,2,-1).GetDate(), (aDate -= 1).GetDate()); + // Year -5 is a leap year. + aDate = Date(28,2,-5); + CPPUNIT_ASSERT_EQUAL( Date(29,2,-5).GetDate(), (aDate += 1).GetDate()); + aDate = Date(1,3,-5); + CPPUNIT_ASSERT_EQUAL( Date(29,2,-5).GetDate(), (aDate -= 1).GetDate()); +} + +CPPUNIT_TEST_SUITE_REGISTRATION(DateTest); + +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */ diff --git a/tools/source/datetime/datetime.cxx b/tools/source/datetime/datetime.cxx index cc0fc13a9ae6..d24f6f818184 100644 --- a/tools/source/datetime/datetime.cxx +++ b/tools/source/datetime/datetime.cxx @@ -25,6 +25,13 @@ DateTime::DateTime( const css::util::DateTime& rDateTime ) { } +DateTime& DateTime::operator =( const css::util::DateTime& rUDateTime ) +{ + Date::operator=( Date( rUDateTime.Day, rUDateTime.Month, rUDateTime.Year)); + Time::operator=( Time( rUDateTime)); + return *this; +} + bool DateTime::IsBetween( const DateTime& rFrom, const DateTime& rTo ) const { if ( (*this >= rFrom) && (*this <= rTo) ) diff --git a/tools/source/datetime/tdate.cxx b/tools/source/datetime/tdate.cxx index 78b1ec63ac70..e4556be8c1c0 100644 --- a/tools/source/datetime/tdate.cxx +++ b/tools/source/datetime/tdate.cxx @@ -29,36 +29,64 @@ static const sal_uInt16 aDaysInMonth[12] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; -// The number of days we internally handle. +// Once upon a time the number of days we internally handled was limited to +// MAX_DAYS 3636532. That changed with a full 16-bit year. // Assuming the first valid positive date in a proleptic Gregorian calendar is -// 0001-01-01, this results in an end date of 9957-06-26. +// 0001-01-01, this resulted in an end date of 9957-06-26. // Hence we documented that years up to and including 9956 are handled. /* XXX: it is unclear history why this value was chosen, the representable * 9999-12-31 would be 3652060 days from 0001-01-01. Even 9998-12-31 to - * distinguish from a maximum possible date would be 3651695. Better keep the - * value in case something somewhere relies on it or there is another reason - * beyond.. - * At least connectivity/source/commontools/dbconversion.cxx has the same - * value to calculate with css::util::Date */ -#define MAX_DAYS 3636532 + * distinguish from a maximum possible date would be 3651695. + * There is connectivity/source/commontools/dbconversion.cxx that still has the + * same value to calculate with css::util::Date */ +/* XXX can that dbconversion cope with years > 9999 or negative years at all? + * Database fields may be limited to positive 4 digits. */ + +static const long MIN_DAYS = -11968265; // -32768-01-01 +static const long MAX_DAYS = 11967900; // 32767-12-31 namespace { -inline long ImpYearToDays( sal_uInt16 nYear ) +const sal_Int16 kYearMax = SAL_MAX_INT16; +const sal_Int16 kYearMin = SAL_MIN_INT16; + +// Days until start of year from zero, so month and day of month can be added. +// year 1 => 0 days, year 2 => 365 days, ... +// year -1 => -366 days, year -2 => -731 days, ... +inline long ImpYearToDays( sal_Int16 nYear ) { - const long nYr(static_cast<long>(nYear) - 1); - return nYr*365 + nYr/4 - nYr/100 + nYr/400; + assert( nYear != 0 ); + long nOffset; + long nYr; + if (nYear < 0) + { + nOffset = -366; + nYr = nYear + 1; + } + else + { + nOffset = 0; + nYr = nYear - 1; + } + return nOffset + nYr*365 + nYr/4 - nYr/100 + nYr/400; } -inline bool ImpIsLeapYear( sal_uInt16 nYear ) +inline bool ImpIsLeapYear( sal_Int16 nYear ) { + // Leap years BCE are -1, -5, -9, ... + // See + // https://en.wikipedia.org/wiki/Proleptic_Gregorian_calendar#Usage + // https://en.wikipedia.org/wiki/0_(year)#History_of_astronomical_usage + assert( nYear != 0 ); + if (nYear < 0) + nYear = -nYear - 1; return ( ( ((nYear % 4) == 0) && ((nYear % 100) != 0) ) || ( (nYear % 400) == 0 ) ); } // All callers must have sanitized or normalized month and year values! -inline sal_uInt16 ImplDaysInMonth( sal_uInt16 nMonth, sal_uInt16 nYear ) +inline sal_uInt16 ImplDaysInMonth( sal_uInt16 nMonth, sal_Int16 nYear ) { if ( nMonth != 2 ) return aDaysInMonth[nMonth-1]; @@ -73,8 +101,29 @@ inline sal_uInt16 ImplDaysInMonth( sal_uInt16 nMonth, sal_uInt16 nYear ) } +void Date::setDateFromDMY( sal_uInt16 nDay, sal_uInt16 nMonth, sal_Int16 nYear ) +{ + SAL_WARN_IF( nYear == 0, "tools","Date::setDateFromDMY - sure about 0 year? It's not in the calendar."); + if (nYear < 0) + mnDate = + (static_cast<sal_Int32>( nYear ) * 10000) - + (static_cast<sal_Int32>( nMonth % 100 ) * 100) - + (static_cast<sal_Int32>( nDay % 100 )); + else + mnDate = + (static_cast<sal_Int32>( nYear ) * 10000) + + (static_cast<sal_Int32>( nMonth % 100 ) * 100) + + (static_cast<sal_Int32>( nDay % 100 )); +} + +void Date::SetDate( sal_Int32 nNewDate ) +{ + assert( ((nNewDate / 10000) != 0) && "you don't want to set a 0 year, do you?" ); + mnDate = nNewDate; +} + // static -sal_uInt16 Date::GetDaysInMonth( sal_uInt16 nMonth, sal_uInt16 nYear ) +sal_uInt16 Date::GetDaysInMonth( sal_uInt16 nMonth, sal_Int16 nYear ) { SAL_WARN_IF( nMonth < 1 || 12 < nMonth, "tools", "Date::GetDaysInMonth - nMonth out of bounds " << nMonth); if (nMonth < 1) @@ -87,7 +136,7 @@ sal_uInt16 Date::GetDaysInMonth( sal_uInt16 nMonth, sal_uInt16 nYear ) long Date::GetAsNormalizedDays() const { // This is a very common datum we often calculate from. - if (nDate == 18991230) // 1899-12-30 + if (mnDate == 18991230) // 1899-12-30 { assert(DateToDays( GetDay(), GetMonth(), GetYear() ) == 693594); return 693594; @@ -95,7 +144,7 @@ long Date::GetAsNormalizedDays() const return DateToDays( GetDay(), GetMonth(), GetYear() ); } -long Date::DateToDays( sal_uInt16 nDay, sal_uInt16 nMonth, sal_uInt16 nYear ) +long Date::DateToDays( sal_uInt16 nDay, sal_uInt16 nMonth, sal_Int16 nYear ) { Normalize( nDay, nMonth, nYear); @@ -108,25 +157,28 @@ long Date::DateToDays( sal_uInt16 nDay, sal_uInt16 nMonth, sal_uInt16 nYear ) static Date lcl_DaysToDate( long nDays ) { + if ( nDays <= MIN_DAYS ) + return Date( 1, 1, kYearMin ); if ( nDays >= MAX_DAYS ) - return Date( 31, 12, 9999 ); - - if ( nDays <= 0 ) - return Date( 1, 0, 0 ); + return Date( 31, 12, kYearMax ); - long nTempDays; - long i = 0; - bool bCalc; + // Day 0 is -0001-12-31, day 1 is 0001-01-01 + const sal_Int16 nSign = (nDays <= 0 ? -1 : 1); + long nTempDays; + long i = 0; + bool bCalc; - sal_uInt16 nYear; + sal_Int16 nYear; do { - nYear = (sal_uInt16)((nDays / 365) - i); + nYear = static_cast<sal_Int16>((nDays / 365) - (i * nSign)); + if (nYear == 0) + nYear = nSign; nTempDays = nDays - ImpYearToDays(nYear); bCalc = false; if ( nTempDays < 1 ) { - i++; + i += nSign; bCalc = true; } else @@ -135,7 +187,7 @@ static Date lcl_DaysToDate( long nDays ) { if ( (nTempDays != 366) || !ImpIsLeapYear( nYear ) ) { - i--; + i -= nSign; bCalc = true; } } @@ -147,7 +199,7 @@ static Date lcl_DaysToDate( long nDays ) while ( nTempDays > static_cast<long>(ImplDaysInMonth( nMonth, nYear )) ) { nTempDays -= ImplDaysInMonth( nMonth, nYear ); - nMonth++; + ++nMonth; } return Date( static_cast<sal_uInt16>(nTempDays), nMonth, nYear ); @@ -195,11 +247,73 @@ void Date::SetMonth( sal_uInt16 nNewMonth ) setDateFromDMY( GetDay(), nNewMonth, GetYear() ); } -void Date::SetYear( sal_uInt16 nNewYear ) +void Date::SetYear( sal_Int16 nNewYear ) { + assert( nNewYear != 0 ); setDateFromDMY( GetDay(), GetMonth(), nNewYear ); } +void Date::AddYears( sal_Int16 nAddYears ) +{ + sal_Int16 nYear = GetYear(); + if (nYear < 0) + { + if (nAddYears < 0) + { + if (nYear < kYearMin - nAddYears) + nYear = kYearMin; + else + nYear += nAddYears; + } + else + { + nYear += nAddYears; + if (nYear == 0) + nYear = 1; + } + } + else + { + if (nAddYears > 0) + { + if (kYearMax - nAddYears < nYear) + nYear = kYearMax; + else + nYear += nAddYears; + } + else + { + nYear += nAddYears; + if (nYear == 0) + nYear = -1; + } + } + + SetYear( nYear ); + if (GetMonth() == 2 && GetDay() == 29 && !ImpIsLeapYear( nYear)) + SetDay(28); +} + +void Date::AddMonths( sal_Int32 nAddMonths ) +{ + long nMonths = GetMonth() + nAddMonths; + long nNewMonth = nMonths % 12; + long nYear = GetYear() + nMonths / 12; + if( nMonths <= 0 || nNewMonth == 0 ) + --nYear; + if( nNewMonth <= 0 ) + nNewMonth += 12; + if (nYear == 0) + nYear = (nAddMonths < 0 ? -1 : 1); + else if (nYear < kYearMin) + nYear = kYearMin; + else if (nYear > kYearMax) + nYear = kYearMax; + SetMonth( static_cast<sal_uInt16>(nNewMonth) ); + SetYear( static_cast<sal_Int16>(nYear) ); + Normalize(); +} + DayOfWeek Date::GetDayOfWeek() const { return static_cast<DayOfWeek>((GetAsNormalizedDays()-1) % 7); @@ -209,7 +323,7 @@ sal_uInt16 Date::GetDayOfYear() const { sal_uInt16 nDay = GetDay(); sal_uInt16 nMonth = GetMonth(); - sal_uInt16 nYear = GetYear(); + sal_Int16 nYear = GetYear(); Normalize( nDay, nMonth, nYear); for( sal_uInt16 i = 1; i < nMonth; i++ ) @@ -245,7 +359,7 @@ sal_uInt16 Date::GetWeekOfYear( DayOfWeek eStartDay, else if ( nWeek == 53 ) { short nDaysInYear = (short)GetDaysInYear(); - short nDaysNextYear = (short)Date( 1, 1, GetYear()+1 ).GetDayOfWeek(); + short nDaysNextYear = (short)Date( 1, 1, GetNextYear() ).GetDayOfWeek(); nDaysNextYear = (nDaysNextYear+(7-(short)eStartDay)) % 7; if ( nDayOfYear > (nDaysInYear-nDaysNextYear-1) ) nWeek = 1; @@ -257,7 +371,7 @@ sal_uInt16 Date::GetWeekOfYear( DayOfWeek eStartDay, // First week of a year is equal to the last week of the previous year if ( nWeek == 0 ) { - Date aLastDatePrevYear( 31, 12, GetYear()-1 ); + Date aLastDatePrevYear( 31, 12, GetPrevYear() ); nWeek = aLastDatePrevYear.GetWeekOfYear( eStartDay, nMinimumNumberOfDaysInWeek ); } } @@ -273,7 +387,7 @@ sal_uInt16 Date::GetWeekOfYear( DayOfWeek eStartDay, else if ( n1WDay == nMinimumNumberOfDaysInWeek + 1 ) { // Year after leapyear - if ( Date( 1, 1, GetYear()-1 ).IsLeapYear() ) + if ( Date( 1, 1, GetPrevYear() ).IsLeapYear() ) nWeek = 53; else nWeek = 52; @@ -307,7 +421,7 @@ sal_uInt16 Date::GetDaysInMonth() const { sal_uInt16 nDay = GetDay(); sal_uInt16 nMonth = GetMonth(); - sal_uInt16 nYear = GetYear(); + sal_Int16 nYear = GetYear(); Normalize( nDay, nMonth, nYear); return ImplDaysInMonth( nMonth, nYear ); @@ -315,7 +429,7 @@ sal_uInt16 Date::GetDaysInMonth() const bool Date::IsLeapYear() const { - sal_uInt16 nYear = GetYear(); + sal_Int16 nYear = GetYear(); return ImpIsLeapYear( nYear ); } @@ -323,7 +437,7 @@ bool Date::IsValidAndGregorian() const { sal_uInt16 nDay = GetDay(); sal_uInt16 nMonth = GetMonth(); - sal_uInt16 nYear = GetYear(); + sal_Int16 nYear = GetYear(); if ( !nMonth || (nMonth > 12) ) return false; @@ -348,7 +462,7 @@ bool Date::IsValidDate() const } //static -bool Date::IsValidDate( sal_uInt16 nDay, sal_uInt16 nMonth, sal_uInt16 nYear ) +bool Date::IsValidDate( sal_uInt16 nDay, sal_uInt16 nMonth, sal_Int16 nYear ) { if ( !nMonth || (nMonth > 12) ) return false; @@ -361,7 +475,7 @@ bool Date::Normalize() { sal_uInt16 nDay = GetDay(); sal_uInt16 nMonth = GetMonth(); - sal_uInt16 nYear = GetYear(); + sal_Int16 nYear = GetYear(); if (!Normalize( nDay, nMonth, nYear)) return false; @@ -372,7 +486,7 @@ bool Date::Normalize() } //static -bool Date::Normalize( sal_uInt16 & rDay, sal_uInt16 & rMonth, sal_uInt16 & rYear ) +bool Date::Normalize( sal_uInt16 & rDay, sal_uInt16 & rMonth, sal_Int16 & rYear ) { if (IsValidDate( rDay, rMonth, rYear)) return false; @@ -381,42 +495,59 @@ bool Date::Normalize( sal_uInt16 & rDay, sal_uInt16 & rMonth, sal_uInt16 & rYear { rYear += rMonth / 12; rMonth = rMonth % 12; + if (rYear == 0) + rYear = 1; } - if (!rMonth) + if (rMonth == 0) { - if (!rYear) + --rYear; + if (rYear == 0) + rYear = -1; + rMonth = 12; + } + + if (rYear < 0) + { + sal_uInt16 nDays; + while (rDay > (nDays = ImplDaysInMonth( rMonth, rYear))) { - rYear = 0; - rMonth = 1; - if (rDay > 31) - rDay -= 31; + rDay -= nDays; + if (rMonth > 1) + --rMonth; else - rDay = 1; - } - else - { - --rYear; - rMonth = 12; + { + if (rYear == kYearMin) + { + rDay = 1; + rMonth = 1; + return true; + } + --rYear; + rMonth = 12; + } } } - sal_uInt16 nDays; - while (rDay > (nDays = ImplDaysInMonth( rMonth, rYear))) + else { - rDay -= nDays; - if (rMonth < 12) - ++rMonth; - else + sal_uInt16 nDays; + while (rDay > (nDays = ImplDaysInMonth( rMonth, rYear))) { - ++rYear; - rMonth = 1; + rDay -= nDays; + if (rMonth < 12) + ++rMonth; + else + { + if (rYear == kYearMax) + { + rDay = 31; + rMonth = 12; + return true; + } + ++rYear; + rMonth = 1; + } } } - if (rYear > 9999) - { - rDay = 31; - rMonth = 12; - rYear = 9999; - } return true; } |