source: TOOLS/WATER_BUDGET/libIGCM_date.py

Last change on this file was 6764, checked in by omamce, 2 months ago

O.M. : water budget - version mars 2024

  • Property svn:keywords set to Date Revision HeadURL Author Id
File size: 19.1 KB
Line 
1#!/usr/bin/env python3
2'''
3This library handles date calculs and convertions in different calendars.
4
5Mostly conversion of IGCM_date.ksh to python.
6
7Dates  formar
8 - Human format     : [yy]yy-mm-dd
9 - Gregorian format : yymmdd
10 - Julian format    : yyddd
11
12  Types of calendars are possible :
13
14  - leap|gregorian|standard (other name leap) :
15      The normal calendar. The time origin for the
16      julian day in this case is 24 Nov -4713.
17  - noleap|365_day :
18      A 365 day year without leap years.
19  - all_leap|366_day :
20      A 366 day year with only leap years.
21  - 360d|360_day :
22      Year of 360 days with month of equal length.
23
24  Warning, to install, configure, run, use any of included software or
25  to read the associated documentation you'll need at least one (1)
26  brain in a reasonably working order. Lack of this implement will
27  void any warranties (either express or implied).  Authors assumes
28  no responsability for errors, omissions, data loss, or any other
29  consequences caused directly or indirectly by the usage of his
30  software by incorrectly or partially configured personal
31
32 SVN information
33 $Author$
34 $Date$
35 $Revision$
36 $Id$
37 $HeadURL$
38'''
39
40import numpy as np
41debug = False
42
43DefaultCalendarType = 'Gregorian'
44
45# Characteristics of the gregorian calender
46mth_length = np.array ( [31, 28, 31,  30,  31,  30,  31,  31,  30,  31,  30,  31] )
47mth_start  = np.array ( [ 0, 31, 59,  90, 120, 151, 181, 212, 243, 273, 304, 334] )
48mth_end    = mth_start + mth_length + 1  # A cause des bornes superieures de Python
49
50# Other caalendars
51mth_length365 = np.array ( [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] )
52mth_length366 = np.array ( [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] )
53mth_length360 = np.array ( [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30] )
54
55# List of possible names for calendar types
56Calendar_gregorian = [ 'leap', 'LEAP', 'Leap', 'gregorian', 'Gregorian', 'GREGORIAN' ]
57Calendar_360d      = [ '360d', '360_day', '360d', '360_days', '360D', '360_DAY', '360D', '360_DAYS' ]
58Calendar_noleap    = [ 'noleap', '365_day', '365_days', 'NOLEAP', '365_DAY', '365_DAYS',  ]
59Calendar_allleap   = [ 'all_leap', '366_day', 'allleap', '366_days', '336d', 'ALL_LEAP', '366_DAY', 'ALLLEAP', '366_DAYS', '336D',  ]
60
61# List of possible names for day, month and year
62YE_name = [ 'YE', 'YEAR', 'Years', 'years', 'YEAR', 'Year', 'year', 'YE', 'ye', 'Y', 'y' ]
63MO_name = [ 'MO', 'MONTHS', 'Months', 'months', 'MONTH', 'Month', 'month', 'MO', 'mo', 'M', 'm' ]
64DA_name = [ 'DA', 'DAYS', 'Days', 'days', 'DAY', 'Day', 'day', 'DA', 'da', 'D', 'd' ]
65
66# Still to do
67# function IGCM_date_DaysInNextPeriod
68# function IGCM_date_DaysInPreviousPeriod
69
70def GetMonthsLengths ( year, CalendarType=DefaultCalendarType ) :
71    '''
72    Returns the month lengths for a given year and calendar type
73    '''
74    if CalendarType in Calendar_360d :
75        zlengths = mth_length360
76       
77    if  CalendarType in Calendar_noleap :
78        zlengths = mth_length365
79       
80    if  CalendarType in Calendar_noleap :
81        zlengths = mth_length366
82
83    if CalendarType in Calendar_gregorian :
84        if IsLeapYear ( year, CalendarType) :
85           zlengths = mth_length366
86        else :
87           zlengths = mth_length365
88
89    return zlengths
90
91def DaysInMonth ( yy, mm=None, CalendarType=DefaultCalendarType) :
92    '''
93    Returns the number of days in a month
94   
95    Usage:  DaysInMonth ( yyyy    , mm, [CalendarType] )
96         or DaysInMonth ( yyyymmdd, [CalendarType] )
97         '''
98    if mm :
99        year = int(yy) ; month = int(mm)
100    else :
101        year, month = GetYearMonth (yy)
102       
103    length = GetMonthsLengths ( year, CalendarType=CalendarType )[ np.mod(month-1, 12) ]
104    return length
105   
106def DaysSinceJC ( date, CalendarType=DefaultCalendarType ) :
107    '''
108    Calculate the days difference between a date and 00010101
109
110    Computation is splitted in three case for the sake of speed
111    '''
112    yy, mo, da = GetYearMonthDay (date)
113    if yy < 500 :
114        date0 = '0101-01-01'
115        if CalendarType in Calendar_360d :
116            aux = -360
117        if CalendarType in Calendar_noleap :
118            aux = -365           
119        if CalendarType in Calendar_allleap :
120            aux = -366
121        if CalendarType in Calendar_gregorian :
122            aux = -366
123           
124    elif yy < 1500 :
125        date0 = '1001-01-01'
126        if CalendarType in Calendar_360d :
127           aux = 359640
128        if CalendarType in Calendar_noleap :
129            aux = 364635           
130        if CalendarType in Calendar_allleap :
131            aux = 365634
132        if CalendarType in Calendar_gregorian :
133            aux = 364877
134           
135    else :
136        date0 = '1901-01-01'
137        if CalendarType in Calendar_360d :
138           aux = 683640
139        if CalendarType in Calendar_noleap :
140            aux = 693135           
141        if CalendarType in Calendar_allleap :
142            aux = 695034
143        if CalendarType in Calendar_gregorian :
144            aux = 693595
145           
146    ndays = DaysBetweenDate ( date, date0 ) + aux
147     
148    return ndays
149
150def IsLeapYear ( year, CalendarType=DefaultCalendarType ) :
151    '''
152    True if Year is a leap year
153    '''
154    yy = int ( year )
155    zis_leap_year = None
156
157    # What is the CalendarType :
158    if CalendarType in Calendar_360d :
159        zis_leap_year = False
160    if CalendarType in Calendar_noleap :
161        zis_leap_year = False
162
163    if CalendarType in Calendar_allleap :
164        zis_leap_year = True
165       
166    if CalendarType in Calendar_gregorian :
167        # A year is a leap year if it is even divisible by 4
168        # but not evenly divisible by 100
169        # unless it is evenly divisible by 400
170       
171        # if it is evenly divisible by 400 it must be a leap year
172        if not zis_leap_year and np.mod ( yy, 400 ) == 0 :
173            zis_leap_year = True
174           
175        # if it is evenly divisible by 100 it must not be a leap year
176        if not zis_leap_year and np.mod ( yy, 100 ) == 0 :
177            zis_leap_year = False
178           
179        # if it is evenly divisible by 4 it must be a leap year
180        if not zis_leap_year and np.mod ( yy, 4 ) == 0 :
181            zis_leap_year = True
182           
183        if not zis_leap_year :
184            zis_leap_year = False
185
186    return zis_leap_year
187
188def DateFormat ( date ) :
189    '''
190    Get date format. Could be human or gregorian
191
192      [yy]yymmdd   is Gregorian
193      [yy]yy-mm-dd is Human
194    '''
195    if isinstance (date, str) :
196        if '-' in date :
197            zdate_format = 'Human'
198        else :
199            zdate_format = 'Gregorian'
200    if isinstance (date, int) : zdate_format = 'Gregorian'
201    return zdate_format
202
203def PrintDate ( ye, mo, da, pformat ) :
204    '''
205    Return a date in the requested format
206    '''
207    zPrintDate = None
208    if pformat == 'Human'     : zPrintDate = f'{ye:04d}-{mo:02d}-{da:02d}'
209    if pformat == 'Gregorian' : zPrintDate = f'{ye:04d}{mo:02d}{da:02d}'
210    return zPrintDate
211
212def ConvertFormatToGregorian ( date ) :
213    '''
214    From a yyyy-mm-dd or yyymmdd date format returns a yyymmdd date format
215    '''
216    return PrintDate ( *GetYearMonthDay ( date ), 'Gregorian' )
217
218def ConvertFormatToHuman ( date ) :
219    '''
220    From a yyyymmdd or yyymmdd date format returns a yyy-mm-dd date format
221    '''
222    return PrintDate ( *GetYearMonthDay ( date ), 'Human' )
223
224def GetYearMonthDay  ( date ) :
225    '''
226    Split Date in format [yy]yymmdd or [yy]yy-mm-dd to yy, mm, dd
227    '''
228    if isinstance (date, str) :
229        if '-' in date :
230            zz = date.split ('-')
231            if len(zz) == 3 :
232                ye, mo, da = zz
233            if len(zz) == 2 :
234                ye, mo = zz
235                da = 0
236            if len(zz) == 1 :
237                ye = zz
238                da = 0 ; mo = 0
239        else :
240            date = int (date)
241           
242    if isinstance (date, int) :
243        if date > 1000000 :
244            da = np.mod ( date, 100)
245            mo = np.mod ( date//100, 100)
246            ye = date // 10000
247        elif date > 100000 :
248            mo = np.mod ( date, 100)
249            ye = date // 100
250            da = 0
251        else :
252            ye = date
253            da = 0 ; mo = 0
254
255    if ye : ye = int (ye)
256    if mo : mo = int (mo)
257    if da : da = int (da)
258    return ye, mo, da
259
260def GetYearMonth ( date ) :
261    '''
262    Split Date in format [yy]yymmdd or [yy]yy-mm-dd to yy, mm, dd
263    '''
264    ye, mo, da = GetYearMonthDay (date)
265    return ye, mo
266
267def DateAddYear ( date, year_inc='1Y' ) :
268    '''
269    Add year(s) to date in format [yy]yymmdd or [yy]yy-mm-dd
270    '''
271    zformat = DateFormat ( date )
272    ye, mo, da = GetYearMonthDay ( date )
273
274    if isinstance ( year_inc, str) :
275        PeriodType, PeriodLength = AnaPeriod ( year_inc )
276        print ( f"DateAddYear {PeriodType=} {PeriodLength=}" )
277        if PeriodType == YE_name[0] :
278            year_inc = PeriodLength
279        else :
280            raise AttributeError ( f'Parameter {year_inc=} is not a year period' )
281           
282    ye_new = ye + year_inc
283    return PrintDate ( ye_new, mo, da, zformat)
284
285def CorrectYearMonth ( ye, mo) :
286    '''
287    Correct month values outside [1,12]
288    '''
289    mo_new = mo
290    ye_new = ye
291
292    while mo_new > 12 :
293        mo_new = mo_new - 12
294        ye_new = ye_new + 1
295
296    while mo_new < 1 :
297        mo_new = mo_new + 12
298        ye_new = ye_new - 1
299
300    return ye_new, mo_new
301
302def CorrectYearMonthDay (ye, mo, da, CalendarType=DefaultCalendarType) :
303    '''
304    Correct month values outside [1,12] and day outside month length
305    '''
306    ye_new, mo_new = CorrectYearMonth ( ye, mo)
307    da_new = da
308   
309    num_day = DaysInMonth (ye, mo, CalendarType)
310   
311    while da_new > num_day :
312        da_new = da_new - num_day
313        mo_new = mo_new + 1
314        ye_new, mo_new = CorrectYearMonth ( ye_new, mo_new)
315        num_day = DaysInMonth (ye_new, mo_new, CalendarType)
316    while da_new < 1 :
317        mo_new = mo_new - 1
318        num_day = DaysInMonth (ye_new, mo_new, CalendarType)
319        da_new = da_new + num_day
320        ye_new, mo_new = CorrectYearMonth ( ye_new, mo_new)
321        num_day = DaysInMonth (ye, mo, CalendarType)
322
323    return ye_new, mo_new, da_new
324
325def DateAddMonth ( date, month_inc=1, CalendarType=DefaultCalendarType, verbose=False ) :
326    '''
327    Add on year(s) to date in format [yy]yymmdd or [yy]yy-mm-dd
328    '''
329    zformat = DateFormat ( date )
330    ye, mo, da = GetYearMonthDay ( date )
331
332    if isinstance ( month_inc, str) :
333        PeriodType, PeriodLength = AnaPeriod ( month_inc )
334        if PeriodType == MO_name[0] :
335            month_inc = PeriodLength
336        else :
337            raise AttributeError ( f'Parameter {month} is not a month period' )
338
339    if month_inc < 0 :
340        ye_inc = -( -month_inc // 12)
341    else : 
342        ye_inc = month_inc // 12
343
344    ye_new = ye + ye_inc
345    mo_new = mo + month_inc # - ye_inc*12
346    ye_new, mo_new = CorrectYearMonth (ye_new, mo_new)
347    lday1 = DaysInMonth ( ye    , mo    , CalendarType=CalendarType )
348    lday2 = DaysInMonth ( ye_new, mo_new, CalendarType=CalendarType )
349    if da == lday1 : da_new = lday2
350    da_new = np.minimum ( da_new, lday2)
351
352    if verbose : print ( f'{ye=} {mo=} {da=} {ye_new=} {mo_new=} {lday1=} {lday2=} {da_new=}' )
353       
354    return PrintDate ( ye_new, mo_new, da_new, zformat)
355
356def DateAddPeriod ( date, period='1YE', CalendarType=DefaultCalendarType ) :
357    '''
358    Add a period to date in format [yy]yymmdd or [yy]yy-mm-dd
359    '''
360    zformat = DateFormat ( date )
361   
362
363    PeriodType, PeriodLength = AnaPeriod ( period )
364
365    if PeriodType == YE_name[0] :
366        new_date = DateAddYear ( date, year_inc=period )
367    if PeriodType == MO_name[0] :
368        new_date = DateAddMonth ( date, month_inc=period, CalendarType=DefaultCalendarType )
369    if PeriodType == DA_name[0] :
370        new_date = AddDaysToDate ( date, ndays=period, CalendarType=DefaultCalendarType )
371    if PeriodType == 'Unknow' :
372        raise AttributeError ( f"DateAddPeriod : period syntax {period=} not understood" )
373
374    ye, mo, da = GetYearMonthDay ( new_date )
375    return PrintDate ( ye, mo, da, zformat )
376   
377def SubOneDayToDate ( date, CalendarType=DefaultCalendarType) :
378    '''
379    Substracts one day to date in format [yy]yymmdd or [yy]yy-mm-dd
380    '''
381    zformat = DateFormat ( date )
382    ye, mo, da = GetYearMonthDay ( date )
383    zlength = GetMonthsLengths ( ye, CalendarType )
384
385    ye = int(ye) ; mo = int(mo) ; da=int(da)
386    if da ==  1 :
387        if mo == 1 :
388            da_new, mo_new, ye_new = zlength[-1  ], 12    , ye - 1
389        else       :
390            da_new, mo_new, ye_new = zlength[mo-2], mo - 1, ye
391    else :
392        da_new, mo_new, ye_new = da - 1, mo, ye
393
394    return PrintDate ( ye_new, mo_new, da_new, zformat)
395
396def AddOneDayToDate ( date, CalendarType=DefaultCalendarType ) :
397    '''
398    Add one day to date in format [yy]yymmdd or [yy]yy-mm-dd
399    '''
400    if debug : print ( f'AddOneDayToDate : {date=}' )
401    zformat = DateFormat ( date )
402    ye, mo, da = GetYearMonthDay ( date )
403    zlength = GetMonthsLengths ( ye, CalendarType )
404
405    ye_new = ye
406    mo_new = mo
407    da_new = da+1
408    if da_new > zlength [mo_new-1] :
409        da_new = 1
410        mo_new = mo_new + 1
411        if mo_new == 13 :
412            mo_new =  1
413            ye_new += 1
414
415    return PrintDate ( ye_new, mo_new, da_new, zformat )
416
417def AddDaysToDate ( date, ndays='1D', CalendarType=DefaultCalendarType ) :
418    '''
419    Add days to date in format [yy]yymmdd or [yy]yy-mm-dd
420    Number of days migth be negative
421    '''
422    zformat = DateFormat ( date )
423     
424    # Break it into pieces
425    yy, mm, dd = GetYearMonthDay ( date )
426
427    if isinstance (ndays, str) :
428        PeriodType, PeriodLength = AnaPeriod (ndays )
429        if PeriodType == DA_name[0] :
430            ndays=PeriodLength
431        else :
432            raise AttributeError ( f'{ndays=} is not a day period' )
433       
434    zdate0 = date
435   
436    if ndays > 0 :
437        for nn in np.arange (ndays) :
438            zdate0 = AddOneDayToDate ( zdate0, CalendarType )
439
440    if ndays < 0 :
441        for nn in np.arange (-ndays) :
442            zdate0 = SubOneDayToDate ( zdate0, CalendarType )
443
444    yy, mm, dd = GetYearMonthDay ( zdate0 )
445   
446    return PrintDate ( yy, mm, dd, zformat )
447
448def AddPeriodToDate ( date, period, CalendarType=DefaultCalendarType ) :
449    '''
450    Add a period to a date.
451    period is specified as '1D', '5YE', '3DA', etc ...
452    '''
453    ndays = DaysInCurrentPeriod ( date, period, CalendarType=CalendarType)
454    new_date = AddDaysToDate ( date, ndays=1, CalendarType=CalendarType )
455
456    return new_date
457
458def DaysInYear (year, CalendarType=DefaultCalendarType ) :
459    '''
460    Return the number of days in a year
461    '''
462    if CalendarType in Calendar_360d :
463        ndays = 360
464 
465    if  CalendarType in Calendar_noleap :
466        ndays = 365
467
468    if  CalendarType in Calendar_allleap :
469        ndays = 366
470
471    if CalendarType in Calendar_gregorian :
472        if IsLeapYear ( year, CalendarType ) :
473          ndays = 366
474        else :
475          ndays = 365
476
477    return ndays
478
479def DaysBetweenDate ( pdate1, pdate2, CalendarType=DefaultCalendarType ) :
480  '''
481  Calculates the days difference between two dates
482
483  This process subtracts pdate2 from pdate1. If pdate2 is larger
484  than pdate1 then reverse the arguments. The calculations are done
485  and then the sign is reversed.
486  '''
487  if pdate1 < pdate2 :
488    date1=pdate2 ; date2=pdate1
489  if pdate1 > pdate2 :
490    date1=pdate1 ; date2=pdate2
491   
492  if pdate1 == pdate2 :
493    res = 0
494  else :
495    res = 0
496    zdate1 = date2
497   
498    while zdate1 < date1  :
499      zdate1 = AddOneDayToDate ( zdate1, CalendarType)
500      res += 1
501   
502    # if argument 2 was larger than argument 1 then
503    # the arguments were reversed before calculating
504    # adjust by reversing the sign
505    if pdate1 < pdate2 : 
506      res = -res
507
508  # and output the results
509  return res
510
511def ConvertGregorianDateToJulian (date, CalendarType=DefaultCalendarType) :
512    '''
513    Convert yyyymmdd to yyyyddd
514    '''
515    ye, mo, da = GetYearMonthDay (date)
516    ndays = DaysBetweenDate ( PrintDate (ye,mo,da, 'Human'), PrintDate (ye,1,1, 'Human'), CalendarType=CalendarType )
517
518    return int ( f'{ye}{ndays+1:03d}' )
519   
520def ConvertJulianDateToGregorian (date, CalendarType=DefaultCalendarType) : 
521    '''
522    Convert yyyyddd to yyyymmdd
523    '''
524   
525    # Break apart the year and the days
526    zdate = int (date)
527    yy = zdate // 1000
528    dd = np.mod (zdate, 1000 )
529   
530    # subtract the number of days in each month starting from 1
531    # from the days in the date. When the day goes below 1, you
532    # have the current month. Add back the number of days in the
533    # month to get the correct day of the month
534    mm=1
535    while dd > 0 :
536        #print ( f'ConvertJulianDateToGregorian {yy=} {mm=} {dd=}' )
537        md = DaysInMonth ( yy, mm, CalendarType=CalendarType)
538        dd = dd - md
539        mm = mm + 1
540
541    # The loop steps one past the correct month, so back up the month
542    dd = dd + md
543    mm = mm - 1 
544   
545    # Assemble the results into a gregorian date
546    return PrintDate ( yy, mm, dd, 'Gregorian')
547
548def DaysInCurrentPeriod ( startdate, period, CalendarType=DefaultCalendarType ) :
549    '''
550    Give the numbers of days during the period from startdate date
551    '''
552    year, month, day = GetYearMonthDay ( startdate )
553    PeriodType, PeriodLength = AnaPeriod ( period )
554
555    if PeriodType == YE_name[0] :
556        PeriodLengthInYears = PeriodLength
557   
558        dateend = DateAddYear ( startdate, PeriodLengthInYears )
559        Length = DaysBetweenDate ( dateend, startdate )
560       
561    elif PeriodType == MO_name[0] :
562        PeriodLengthInMonths = PeriodLength
563
564        year0       = year
565        treatedYear = 0
566        Length      = 0
567        for i in np.arange ( PeriodLengthInMonths ) :
568            Length = Length + DaysInMonth ( year, month + i-12*treatedYear, CalendarType=CalendarType)
569            if month + i >= 12 * (treatedYear + 1) :
570                year = year0 + 1
571                treatedYear = treatedYear + 1
572
573    elif PeriodType == DA_name[0] :
574        PeriodLengthInDays = PeriodLength
575        Length = PeriodLengthInDays
576
577    else :
578      Length = None
579     
580    return Length
581
582def AnaPeriod ( period ) :
583    '''
584    Decodes a period definition like '1Y', ''1MO', 'DA', etc ...
585    Return period types (string) and period length (integer)
586    '''
587    periodName   = rmDigits  (period)
588    periodLength = getDigits (period)
589
590    if '-' in periodName :
591        Neg = True
592    else :
593        Neg = False
594
595    periodName = periodName.replace ( '-', '')
596   
597    if periodName in YE_name : 
598        PeriodType = YE_name[0]
599        if periodLength == '' :
600            PeriodLength = 1
601        else : 
602            PeriodLength = int ( periodLength )
603       
604    elif periodName in MO_name :
605        PeriodType = MO_name[0]
606        if periodLength == '' :
607            PeriodLength = 1
608        else : 
609            PeriodLength = int ( periodLength )
610
611    elif periodName in DA_name :
612        PeriodType = DA_name[0]
613        if periodLength == '' :
614            PeriodLength = 1
615        else : 
616            PeriodLength = int ( periodLength )
617
618    else :
619        PeriodType   = 'Unknown'
620        PeriodLength = 0
621
622    if Neg : PeriodLength = -PeriodLength
623     
624    return PeriodType, PeriodLength
625
626def getDigits ( s ) :
627    '''Extract digits in a string'''
628    return ''.join (i for i in s if i.isdigit())
629
630def rmDigits ( s ) :
631    '''Removes digits from aa string'''
632    return ''.join (i for i in s if not i.isdigit())
Note: See TracBrowser for help on using the repository browser.