FMS  2025.04
Flexible Modeling System
get_cal_time.F90
1 !***********************************************************************
2 !* Apache License 2.0
3 !*
4 !* This file is part of the GFDL Flexible Modeling System (FMS).
5 !*
6 !* Licensed under the Apache License, Version 2.0 (the "License");
7 !* you may not use this file except in compliance with the License.
8 !* You may obtain a copy of the License at
9 !*
10 !* http://www.apache.org/licenses/LICENSE-2.0
11 !*
12 !* FMS is distributed in the hope that it will be useful, but WITHOUT
13 !* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied;
14 !* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
15 !* PARTICULAR PURPOSE. See the License for the specific language
16 !* governing permissions and limitations under the License.
17 !***********************************************************************
18 !> @defgroup get_cal_time_mod get_cal_time_mod
19 !> @ingroup time_manager
20 !> @brief Given a time increment as a real number, and base time and calendar
21 !! as a character strings, returns time as a time_type variable.
22 
23 !> @addtogroup get_cal_time_mod
24 !> @{
25 module get_cal_time_mod
26 
27 use fms_mod, only: error_mesg, fatal, write_version_number, lowercase, &
29  mpp_pe, mpp_root_pe
30 
31 use time_manager_mod, only: time_type, operator(+), operator(-), set_time, get_time, &
32  no_calendar, thirty_day_months, noleap, julian, gregorian, &
35 use mpp_mod, only: input_nml_file
36 use platform_mod, only: r8_kind, r4_kind
37 
38 implicit none
39 private
40 
41 public :: get_cal_time
42 
43 logical :: module_is_initialized=.false. !> This module is initialized on
44  !! the first call to get_cal_time
45  !! because there is no constructor.
46 ! <NAMELIST NAME="get_cal_time_nml">
47 ! <DATA NAME="allow_calendar_conversion" TYPE="logical" DEFAULT=".true.">
48 ! This sets the default value of the optional argument named "permit_calendar_conversion" of get_cal_time.
49 ! This namelist is deprecated as of the memphis release.
50 ! If calendar conversion is not desired, then it is recommended that permit_calendar_conversion
51 ! be present in the call to get_cal_time and that it be set to .false.
52 ! </DATA>
53 
54 logical :: allow_calendar_conversion=.true.
55 
56 namelist / get_cal_time_nml / allow_calendar_conversion
57 ! </NAMELIST>
58 
59 ! Include variable "version" to be written to log file.
60 #include<file_version.h>
61 
62 !> Added for mixed precision support.
63 !! Updates force time_manager math to be done with kind=8 reals
64 !! _wrap just casts a passed in r4 to r8 and calls r8 version
65 interface get_cal_time
66  module procedure get_calendar_time
67  module procedure get_calendar_time_wrap
68 end interface
69 
70 contains
71 !> @brief Calculates what a given calendar time would be after a interval of time
72 !!
73 !! @param time_increment A time interval
74 !! @param units
75 !! Examples of acceptable values of units:
76 !! - 'days since 1980-01-01 00:00:00'
77 !! - 'hours since 1980-1-1 0:0:0'
78 !! - 'minutes since 0001-4-12'
79 !! The first word in the string must be
80 !! 'years', 'months', 'days', 'hours', 'minutes' or 'seconds'.
81 !! The second word must be 'since'.
82 !! year number must occupy 4 spaces.
83 !! Number of months, days, hours, minutes, seconds may occupy 1 or 2 spaces
84 !! year, month and day must be separated by a '-'
85 !! hour, minute, second must be separated by a ':'
86 !! hour, minute, second are optional. If not present then zero is assumed.
87 !!
88 !! Because months are not equal increments of time, and, for julian calendar,
89 !! neither are years, the 'years since' and 'month since' cases deserve
90 !! further explaination.
91 !!
92 !! When 'years since' is used:
93 !! The year number is increased by floor(time_increment) to obtain a time T1.
94 !! The year number is increased by floor(time_increment)+1 to obtain a time T2.
95 !! The time returned is T1 + (time_increment-floor(time_increment))*(T2-T1).
96 !!
97 !! When 'months since' is used:
98 !! The month number is increased by floor(time_increment). If it falls outside
99 !! to range 1 to 12 then it is adjusted along with the year number to convert
100 !! to a valid date. The number of days in the month of this date is used to
101 !! compute the time interval of the fraction.
102 !! That is:
103 !! The month number is increased by floor(time_increment) to obtain a time T1.
104 !! delt = the number of days in the month in which T1 falls.
105 !! The time returned is T1 + ((time_increment-floor(time_increment))*delt.
106 !! Two of the consequences of this scheme should be kept in mind.
107 !! -- The time since should not be from the 29'th to 31'st of a month,
108 !! since an invalid date is likely to result, triggering an error stop.
109 !! -- When time since is from the begining of a month, the fraction of a month
110 !! will never advance into the month after that which results from only
111 !! the whole number.
112 !!
113 !! When NO_CALENDAR is in effect, units attribute must specify a starting
114 !! day and second, with day number appearing first
115 !!
116 !! Example: 'days since 100 0' Indicates 100 days 0 seconds
117 !!
118 !! @param calendar
119 !! Acceptable values of calendar are:
120 !! 'noleap'
121 !! '365_day'
122 !! '360_day'
123 !! 'julian'
124 !! 'thirty_day_months'
125 !! 'no_calendar'
126 !!
127 !! @param optional permit_calendar_conversion
128 !! It is sometimes desirable to allow the value of the intent(in) argument
129 !! "calendar" to be different than the calendar in use by time_manager_mod.
130 !! If this is not desirable, then the optional variable "permit_calendar_conversion"
131 !! should be set to .false. so as to allow an error check.
132 !! When calendar conversion is done, the time returned is the time in the
133 !! time_manager's calendar, but corresponds to the date computed using the input calendar.
134 !! For example, suppose the time_manager is using the julian calendar and
135 !! the values of the input arguments of get_cal_time are:
136 !! time_increment = 59.0
137 !! units = 'days since 1980-1-1 00:00:00'
138 !! calendar = 'noleap'
139 !! Because it will use the noleap calendar to calculate the date, get_cal_time will return
140 !! value of time for midnight March 1 1980, but it will be time in the julian calendar
141 !! rather than the noleap calendar. It will never return a value of time corresponding
142 !! to anytime during the day Feb 29.
143 !!
144 !! Another example:
145 !! Suppose the time_manager is using either the noleap or julian calendars,
146 !! and the values of the input arguments are:
147 !! time_increment = 30.0
148 !! units = 'days since 1980-1-1'
149 !! calendar = 'thirty_day_months'
150 !! In this case get_cal_time will return the value of time for Feb 1 1980 00:00:00,
151 !! but in the time_manager's calendar.
152 !!
153 !! Calendar conversion may result in a fatal error when the input calendar type is
154 !! a calendar that has more days per year than that of the time_manager's calendar.
155 !! For example, if the input calendar type is julian and the time_manager's calendar
156 !! is thirty_day_months, then get_cal_time will try to convert Jan 31 to a time in
157 !! the thirty_day_months calendar, resulting in a fatal error.
158 !!
159 !! @note This option was originally coded to allow noleap calendar as input when
160 !! the julian calendar was in effect by the time_manager.
161 function get_calendar_time(time_increment, units, calendar, permit_calendar_conversion)
162 real(r8_kind), intent(in) :: time_increment
163 character(len=*), intent(in) :: units
164 character(len=*), intent(in) :: calendar
165 logical, intent(in), optional :: permit_calendar_conversion
167 integer :: year, month, day, hour, minute, second
168 integer :: i1, increment_seconds, increment_days, increment_years, increment_months
169 real(r8_kind) :: month_fraction
170 integer :: calendar_tm_i, calendar_in_i, ierr, io, logunit
171 logical :: correct_form
172 character(len=32) :: calendar_in_c
173 character(len=64) :: err_msg
174 type(time_type) :: base_time, base_time_plus_one_yr
175 real(r8_kind) :: dt
176 logical :: permit_conversion_local
177 integer, parameter :: spd_int = 86400 !< seconds per day as int
178 real(r8_kind), parameter :: spd_real = 86400.0_r8_kind !< seconds per day as 64 bit real
179 
180 if(.not.module_is_initialized) then
181  read (input_nml_file, get_cal_time_nml, iostat=io)
182  ierr = check_nml_error(io, 'get_cal_time_nml')
183 
184  call write_version_number("get_cal_time_MOD", version)
185  logunit = stdlog()
186  if(mpp_pe() == mpp_root_pe()) write (logunit, nml=get_cal_time_nml)
187  module_is_initialized = .true.
188 endif
189 
190 if(present(permit_calendar_conversion)) then
191  permit_conversion_local = permit_calendar_conversion
192 else
193  permit_conversion_local = allow_calendar_conversion
194 endif
195 
196 calendar_in_c = lowercase(trim(cut0(calendar)))
197 
198 correct_form = (trim(calendar_in_c)) == 'noleap' .or. (trim(calendar_in_c)) == '365_day' .or. &
199  (trim(calendar_in_c)) == '365_days' .or. &
200  (trim(calendar_in_c)) == '360_day' .or. (trim(calendar_in_c)) == 'julian' .or. &
201  (trim(calendar_in_c)) == 'no_calendar'.or. (trim(calendar_in_c)) == 'thirty_day_months' .or. &
202  (trim(calendar_in_c)) == 'gregorian'
203 
204 if(.not.correct_form) then
205  call error_mesg('get_calendar_time','"'//trim(calendar_in_c)//'"'// &
206  ' is not an acceptable calendar attribute. acceptable calendars are: '// &
207  ' noleap, 365_day, 365_days, 360_day, julian, no_calendar, thirty_day_months, gregorian',fatal)
208 endif
209 
210 calendar_tm_i = get_calendar_type()
211 
212 if(.not.permit_conversion_local) then
213  correct_form = (trim(calendar_in_c) == 'noleap' .and. calendar_tm_i == noleap) .or. &
214  (trim(calendar_in_c) == '365_day' .and. calendar_tm_i == noleap) .or. &
215  (trim(calendar_in_c) == '365_days' .and. calendar_tm_i == noleap) .or. &
216  (trim(calendar_in_c) == '360_day' .and. calendar_tm_i == thirty_day_months) .or. &
217  (trim(calendar_in_c) == 'thirty_day_months' .and. calendar_tm_i == thirty_day_months) .or. &
218  (trim(calendar_in_c) == 'julian' .and. calendar_tm_i == julian) .or. &
219  (trim(calendar_in_c) == 'no_calendar' .and. calendar_tm_i == no_calendar) .or. &
220  (trim(calendar_in_c) == 'gregorian' .and. calendar_tm_i == gregorian)
221  if(.not.correct_form) then
222  call error_mesg('get_calendar_time','calendar not consistent with calendar type in use by time_manager.'// &
223  ' calendar='//trim(calendar_in_c)//'. Type in use by time_manager='// &
224  & valid_calendar_types(calendar_tm_i),fatal)
225  endif
226 endif
227 
228 if (permit_conversion_local) then
229  select case (trim(calendar_in_c))
230  case ('noleap')
231  calendar_in_i = noleap
232  case ('365_day')
233  calendar_in_i = noleap
234  case ('365_days')
235  calendar_in_i = noleap
236  case ('360_day')
237  calendar_in_i = thirty_day_months
238  case ('thirty_day_months')
239  calendar_in_i = thirty_day_months
240  case ('julian')
241  calendar_in_i = julian
242  case ('no_calendar')
243  calendar_in_i = no_calendar
244  case ('gregorian')
245  calendar_in_i = gregorian
246  case default
247  call error_mesg('get_calendar_time', &
248  trim(calendar_in_c)//' is an invalid calendar type (specified in call to get_calendar_time)',fatal)
249  end select
250 else
251  calendar_in_i = calendar_tm_i
252 end if
253 
254 correct_form = lowercase(units(1:10)) == 'days since' .or. &
255  lowercase(units(1:11)) == 'hours since' .or. &
256  lowercase(units(1:13)) == 'minutes since' .or. &
257  lowercase(units(1:13)) == 'seconds since'
258 
259 if(calendar_in_i /= no_calendar) then
260  correct_form = correct_form .or. &
261  lowercase(units(1:11)) == 'years since' .or. &
262  lowercase(units(1:12)) == 'months since'
263 endif
264 
265 if(.not.correct_form) then
266  call error_mesg('get_calendar_time',trim(units)//' is an invalid string for units.' // &
267  ' units must begin with a time unit then the word "since"' // &
268  ' Valid time units are: "seconds" "minutes", "hours", "days", and, ' // &
269  ' except when NO_CALENDAR is in effect, "months" and "years"',fatal)
270 endif
271 
272 if(calendar_in_i /= calendar_tm_i) then
273 ! switch to calendar type specified as input argument,
274 ! will switch back before returning.
275  call set_calendar_type(calendar_in_i)
276 endif
277 
278 ! index(string, substring[,back])
279 ! Returns the starting position of substring as a substring of string,
280 ! or zero if it does not occur as a substring. Default value of back is
281 ! .false. If back is .false., the starting position of the first such
282 ! substring is returned. If back is .true., the starting position of the
283 ! last such substring is returned.
284 ! Returns zero if substring is not a substring of string (regardless of value of back)
285 
286 i1 = index(units,'since') + 5
287 if(calendar_in_i == no_calendar) then
288  base_time = set_time(units(i1:len_trim(units)))
289 else
290  base_time = set_date(units(i1:len_trim(units)))
291 endif
292 
293 if(lowercase(units(1:10)) == 'days since') then
294  increment_days = floor(time_increment)
295  increment_seconds = int(spd_real*(time_increment - real(increment_days, r8_kind)))
296 else if(lowercase(units(1:11)) == 'hours since') then
297  increment_days = floor(time_increment/24.0_r8_kind)
298  increment_seconds = int(spd_real*(time_increment/24.0_r8_kind - real(increment_days, r8_kind)))
299 else if(lowercase(units(1:13)) == 'minutes since') then
300  increment_days = floor(time_increment/1440.0_r8_kind)
301  increment_seconds = int(spd_real*(time_increment/1440.0_r8_kind - real(increment_days, r8_kind)))
302 else if(lowercase(units(1:13)) == 'seconds since') then
303  increment_days = floor(time_increment/spd_real)
304  increment_seconds = int(spd_real*(time_increment/spd_real - real(increment_days, r8_kind)))
305 else if(lowercase(units(1:11)) == 'years since') then
306 ! The time period between between (base_time + time_increment) and
307 ! (base_time + time_increment + 1 year) may be 360, 365, or 366 days.
308 ! This must be determined to handle time increments with year fractions.
309  call get_date(base_time, year,month,day,hour,minute,second)
310  base_time = set_date(year+floor(time_increment) ,month,day,hour,minute,second)
311  base_time_plus_one_yr = set_date(year+floor(time_increment)+1,month,day,hour,minute,second)
312  call get_time(base_time_plus_one_yr - base_time, second, day)
313  dt = real(day*spd_int+second, r8_kind)*(time_increment-real(floor(time_increment), r8_kind))
314  increment_days = floor(dt/spd_real)
315  increment_seconds = int(dt - real(increment_days*spd_int, r8_kind))
316 else if(lowercase(units(1:12)) == 'months since') then
317  month_fraction = time_increment - real(floor(time_increment), r8_kind)
318  increment_years = floor(time_increment/12.0_r8_kind)
319  increment_months = floor(time_increment) - 12*increment_years
320  call get_date(base_time, year,month,day,hour,minute,second)
321  base_time = set_date(year+increment_years,month+increment_months ,day,hour,minute,second)
322  dt = real( spd_int*days_in_month(base_time), r8_kind) * month_fraction
323  increment_days = floor(dt/spd_real)
324  increment_seconds = int(dt - real(increment_days, r8_kind)*spd_real)
325 else
326  call error_mesg('get_calendar_time','"'//trim(units)//'" is not an acceptable units attribute of time.'// &
327  & ' It must begin with: "years since", "months since", "days since", "hours since", "minutes since",'// &
328  & ' or "seconds since"',fatal)
329 endif
330 
331 if (calendar_in_i /= calendar_tm_i) then
332  if(calendar_in_i == no_calendar .or. calendar_tm_i == no_calendar) then
333  call error_mesg('get_calendar_time','Cannot do calendar conversion because input calendar is '// &
334  trim(valid_calendar_types(calendar_in_i))//' and time_manager is using '// &
335  trim(valid_calendar_types(calendar_tm_i))// &
336  ' Conversion cannot be done if either is NO_CALENDAR',fatal)
337  endif
338  call get_date(base_time,year, month, day, hour, minute, second)
339  get_calendar_time = set_date(year,month,day,hour,minute,second) + set_time(increment_seconds, increment_days)
340  call get_date(get_calendar_time,year,month,day,hour,minute,second)
341  call set_calendar_type(calendar_tm_i)
342  get_calendar_time = set_date(year,month,day,hour,minute,second, err_msg=err_msg)
343  if(err_msg /= '') then
344  call error_mesg('get_calendar_time','Error in function get_calendar_time: '//trim(err_msg)// &
345  ' Note that the time_manager is using the '//trim(valid_calendar_types(calendar_tm_i))//' calendar '// &
346  'while the calendar type passed to function get_calendar_time is '//calendar_in_c,fatal)
347  endif
348 else
349  get_calendar_time = base_time + set_time(increment_seconds, increment_days)
350 endif
351 
352 end function get_calendar_time
353 
354 !------------------------------------------------------------------------
355 
356 !> For mixed precision support, just casts to passed in increment to r8
357 function get_calendar_time_wrap(time_increment, units, calendar, permit_calendar_conversion)
358  real(r4_kind), intent(in) :: time_increment
359  character(len=*), intent(in) :: units
360  character(len=*), intent(in) :: calendar
361  logical, intent(in), optional :: permit_calendar_conversion
363 
364  get_calendar_time_wrap = get_cal_time( real(time_increment, r8_kind), units, calendar, &
365  permit_calendar_conversion=permit_calendar_conversion)
366 end function
367 
368 !------------------------------------------------------------------------
369 
370 function cut0(string)
371 character(len=256) :: cut0
372 character(len=*), intent(in) :: string
373 integer :: i
374 
375 cut0 = string
376 
377 do i=1,len(string)
378  if(ichar(string(i:i)) == 0 ) then
379  cut0(i:i) = ' '
380  endif
381 enddo
382 
383 return
384 end function cut0
385 !------------------------------------------------------------------------
386 end module get_cal_time_mod
387 !> @}
388 ! close documentation grouping
integer function, public check_nml_error(IOSTAT, NML_NAME)
Checks the iostat argument that is returned after reading a namelist and determines if the error code...
Definition: fms.F90:523
subroutine, public write_version_number(version, tag, unit)
Prints to the log file (or a specified unit) the version id string and tag name.
Definition: fms.F90:701
subroutine, public error_mesg(routine, message, level)
Print notes, warnings and error messages; terminates program for warning and error messages....
Definition: fms.F90:441
type(time_type) function get_calendar_time(time_increment, units, calendar, permit_calendar_conversion)
Calculates what a given calendar time would be after a interval of time.
type(time_type) function get_calendar_time_wrap(time_increment, units, calendar, permit_calendar_conversion)
For mixed precision support, just casts to passed in increment to r8.
Added for mixed precision support. Updates force time_manager math to be done with kind=8 reals _wrap...
integer function stdlog()
This function returns the current standard fortran unit numbers for log messages. Log messages,...
Definition: mpp_util.inc:58
integer function mpp_pe()
Returns processor ID.
Definition: mpp_util.inc:406
subroutine, public get_time(Time, seconds, days, ticks, err_msg)
Returns days and seconds ( < 86400 ) corresponding to a time. err_msg should be checked for any error...
character(len=24) function, public valid_calendar_types(ncal, err_msg)
Returns a character string that describes the calendar type corresponding to the input integer.
subroutine, public get_date(time, year, month, day, hour, minute, second, tick, err_msg)
Gets the date for different calendar types. Given a time_interval, returns the corresponding date und...
integer function, public days_in_month(Time, err_msg)
Given a time, computes the corresponding date given the selected date time mapping algorithm.
integer function, public get_calendar_type()
Returns default calendar type for mapping from time to date.
subroutine, public set_calendar_type(type, err_msg)
Sets calendar_type for mapping an interval to a date. For the Gregorian calendar, negative years and ...
Given an input date in year, month, days, etc., creates a time_type that represents this time interva...
Given some number of seconds and days, returns the corresponding time_type.
Type to represent amounts of time. Implemented as seconds and days to allow for larger intervals.