1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ 2 /* 3 * This file is part of the LibreOffice project. 4 * 5 * This Source Code Form is subject to the terms of the Mozilla Public 6 * License, v. 2.0. If a copy of the MPL was not distributed with this 7 * file, You can obtain one at http://mozilla.org/MPL/2.0/. 8 * 9 * This file incorporates work covered by the following license notice: 10 * 11 * Licensed to the Apache Software Foundation (ASF) under one or more 12 * contributor license agreements. See the NOTICE file distributed 13 * with this work for additional information regarding copyright 14 * ownership. The ASF licenses this file to you under the Apache 15 * License, Version 2.0 (the "License"); you may not use this file 16 * except in compliance with the License. You may obtain a copy of 17 * the License at http://www.apache.org/licenses/LICENSE-2.0 . 18 */ 19 20 #include <algorithm> 21 22 #include <calendar_gregorian.hxx> 23 #include <localedata.hxx> 24 #include <nativenumbersupplier.hxx> 25 #include <com/sun/star/i18n/CalendarDisplayCode.hpp> 26 #include <com/sun/star/i18n/CalendarDisplayIndex.hpp> 27 #include <com/sun/star/i18n/NativeNumberMode.hpp> 28 #include <com/sun/star/i18n/reservedWords.hpp> 29 #include <cppuhelper/supportsservice.hxx> 30 #include <rtl/math.hxx> 31 #include <sal/log.hxx> 32 33 #include <stdio.h> 34 #include <string.h> 35 36 #define erDUMP_ICU_CALENDAR 0 37 #define erDUMP_I18N_CALENDAR 0 38 #if erDUMP_ICU_CALENDAR || erDUMP_I18N_CALENDAR 39 // If both are used, DUMP_ICU_CAL_MSG() must be used before DUMP_I18N_CAL_MSG() 40 // to obtain internally set values from ICU, else Calendar::get() calls in 41 // DUMP_I18N_CAL_MSG() recalculate! 42 43 // These pieces of macro are shamelessly borrowed from icu's olsontz.cpp, the 44 // double parens'ed approach to pass multiple parameters as one macro parameter 45 // is appealing. 46 static void debug_cal_loc(const char *f, int32_t l) 47 { 48 fprintf(stderr, "%s:%d: ", f, l); 49 } 50 # include <stdarg.h> 51 static void debug_cal_msg(const char *pat, ...) 52 { 53 va_list ap; 54 va_start(ap, pat); 55 vfprintf(stderr, pat, ap); 56 va_end(ap); 57 } 58 59 #if erDUMP_ICU_CALENDAR 60 // Make icu with 61 // DEFS = -DU_DEBUG_CALSVC -DUCAL_DEBUG_DUMP 62 // in workdir/UnpackedTarball/icu/source/icudefs.mk 63 // May need some patches to fix unmaintained things there. 64 extern void ucal_dump( const icu::Calendar & ); 65 static void debug_icu_cal_dump( const ::icu::Calendar & r ) 66 { 67 ucal_dump(r); 68 fflush(stderr); 69 // set a breakpoint here to pause display between dumps 70 } 71 // must use double parens, i.e.: DUMP_ICU_CAL_MSG(("four is: %d",4)); 72 #define DUMP_ICU_CAL_MSG(x) {debug_cal_loc(__FILE__,__LINE__);debug_cal_msg x;debug_icu_cal_dump(*body);} 73 #else // erDUMP_ICU_CALENDAR 74 #define DUMP_ICU_CAL_MSG(x) 75 #endif // erDUMP_ICU_CALENDAR 76 77 #if erDUMP_I18N_CALENDAR 78 static void debug_cal_millis_to_time( long nMillis, long & h, long & m, long & s, long & f ) 79 { 80 int sign = (nMillis < 0 ? -1 : 1); 81 nMillis = ::std::abs(nMillis); 82 h = sign * nMillis / (60 * 60 * 1000); 83 nMillis -= sign * h * (60 * 60 * 1000); 84 m = nMillis / (60 * 1000); 85 nMillis -= m * (60 * 1000); 86 s = nMillis / (1000); 87 nMillis -= s * (1000); 88 f = nMillis; 89 } 90 static void debug_i18n_cal_dump( const ::icu::Calendar & r ) 91 { 92 UErrorCode status; 93 long nMillis, h, m, s, f; 94 fprintf( stderr, " %04ld", (long)r.get( UCAL_YEAR, status = U_ZERO_ERROR)); 95 fprintf( stderr, "-%02ld", (long)r.get( UCAL_MONTH, status = U_ZERO_ERROR)+1); 96 fprintf( stderr, "-%02ld", (long)r.get( UCAL_DATE, status = U_ZERO_ERROR)); 97 fprintf( stderr, " %02ld", (long)r.get( UCAL_HOUR_OF_DAY, status = U_ZERO_ERROR)); 98 fprintf( stderr, ":%02ld", (long)r.get( UCAL_MINUTE, status = U_ZERO_ERROR)); 99 fprintf( stderr, ":%02ld", (long)r.get( UCAL_SECOND, status = U_ZERO_ERROR)); 100 fprintf( stderr, " zone: %ld", (long)(nMillis = r.get( UCAL_ZONE_OFFSET, status = U_ZERO_ERROR))); 101 fprintf( stderr, " (%f min)", (double)nMillis / 60000); 102 debug_cal_millis_to_time( nMillis, h, m, s, f); 103 fprintf( stderr, " (%ld:%02ld:%02ld.%ld)", h, m, s, f); 104 fprintf( stderr, " DST: %ld", (long)(nMillis = r.get( UCAL_DST_OFFSET, status = U_ZERO_ERROR))); 105 fprintf( stderr, " (%f min)", (double)nMillis / 60000); 106 debug_cal_millis_to_time( nMillis, h, m, s, f); 107 fprintf( stderr, " (%ld:%02ld:%02ld.%ld)", h, m, s, f); 108 fprintf( stderr, "\n"); 109 fflush(stderr); 110 } 111 // must use double parens, i.e.: DUMP_I18N_CAL_MSG(("four is: %d",4)); 112 #define DUMP_I18N_CAL_MSG(x) {debug_cal_loc(__FILE__,__LINE__);debug_cal_msg x;debug_i18n_cal_dump(*body);} 113 #else // erDUMP_I18N_CALENDAR 114 #define DUMP_I18N_CAL_MSG(x) 115 #endif // erDUMP_I18N_CALENDAR 116 117 #else // erDUMP_ICU_CALENDAR || erDUMP_I18N_CALENDAR 118 #define DUMP_ICU_CAL_MSG(x) 119 #define DUMP_I18N_CAL_MSG(x) 120 #endif // erDUMP_ICU_CALENDAR || erDUMP_I18N_CALENDAR 121 122 123 using namespace ::com::sun::star::uno; 124 using namespace ::com::sun::star::i18n; 125 using namespace ::com::sun::star::lang; 126 127 128 namespace i18npool { 129 130 #define ERROR RuntimeException() 131 132 Calendar_gregorian::Calendar_gregorian() 133 : mxNatNum(new NativeNumberSupplierService) 134 { 135 init(nullptr); 136 } 137 Calendar_gregorian::Calendar_gregorian(const Era *_earArray) 138 : mxNatNum(new NativeNumberSupplierService) 139 { 140 init(_earArray); 141 } 142 void 143 Calendar_gregorian::init(const Era *_eraArray) 144 { 145 cCalendar = "com.sun.star.i18n.Calendar_gregorian"; 146 147 fieldSet = 0; 148 149 // #i102356# With icu::Calendar::createInstance(UErrorCode) in a Thai 150 // th_TH system locale we accidentally used a Buddhist calendar. Though 151 // the ICU documentation says that should be the case only for 152 // th_TH_TRADITIONAL (and ja_JP_TRADITIONAL Gengou), a plain th_TH 153 // already triggers that behavior, ja_JP does not. Strange enough, 154 // passing a th_TH locale to the calendar creation doesn't trigger 155 // this. 156 // See also http://userguide.icu-project.org/datetime/calendar 157 158 // Whatever ICU offers as the default calendar for a locale, ensure we 159 // have a Gregorian calendar as requested. 160 161 /* XXX: with the current implementation the aLocale member variable is 162 * not set prior to loading a calendar from locale data. This 163 * creates an empty (root) locale for ICU, but at least the correct 164 * calendar is used. The language part must not be NULL (respectively 165 * not all, language and country and variant), otherwise the current 166 * default locale would be used again and the calendar keyword ignored. 167 * */ 168 icu::Locale aIcuLocale( "", nullptr, nullptr, "calendar=gregorian"); 169 170 /* XXX: not specifying a timezone when creating a calendar assigns the 171 * system's timezone with all DST quirks, invalid times when switching 172 * to/from DST and so on. The XCalendar* interfaces are defined to support 173 * local time and UTC time so we can not override that here. 174 */ 175 UErrorCode status = U_ZERO_ERROR; 176 body.reset( icu::Calendar::createInstance( aIcuLocale, status) ); 177 if (!body || !U_SUCCESS(status)) throw ERROR; 178 eraArray=_eraArray; 179 } 180 181 Calendar_gregorian::~Calendar_gregorian() 182 { 183 } 184 185 Calendar_hanja::Calendar_hanja() 186 { 187 cCalendar = "com.sun.star.i18n.Calendar_hanja"; 188 } 189 190 OUString SAL_CALL 191 Calendar_hanja::getDisplayName( sal_Int16 displayIndex, sal_Int16 idx, sal_Int16 nameType ) 192 { 193 if ( displayIndex == CalendarDisplayIndex::AM_PM ) { 194 // Am/Pm string for Korean Hanja calendar will refer to Japanese locale 195 css::lang::Locale jaLocale("ja", OUString(), OUString()); 196 if (idx == 0) return LocaleDataImpl::get()->getLocaleItem(jaLocale).timeAM; 197 else if (idx == 1) return LocaleDataImpl::get()->getLocaleItem(jaLocale).timePM; 198 else throw ERROR; 199 } 200 else 201 return Calendar_gregorian::getDisplayName( displayIndex, idx, nameType ); 202 } 203 204 void SAL_CALL 205 Calendar_hanja::loadCalendar( const OUString& /*uniqueID*/, const css::lang::Locale& rLocale ) 206 { 207 // Since this class could be called by service name 'hanja_yoil', we have to 208 // rename uniqueID to get right calendar defined in locale data. 209 Calendar_gregorian::loadCalendar("hanja", rLocale); 210 } 211 212 static const Era gengou_eraArray[] = { 213 {1868, 1, 1, 0}, // Meiji 214 {1912, 7, 30, 0}, // Taisho 215 {1926, 12, 25, 0}, // Showa 216 {1989, 1, 8, 0}, // Heisei 217 {2019, 5, 1, 0}, // Reiwa 218 {0, 0, 0, 0} 219 }; 220 Calendar_gengou::Calendar_gengou() : Calendar_gregorian(gengou_eraArray) 221 { 222 cCalendar = "com.sun.star.i18n.Calendar_gengou"; 223 } 224 225 static const Era ROC_eraArray[] = { 226 {1912, 1, 1, kDisplayEraForcedLongYear}, // #i116701# 227 {0, 0, 0, 0} 228 }; 229 Calendar_ROC::Calendar_ROC() : Calendar_gregorian(ROC_eraArray) 230 { 231 cCalendar = "com.sun.star.i18n.Calendar_ROC"; 232 } 233 234 /** 235 * The start year of the Korean traditional calendar (Dan-gi) is the inaugural 236 * year of Dan-gun (BC 2333). 237 */ 238 static const Era dangi_eraArray[] = { 239 {-2332, 1, 1, 0}, 240 {0, 0, 0, 0} 241 }; 242 Calendar_dangi::Calendar_dangi() : Calendar_gregorian(dangi_eraArray) 243 { 244 cCalendar = "com.sun.star.i18n.Calendar_dangi"; 245 } 246 247 static const Era buddhist_eraArray[] = { 248 {-542, 1, 1, 0}, 249 {0, 0, 0, 0} 250 }; 251 Calendar_buddhist::Calendar_buddhist() : Calendar_gregorian(buddhist_eraArray) 252 { 253 cCalendar = "com.sun.star.i18n.Calendar_buddhist"; 254 } 255 256 void SAL_CALL 257 Calendar_gregorian::loadCalendar( const OUString& uniqueID, const css::lang::Locale& rLocale ) 258 { 259 // init. fieldValue[] 260 getValue(); 261 262 aLocale = rLocale; 263 const Sequence< Calendar2 > xC = LocaleDataImpl::get()->getAllCalendars2(rLocale); 264 for (const auto& rCal : xC) 265 { 266 if (uniqueID == rCal.Name) 267 { 268 aCalendar = rCal; 269 // setup minimalDaysInFirstWeek 270 setMinimumNumberOfDaysForFirstWeek( 271 aCalendar.MinimumNumberOfDaysForFirstWeek); 272 // setup first day of week 273 for (sal_Int16 day = sal::static_int_cast<sal_Int16>( 274 aCalendar.Days.getLength()-1); day>=0; day--) 275 { 276 if (aCalendar.StartOfWeek == aCalendar.Days[day].ID) 277 { 278 setFirstDayOfWeek( day); 279 return; 280 } 281 } 282 } 283 } 284 // Calendar is not for the locale 285 throw ERROR; 286 } 287 288 289 css::i18n::Calendar2 SAL_CALL 290 Calendar_gregorian::getLoadedCalendar2() 291 { 292 return aCalendar; 293 } 294 295 css::i18n::Calendar SAL_CALL 296 Calendar_gregorian::getLoadedCalendar() 297 { 298 return LocaleDataImpl::downcastCalendar( aCalendar); 299 } 300 301 OUString SAL_CALL 302 Calendar_gregorian::getUniqueID() 303 { 304 return aCalendar.Name; 305 } 306 307 void SAL_CALL 308 Calendar_gregorian::setDateTime( double fTimeInDays ) 309 { 310 // ICU handles dates in milliseconds as double values and uses floor() 311 // to obtain integer values, which may yield a date decremented by one 312 // for odd (historical) timezone values where the computed value due to 313 // rounding errors has a fractional part in milliseconds. Ensure we 314 // pass a value without fraction here. If not, that may lead to 315 // fdo#44286 or fdo#52619 and the like, e.g. when passing 316 // -2136315212000.000244 instead of -2136315212000.000000 317 double fM = fTimeInDays * U_MILLIS_PER_DAY; 318 double fR = rtl::math::round( fM ); 319 SAL_INFO_IF( fM != fR, "i18npool", 320 "Calendar_gregorian::setDateTime: " << std::fixed << fM << " rounded to " << fR); 321 UErrorCode status = U_ZERO_ERROR; 322 body->setTime( fR, status); 323 if ( !U_SUCCESS(status) ) throw ERROR; 324 getValue(); 325 } 326 327 double SAL_CALL 328 Calendar_gregorian::getDateTime() 329 { 330 if (fieldSet) { 331 setValue(); 332 getValue(); 333 } 334 UErrorCode status = U_ZERO_ERROR; 335 double fR = body->getTime(status); 336 if ( !U_SUCCESS(status) ) throw ERROR; 337 return fR / U_MILLIS_PER_DAY; 338 } 339 340 void SAL_CALL 341 Calendar_gregorian::setLocalDateTime( double fTimeInDays ) 342 { 343 // See setDateTime() for why the rounding. 344 double fM = fTimeInDays * U_MILLIS_PER_DAY; 345 double fR = rtl::math::round( fM ); 346 SAL_INFO_IF( fM != fR, "i18npool", 347 "Calendar_gregorian::setLocalDateTime: " << std::fixed << fM << " rounded to " << fR); 348 int32_t nZoneOffset, nDSTOffset; 349 UErrorCode status = U_ZERO_ERROR; 350 body->getTimeZone().getOffset( fR, TRUE, nZoneOffset, nDSTOffset, status ); 351 if ( !U_SUCCESS(status) ) throw ERROR; 352 status = U_ZERO_ERROR; 353 body->setTime( fR - (nZoneOffset + nDSTOffset), status ); 354 if ( !U_SUCCESS(status) ) throw ERROR; 355 getValue(); 356 } 357 358 double SAL_CALL 359 Calendar_gregorian::getLocalDateTime() 360 { 361 if (fieldSet) { 362 setValue(); 363 getValue(); 364 } 365 UErrorCode status = U_ZERO_ERROR; 366 double fTime = body->getTime( status ); 367 if ( !U_SUCCESS(status) ) throw ERROR; 368 status = U_ZERO_ERROR; 369 int32_t nZoneOffset = body->get( UCAL_ZONE_OFFSET, status ); 370 if ( !U_SUCCESS(status) ) throw ERROR; 371 status = U_ZERO_ERROR; 372 int32_t nDSTOffset = body->get( UCAL_DST_OFFSET, status ); 373 if ( !U_SUCCESS(status) ) throw ERROR; 374 return (fTime + (nZoneOffset + nDSTOffset)) / U_MILLIS_PER_DAY; 375 } 376 377 bool Calendar_gregorian::setTimeZone( const OUString& rTimeZone ) 378 { 379 if (fieldSet) 380 { 381 setValue(); 382 getValue(); 383 } 384 const icu::UnicodeString aID( reinterpret_cast<const UChar*>(rTimeZone.getStr()), rTimeZone.getLength()); 385 const std::unique_ptr<const icu::TimeZone> pTZ( icu::TimeZone::createTimeZone(aID)); 386 if (!pTZ) 387 return false; 388 389 body->setTimeZone(*pTZ); 390 return true; 391 } 392 393 // map field value from gregorian calendar to other calendar, it can be overwritten by derived class. 394 // By using eraArray, it can take care Japanese and Taiwan ROC calendar. 395 void Calendar_gregorian::mapFromGregorian() 396 { 397 if (!eraArray) 398 return; 399 400 sal_Int16 e, y, m, d; 401 402 e = fieldValue[CalendarFieldIndex::ERA]; 403 y = fieldValue[CalendarFieldIndex::YEAR]; 404 m = fieldValue[CalendarFieldIndex::MONTH] + 1; 405 d = fieldValue[CalendarFieldIndex::DAY_OF_MONTH]; 406 407 // since the year is reversed for first era, it is reversed again here for Era compare. 408 if (e == 0) 409 y = 1 - y; 410 411 for (e = 0; eraArray[e].year; e++) 412 if ((y != eraArray[e].year) ? y < eraArray[e].year : 413 (m != eraArray[e].month) ? m < eraArray[e].month : d < eraArray[e].day) 414 break; 415 416 fieldValue[CalendarFieldIndex::ERA] = e; 417 fieldValue[CalendarFieldIndex::YEAR] = 418 sal::static_int_cast<sal_Int16>( (e == 0) ? (eraArray[0].year - y) : (y - eraArray[e-1].year + 1) ); 419 } 420 421 #define FIELDS ((1 << CalendarFieldIndex::ERA) | (1 << CalendarFieldIndex::YEAR)) 422 // map field value from other calendar to gregorian calendar, it can be overwritten by derived class. 423 // By using eraArray, it can take care Japanese and Taiwan ROC calendar. 424 void Calendar_gregorian::mapToGregorian() 425 { 426 if (eraArray && (fieldSet & FIELDS)) { 427 sal_Int16 y, e = fieldValue[CalendarFieldIndex::ERA]; 428 if (e == 0) 429 y = sal::static_int_cast<sal_Int16>( eraArray[0].year - fieldValue[CalendarFieldIndex::YEAR] ); 430 else 431 y = sal::static_int_cast<sal_Int16>( eraArray[e-1].year + fieldValue[CalendarFieldIndex::YEAR] - 1 ); 432 433 fieldSetValue[CalendarFieldIndex::ERA] = y <= 0 ? 0 : 1; 434 fieldSetValue[CalendarFieldIndex::YEAR] = (y <= 0 ? 1 - y : y); 435 fieldSet |= FIELDS; 436 } 437 } 438 439 /// @throws RuntimeException 440 static UCalendarDateFields fieldNameConverter(sal_Int16 fieldIndex) 441 { 442 UCalendarDateFields f; 443 444 switch (fieldIndex) { 445 case CalendarFieldIndex::AM_PM: f = UCAL_AM_PM; break; 446 case CalendarFieldIndex::DAY_OF_MONTH: f = UCAL_DATE; break; 447 case CalendarFieldIndex::DAY_OF_WEEK: f = UCAL_DAY_OF_WEEK; break; 448 case CalendarFieldIndex::DAY_OF_YEAR: f = UCAL_DAY_OF_YEAR; break; 449 case CalendarFieldIndex::DST_OFFSET: f = UCAL_DST_OFFSET; break; 450 case CalendarFieldIndex::ZONE_OFFSET: f = UCAL_ZONE_OFFSET; break; 451 case CalendarFieldIndex::HOUR: f = UCAL_HOUR_OF_DAY; break; 452 case CalendarFieldIndex::MINUTE: f = UCAL_MINUTE; break; 453 case CalendarFieldIndex::SECOND: f = UCAL_SECOND; break; 454 case CalendarFieldIndex::MILLISECOND: f = UCAL_MILLISECOND; break; 455 case CalendarFieldIndex::WEEK_OF_MONTH: f = UCAL_WEEK_OF_MONTH; break; 456 case CalendarFieldIndex::WEEK_OF_YEAR: f = UCAL_WEEK_OF_YEAR; break; 457 case CalendarFieldIndex::YEAR: f = UCAL_YEAR; break; 458 case CalendarFieldIndex::MONTH: f = UCAL_MONTH; break; 459 case CalendarFieldIndex::ERA: f = UCAL_ERA; break; 460 default: throw ERROR; 461 } 462 return f; 463 } 464 465 void SAL_CALL 466 Calendar_gregorian::setValue( sal_Int16 fieldIndex, sal_Int16 value ) 467 { 468 if (fieldIndex < 0 || FIELD_INDEX_COUNT <= fieldIndex) 469 throw ERROR; 470 fieldSet |= (1 << fieldIndex); 471 fieldValue[fieldIndex] = value; 472 } 473 474 bool Calendar_gregorian::getCombinedOffset( sal_Int32 & o_nOffset, 475 sal_Int16 nParentFieldIndex, sal_Int16 nChildFieldIndex ) const 476 { 477 o_nOffset = 0; 478 bool bFieldsSet = false; 479 if (fieldSet & (1 << nParentFieldIndex)) 480 { 481 bFieldsSet = true; 482 o_nOffset = static_cast<sal_Int32>( fieldValue[nParentFieldIndex]) * 60000; 483 } 484 if (fieldSet & (1 << nChildFieldIndex)) 485 { 486 bFieldsSet = true; 487 if (o_nOffset < 0) 488 o_nOffset -= static_cast<sal_uInt16>( fieldValue[nChildFieldIndex]); 489 else 490 o_nOffset += static_cast<sal_uInt16>( fieldValue[nChildFieldIndex]); 491 } 492 return bFieldsSet; 493 } 494 495 bool Calendar_gregorian::getZoneOffset( sal_Int32 & o_nOffset ) const 496 { 497 return getCombinedOffset( o_nOffset, CalendarFieldIndex::ZONE_OFFSET, 498 CalendarFieldIndex::ZONE_OFFSET_SECOND_MILLIS); 499 } 500 501 bool Calendar_gregorian::getDSTOffset( sal_Int32 & o_nOffset ) const 502 { 503 return getCombinedOffset( o_nOffset, CalendarFieldIndex::DST_OFFSET, 504 CalendarFieldIndex::DST_OFFSET_SECOND_MILLIS); 505 } 506 507 void Calendar_gregorian::submitFields() 508 { 509 for (sal_Int16 fieldIndex = 0; fieldIndex < FIELD_INDEX_COUNT; fieldIndex++) 510 { 511 if (fieldSet & (1 << fieldIndex)) 512 { 513 switch (fieldIndex) 514 { 515 default: 516 body->set(fieldNameConverter(fieldIndex), fieldSetValue[fieldIndex]); 517 break; 518 case CalendarFieldIndex::ZONE_OFFSET: 519 case CalendarFieldIndex::DST_OFFSET: 520 case CalendarFieldIndex::ZONE_OFFSET_SECOND_MILLIS: 521 case CalendarFieldIndex::DST_OFFSET_SECOND_MILLIS: 522 break; // nothing, extra handling 523 } 524 } 525 } 526 sal_Int32 nZoneOffset, nDSTOffset; 527 if (getZoneOffset( nZoneOffset)) 528 body->set( fieldNameConverter( CalendarFieldIndex::ZONE_OFFSET), nZoneOffset); 529 if (getDSTOffset( nDSTOffset)) 530 body->set( fieldNameConverter( CalendarFieldIndex::DST_OFFSET), nDSTOffset); 531 } 532 533 void Calendar_gregorian::setValue() 534 { 535 // Copy fields before calling submitFields() directly or indirectly below. 536 memcpy(fieldSetValue, fieldValue, sizeof(fieldSetValue)); 537 // Possibly setup ERA and YEAR in fieldSetValue. 538 mapToGregorian(); 539 540 DUMP_ICU_CAL_MSG(("%s\n","setValue() before submission")); 541 DUMP_I18N_CAL_MSG(("%s\n","setValue() before submission")); 542 543 submitFields(); 544 545 DUMP_ICU_CAL_MSG(("%s\n","setValue() after submission")); 546 DUMP_I18N_CAL_MSG(("%s\n","setValue() after submission")); 547 548 #if erDUMP_ICU_CALENDAR || erDUMP_I18N_CALENDAR 549 { 550 // force icu::Calendar to recalculate 551 UErrorCode status; 552 sal_Int32 nTmp = body->get( UCAL_DATE, status = U_ZERO_ERROR); 553 DUMP_ICU_CAL_MSG(("%s: %d\n","setValue() result day",nTmp)); 554 DUMP_I18N_CAL_MSG(("%s: %d\n","setValue() result day",nTmp)); 555 } 556 #endif 557 } 558 559 void Calendar_gregorian::getValue() 560 { 561 DUMP_ICU_CAL_MSG(("%s\n","getValue()")); 562 DUMP_I18N_CAL_MSG(("%s\n","getValue()")); 563 for (sal_Int16 fieldIndex = 0; fieldIndex < FIELD_INDEX_COUNT; fieldIndex++) 564 { 565 if (fieldIndex == CalendarFieldIndex::ZONE_OFFSET_SECOND_MILLIS || 566 fieldIndex == CalendarFieldIndex::DST_OFFSET_SECOND_MILLIS) 567 continue; // not ICU fields 568 569 UErrorCode status = U_ZERO_ERROR; 570 sal_Int32 value = body->get( fieldNameConverter( 571 fieldIndex), status); 572 if ( !U_SUCCESS(status) ) throw ERROR; 573 574 // Convert millisecond to minute for ZONE and DST and set remainder in 575 // second field. 576 if (fieldIndex == CalendarFieldIndex::ZONE_OFFSET) 577 { 578 sal_Int32 nMinutes = value / 60000; 579 sal_Int16 nMillis = static_cast<sal_Int16>( static_cast<sal_uInt16>( 580 abs( value - nMinutes * 60000))); 581 fieldValue[CalendarFieldIndex::ZONE_OFFSET] = static_cast<sal_Int16>( nMinutes); 582 fieldValue[CalendarFieldIndex::ZONE_OFFSET_SECOND_MILLIS] = nMillis; 583 } 584 else if (fieldIndex == CalendarFieldIndex::DST_OFFSET) 585 { 586 sal_Int32 nMinutes = value / 60000; 587 sal_Int16 nMillis = static_cast<sal_Int16>( static_cast<sal_uInt16>( 588 abs( value - nMinutes * 60000))); 589 fieldValue[CalendarFieldIndex::DST_OFFSET] = static_cast<sal_Int16>( nMinutes); 590 fieldValue[CalendarFieldIndex::DST_OFFSET_SECOND_MILLIS] = nMillis; 591 } 592 else 593 fieldValue[fieldIndex] = static_cast<sal_Int16>(value); 594 595 // offset 1 since the value for week start day SunDay is different between Calendar and Weekdays. 596 if ( fieldIndex == CalendarFieldIndex::DAY_OF_WEEK ) 597 fieldValue[fieldIndex]--; // UCAL_SUNDAY:/* == 1 */ ==> Weekdays::SUNDAY /* ==0 */ 598 } 599 mapFromGregorian(); 600 fieldSet = 0; 601 } 602 603 sal_Int16 SAL_CALL 604 Calendar_gregorian::getValue( sal_Int16 fieldIndex ) 605 { 606 if (fieldIndex < 0 || FIELD_INDEX_COUNT <= fieldIndex) 607 throw ERROR; 608 609 if (fieldSet) { 610 setValue(); 611 getValue(); 612 } 613 614 return fieldValue[fieldIndex]; 615 } 616 617 void SAL_CALL 618 Calendar_gregorian::addValue( sal_Int16 fieldIndex, sal_Int32 value ) 619 { 620 // since ZONE and DST could not be add, we don't need to convert value here 621 UErrorCode status = U_ZERO_ERROR; 622 body->add(fieldNameConverter(fieldIndex), value, status); 623 if ( !U_SUCCESS(status) ) throw ERROR; 624 getValue(); 625 } 626 627 sal_Bool SAL_CALL 628 Calendar_gregorian::isValid() 629 { 630 if (fieldSet) { 631 sal_Int32 tmp = fieldSet; 632 setValue(); 633 memcpy(fieldSetValue, fieldValue, sizeof(fieldSetValue)); 634 getValue(); 635 for ( sal_Int16 fieldIndex = 0; fieldIndex < FIELD_INDEX_COUNT; fieldIndex++ ) { 636 // compare only with fields that are set and reset fieldSet[] 637 if (tmp & (1 << fieldIndex)) { 638 if (fieldSetValue[fieldIndex] != fieldValue[fieldIndex]) 639 return false; 640 } 641 } 642 } 643 return true; 644 } 645 646 // NativeNumberMode has different meaning between Number and Calendar for Asian locales. 647 // Here is the mapping table 648 // calendar(q/y/m/d) zh_CN zh_TW ja ko 649 // NatNum1 NatNum1/1/7/7 NatNum1/1/7/7 NatNum1/1/4/4 NatNum1/1/7/7 650 // NatNum2 NatNum2/2/8/8 NatNum2/2/8/8 NatNum2/2/5/5 NatNum2/2/8/8 651 // NatNum3 NatNum3/3/3/3 NatNum3/3/3/3 NatNum3/3/3/3 NatNum3/3/3/3 652 // NatNum4 NatNum9/9/11/11 653 654 static sal_Int16 NatNumForCalendar(const css::lang::Locale& aLocale, 655 sal_Int32 nCalendarDisplayCode, sal_Int16 nNativeNumberMode, sal_Int16 value ) 656 { 657 bool isShort = ((nCalendarDisplayCode == CalendarDisplayCode::SHORT_YEAR || 658 nCalendarDisplayCode == CalendarDisplayCode::LONG_YEAR) && value >= 100) || 659 nCalendarDisplayCode == CalendarDisplayCode::SHORT_QUARTER || 660 nCalendarDisplayCode == CalendarDisplayCode::LONG_QUARTER; 661 bool isChinese = aLocale.Language == "zh"; 662 bool isJapanese = aLocale.Language == "ja"; 663 bool isKorean = aLocale.Language == "ko"; 664 665 if (isChinese || isJapanese || isKorean) { 666 switch (nNativeNumberMode) { 667 case NativeNumberMode::NATNUM1: 668 if (!isShort) 669 nNativeNumberMode = isJapanese ? NativeNumberMode::NATNUM4 : NativeNumberMode::NATNUM7; 670 break; 671 case NativeNumberMode::NATNUM2: 672 if (!isShort) 673 nNativeNumberMode = isJapanese ? NativeNumberMode::NATNUM5 : NativeNumberMode::NATNUM8; 674 break; 675 case NativeNumberMode::NATNUM3: 676 break; 677 case NativeNumberMode::NATNUM4: 678 if (isKorean) 679 return isShort ? NativeNumberMode::NATNUM9 : NativeNumberMode::NATNUM11; 680 [[fallthrough]]; 681 default: return 0; 682 } 683 } 684 return nNativeNumberMode; 685 } 686 687 static sal_Int32 DisplayCode2FieldIndex(sal_Int32 nCalendarDisplayCode) 688 { 689 switch( nCalendarDisplayCode ) { 690 case CalendarDisplayCode::SHORT_DAY: 691 case CalendarDisplayCode::LONG_DAY: 692 return CalendarFieldIndex::DAY_OF_MONTH; 693 case CalendarDisplayCode::SHORT_DAY_NAME: 694 case CalendarDisplayCode::LONG_DAY_NAME: 695 case CalendarDisplayCode::NARROW_DAY_NAME: 696 return CalendarFieldIndex::DAY_OF_WEEK; 697 case CalendarDisplayCode::SHORT_QUARTER: 698 case CalendarDisplayCode::LONG_QUARTER: 699 case CalendarDisplayCode::SHORT_MONTH: 700 case CalendarDisplayCode::LONG_MONTH: 701 case CalendarDisplayCode::SHORT_MONTH_NAME: 702 case CalendarDisplayCode::LONG_MONTH_NAME: 703 case CalendarDisplayCode::NARROW_MONTH_NAME: 704 case CalendarDisplayCode::SHORT_GENITIVE_MONTH_NAME: 705 case CalendarDisplayCode::LONG_GENITIVE_MONTH_NAME: 706 case CalendarDisplayCode::NARROW_GENITIVE_MONTH_NAME: 707 case CalendarDisplayCode::SHORT_PARTITIVE_MONTH_NAME: 708 case CalendarDisplayCode::LONG_PARTITIVE_MONTH_NAME: 709 case CalendarDisplayCode::NARROW_PARTITIVE_MONTH_NAME: 710 return CalendarFieldIndex::MONTH; 711 case CalendarDisplayCode::SHORT_YEAR: 712 case CalendarDisplayCode::LONG_YEAR: 713 return CalendarFieldIndex::YEAR; 714 case CalendarDisplayCode::SHORT_ERA: 715 case CalendarDisplayCode::LONG_ERA: 716 return CalendarFieldIndex::ERA; 717 case CalendarDisplayCode::SHORT_YEAR_AND_ERA: 718 case CalendarDisplayCode::LONG_YEAR_AND_ERA: 719 return CalendarFieldIndex::YEAR; 720 default: 721 return 0; 722 } 723 } 724 725 sal_Int16 SAL_CALL 726 Calendar_gregorian::getFirstDayOfWeek() 727 { 728 // UCAL_SUNDAY == 1, Weekdays::SUNDAY == 0 => offset -1 729 // Check for underflow just in case we're called "out of sync". 730 return ::std::max( sal::static_int_cast<sal_Int16>(0), 731 sal::static_int_cast<sal_Int16>( static_cast<sal_Int16>( 732 body->getFirstDayOfWeek()) - 1)); 733 } 734 735 void SAL_CALL 736 Calendar_gregorian::setFirstDayOfWeek( sal_Int16 day ) 737 { 738 // Weekdays::SUNDAY == 0, UCAL_SUNDAY == 1 => offset +1 739 body->setFirstDayOfWeek( static_cast<UCalendarDaysOfWeek>( day + 1)); 740 } 741 742 void SAL_CALL 743 Calendar_gregorian::setMinimumNumberOfDaysForFirstWeek( sal_Int16 days ) 744 { 745 aCalendar.MinimumNumberOfDaysForFirstWeek = days; 746 body->setMinimalDaysInFirstWeek( static_cast<uint8_t>( days)); 747 } 748 749 sal_Int16 SAL_CALL 750 Calendar_gregorian::getMinimumNumberOfDaysForFirstWeek() 751 { 752 return aCalendar.MinimumNumberOfDaysForFirstWeek; 753 } 754 755 sal_Int16 SAL_CALL 756 Calendar_gregorian::getNumberOfMonthsInYear() 757 { 758 return static_cast<sal_Int16>(aCalendar.Months.getLength()); 759 } 760 761 762 sal_Int16 SAL_CALL 763 Calendar_gregorian::getNumberOfDaysInWeek() 764 { 765 return static_cast<sal_Int16>(aCalendar.Days.getLength()); 766 } 767 768 769 Sequence< CalendarItem > SAL_CALL 770 Calendar_gregorian::getDays() 771 { 772 return LocaleDataImpl::downcastCalendarItems( aCalendar.Days); 773 } 774 775 776 Sequence< CalendarItem > SAL_CALL 777 Calendar_gregorian::getMonths() 778 { 779 return LocaleDataImpl::downcastCalendarItems( aCalendar.Months); 780 } 781 782 783 Sequence< CalendarItem2 > SAL_CALL 784 Calendar_gregorian::getDays2() 785 { 786 return aCalendar.Days; 787 } 788 789 790 Sequence< CalendarItem2 > SAL_CALL 791 Calendar_gregorian::getMonths2() 792 { 793 return aCalendar.Months; 794 } 795 796 797 Sequence< CalendarItem2 > SAL_CALL 798 Calendar_gregorian::getGenitiveMonths2() 799 { 800 return aCalendar.GenitiveMonths; 801 } 802 803 804 Sequence< CalendarItem2 > SAL_CALL 805 Calendar_gregorian::getPartitiveMonths2() 806 { 807 return aCalendar.PartitiveMonths; 808 } 809 810 811 OUString SAL_CALL 812 Calendar_gregorian::getDisplayName( sal_Int16 displayIndex, sal_Int16 idx, sal_Int16 nameType ) 813 { 814 OUString aStr; 815 816 switch( displayIndex ) { 817 case CalendarDisplayIndex::AM_PM:/* ==0 */ 818 if (idx == 0) aStr = LocaleDataImpl::get()->getLocaleItem(aLocale).timeAM; 819 else if (idx == 1) aStr = LocaleDataImpl::get()->getLocaleItem(aLocale).timePM; 820 else throw ERROR; 821 break; 822 case CalendarDisplayIndex::DAY: 823 if( idx >= aCalendar.Days.getLength() ) throw ERROR; 824 if (nameType == 0) aStr = aCalendar.Days[idx].AbbrevName; 825 else if (nameType == 1) aStr = aCalendar.Days[idx].FullName; 826 else if (nameType == 2) aStr = aCalendar.Days[idx].NarrowName; 827 else throw ERROR; 828 break; 829 case CalendarDisplayIndex::MONTH: 830 if( idx >= aCalendar.Months.getLength() ) throw ERROR; 831 if (nameType == 0) aStr = aCalendar.Months[idx].AbbrevName; 832 else if (nameType == 1) aStr = aCalendar.Months[idx].FullName; 833 else if (nameType == 2) aStr = aCalendar.Months[idx].NarrowName; 834 else throw ERROR; 835 break; 836 case CalendarDisplayIndex::GENITIVE_MONTH: 837 if( idx >= aCalendar.GenitiveMonths.getLength() ) throw ERROR; 838 if (nameType == 0) aStr = aCalendar.GenitiveMonths[idx].AbbrevName; 839 else if (nameType == 1) aStr = aCalendar.GenitiveMonths[idx].FullName; 840 else if (nameType == 2) aStr = aCalendar.GenitiveMonths[idx].NarrowName; 841 else throw ERROR; 842 break; 843 case CalendarDisplayIndex::PARTITIVE_MONTH: 844 if( idx >= aCalendar.PartitiveMonths.getLength() ) throw ERROR; 845 if (nameType == 0) aStr = aCalendar.PartitiveMonths[idx].AbbrevName; 846 else if (nameType == 1) aStr = aCalendar.PartitiveMonths[idx].FullName; 847 else if (nameType == 2) aStr = aCalendar.PartitiveMonths[idx].NarrowName; 848 else throw ERROR; 849 break; 850 case CalendarDisplayIndex::ERA: 851 if( idx >= aCalendar.Eras.getLength() ) throw ERROR; 852 if (nameType == 0) aStr = aCalendar.Eras[idx].AbbrevName; 853 else if (nameType == 1) aStr = aCalendar.Eras[idx].FullName; 854 else throw ERROR; 855 break; 856 case CalendarDisplayIndex::YEAR: 857 break; 858 default: 859 throw ERROR; 860 } 861 return aStr; 862 } 863 864 // Methods in XExtendedCalendar 865 OUString SAL_CALL 866 Calendar_gregorian::getDisplayString( sal_Int32 nCalendarDisplayCode, sal_Int16 nNativeNumberMode ) 867 { 868 return getDisplayStringImpl( nCalendarDisplayCode, nNativeNumberMode, false); 869 } 870 871 OUString 872 Calendar_gregorian::getDisplayStringImpl( sal_Int32 nCalendarDisplayCode, sal_Int16 nNativeNumberMode, bool bEraMode ) 873 { 874 sal_Int16 value = getValue(sal::static_int_cast<sal_Int16>( DisplayCode2FieldIndex(nCalendarDisplayCode) )); 875 OUString aOUStr; 876 877 if (nCalendarDisplayCode == CalendarDisplayCode::SHORT_QUARTER || 878 nCalendarDisplayCode == CalendarDisplayCode::LONG_QUARTER) { 879 Sequence< OUString> xR = LocaleDataImpl::get()->getReservedWord(aLocale); 880 sal_Int16 quarter = value / 3; 881 // Since this base class method may be called by derived calendar 882 // classes where a year consists of more than 12 months we need a check 883 // to not run out of bounds of reserved quarter words. Perhaps a more 884 // clean way (instead of dividing by 3) would be to first get the 885 // number of months, divide by 4 and then use that result to divide the 886 // actual month value. 887 if ( quarter > 3 ) 888 quarter = 3; 889 quarter = sal::static_int_cast<sal_Int16>( quarter + 890 ((nCalendarDisplayCode == CalendarDisplayCode::SHORT_QUARTER) ? 891 reservedWords::QUARTER1_ABBREVIATION : reservedWords::QUARTER1_WORD) ); 892 aOUStr = xR[quarter]; 893 } else { 894 // The "#100211# - checked" comments serve for detection of "use of 895 // sprintf is safe here" conditions. An sprintf encountered without 896 // having that comment triggers alarm ;-) 897 char aStr[10]; 898 switch( nCalendarDisplayCode ) { 899 case CalendarDisplayCode::SHORT_MONTH: 900 value += 1; // month is zero based 901 [[fallthrough]]; 902 case CalendarDisplayCode::SHORT_DAY: 903 sprintf(aStr, "%d", value); // #100211# - checked 904 break; 905 case CalendarDisplayCode::LONG_YEAR: 906 if ( aCalendar.Name == "gengou" ) 907 sprintf(aStr, "%02d", value); // #100211# - checked 908 else 909 sprintf(aStr, "%d", value); // #100211# - checked 910 break; 911 case CalendarDisplayCode::LONG_MONTH: 912 value += 1; // month is zero based 913 sprintf(aStr, "%02d", value); // #100211# - checked 914 break; 915 case CalendarDisplayCode::SHORT_YEAR: 916 // Take last 2 digits, or only one if value<10, for example, 917 // in case of the Gengou calendar. For combined era+year always 918 // the full year is displayed, without leading 0. 919 // Workaround for non-combined calls in certain calendars is 920 // the kDisplayEraForcedLongYear flag, but this also could get 921 // called for YY not only E format codes, no differentiation 922 // possible here; the good news is that usually the Gregorian 923 // calendar is the default and hence YY calls for Gregorian and 924 // E for the other calendar and currently (2013-02-28) ROC is 925 // the only calendar using this. 926 // See i#116701 and fdo#60915 927 if (value < 100 || bEraMode || (eraArray && (eraArray[0].flags & kDisplayEraForcedLongYear))) 928 sprintf(aStr, "%d", value); // #100211# - checked 929 else 930 sprintf(aStr, "%02d", value % 100); // #100211# - checked 931 break; 932 case CalendarDisplayCode::LONG_DAY: 933 sprintf(aStr, "%02d", value); // #100211# - checked 934 break; 935 936 case CalendarDisplayCode::SHORT_DAY_NAME: 937 return getDisplayName(CalendarDisplayIndex::DAY, value, 0); 938 case CalendarDisplayCode::LONG_DAY_NAME: 939 return getDisplayName(CalendarDisplayIndex::DAY, value, 1); 940 case CalendarDisplayCode::NARROW_DAY_NAME: 941 return getDisplayName(CalendarDisplayIndex::DAY, value, 2); 942 case CalendarDisplayCode::SHORT_MONTH_NAME: 943 return getDisplayName(CalendarDisplayIndex::MONTH, value, 0); 944 case CalendarDisplayCode::LONG_MONTH_NAME: 945 return getDisplayName(CalendarDisplayIndex::MONTH, value, 1); 946 case CalendarDisplayCode::NARROW_MONTH_NAME: 947 return getDisplayName(CalendarDisplayIndex::MONTH, value, 2); 948 case CalendarDisplayCode::SHORT_GENITIVE_MONTH_NAME: 949 return getDisplayName(CalendarDisplayIndex::GENITIVE_MONTH, value, 0); 950 case CalendarDisplayCode::LONG_GENITIVE_MONTH_NAME: 951 return getDisplayName(CalendarDisplayIndex::GENITIVE_MONTH, value, 1); 952 case CalendarDisplayCode::NARROW_GENITIVE_MONTH_NAME: 953 return getDisplayName(CalendarDisplayIndex::GENITIVE_MONTH, value, 2); 954 case CalendarDisplayCode::SHORT_PARTITIVE_MONTH_NAME: 955 return getDisplayName(CalendarDisplayIndex::PARTITIVE_MONTH, value, 0); 956 case CalendarDisplayCode::LONG_PARTITIVE_MONTH_NAME: 957 return getDisplayName(CalendarDisplayIndex::PARTITIVE_MONTH, value, 1); 958 case CalendarDisplayCode::NARROW_PARTITIVE_MONTH_NAME: 959 return getDisplayName(CalendarDisplayIndex::PARTITIVE_MONTH, value, 2); 960 case CalendarDisplayCode::SHORT_ERA: 961 return getDisplayName(CalendarDisplayIndex::ERA, value, 0); 962 case CalendarDisplayCode::LONG_ERA: 963 return getDisplayName(CalendarDisplayIndex::ERA, value, 1); 964 965 case CalendarDisplayCode::SHORT_YEAR_AND_ERA: 966 return getDisplayStringImpl( CalendarDisplayCode::SHORT_ERA, nNativeNumberMode, true ) + 967 getDisplayStringImpl( CalendarDisplayCode::SHORT_YEAR, nNativeNumberMode, true ); 968 969 case CalendarDisplayCode::LONG_YEAR_AND_ERA: 970 return getDisplayStringImpl( CalendarDisplayCode::LONG_ERA, nNativeNumberMode, true ) + 971 getDisplayStringImpl( CalendarDisplayCode::LONG_YEAR, nNativeNumberMode, true ); 972 973 default: 974 throw ERROR; 975 } 976 aOUStr = OUString::createFromAscii(aStr); 977 } 978 // NatNum12 used only for selected parts 979 if (nNativeNumberMode > 0 && nNativeNumberMode != 12) { 980 // For Japanese calendar, first year calls GAN, see bug 111668 for detail. 981 if (eraArray == gengou_eraArray && value == 1 982 && (nCalendarDisplayCode == CalendarDisplayCode::SHORT_YEAR || 983 nCalendarDisplayCode == CalendarDisplayCode::LONG_YEAR) 984 && (nNativeNumberMode == NativeNumberMode::NATNUM1 || 985 nNativeNumberMode == NativeNumberMode::NATNUM2)) { 986 static sal_Unicode gan = 0x5143; 987 return OUString(&gan, 1); 988 } 989 sal_Int16 nNatNum = NatNumForCalendar(aLocale, nCalendarDisplayCode, nNativeNumberMode, value); 990 if (nNatNum > 0) 991 return mxNatNum->getNativeNumberString(aOUStr, aLocale, nNatNum); 992 } 993 return aOUStr; 994 } 995 996 // Methods in XExtendedCalendar 997 OUString SAL_CALL 998 Calendar_buddhist::getDisplayString( sal_Int32 nCalendarDisplayCode, sal_Int16 nNativeNumberMode ) 999 { 1000 // make year and era in different order for year before and after 0. 1001 if ((nCalendarDisplayCode == CalendarDisplayCode::LONG_YEAR_AND_ERA || 1002 nCalendarDisplayCode == CalendarDisplayCode::SHORT_YEAR_AND_ERA) && 1003 getValue(CalendarFieldIndex::ERA) == 0) { 1004 if (nCalendarDisplayCode == CalendarDisplayCode::LONG_YEAR_AND_ERA) 1005 return getDisplayStringImpl( CalendarDisplayCode::SHORT_YEAR, nNativeNumberMode, true ) + 1006 getDisplayStringImpl( CalendarDisplayCode::SHORT_ERA, nNativeNumberMode, true ); 1007 else 1008 return getDisplayStringImpl( CalendarDisplayCode::LONG_YEAR, nNativeNumberMode, true ) + 1009 getDisplayStringImpl( CalendarDisplayCode::LONG_ERA, nNativeNumberMode, true ); 1010 } 1011 return Calendar_gregorian::getDisplayString(nCalendarDisplayCode, nNativeNumberMode); 1012 } 1013 1014 OUString SAL_CALL 1015 Calendar_gregorian::getImplementationName() 1016 { 1017 return OUString::createFromAscii(cCalendar); 1018 } 1019 1020 sal_Bool SAL_CALL 1021 Calendar_gregorian::supportsService(const OUString& rServiceName) 1022 { 1023 return cppu::supportsService(this, rServiceName); 1024 } 1025 1026 Sequence< OUString > SAL_CALL 1027 Calendar_gregorian::getSupportedServiceNames() 1028 { 1029 Sequence< OUString > aRet { OUString::createFromAscii(cCalendar) }; 1030 return aRet; 1031 } 1032 1033 } 1034 1035 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */ 1036
