;+
; NAME:
;    nz_local_time.pro
;
; PURPOSE:
;    This function converts times between time zones within Aotearoa New 
;    Zealand, including Universal Time.
;
; CATEGORY:
;    Calendar
;
; CALLING SEQUENCE:
;    result = nz_local_time( time_in )
;
; INPUTS:
;    TIME_IN:  The required input time, as a scalar or vector string of format 
;        'yyyymmddhhmmss'.
;    ZONE_IN, ZONE_OUT
;
; KEYWORD PARAMETERS:
;    ZONE_IN:  An optional scalar string specifying the time zone of the input 
;        times.  Supported values are:
;        * 'CHAST':  Standard time in the Chatham Islands, UTC+12:45.
;        * 'CHAT':  Local time in the Chatham Islands, including standard time 
;            and daylight savings time depending on the time of year.
;        * 'NZST':  Standard time in the mainland Aotearoa New Zealand, 
;            UTC+12:00.  This is the default.
;        * 'NZT':  Local time in mainland Aotearoa New Zealand, including 
;            standard time and daylight savings time depending on the time of 
;            year.
;        * 'UTC':  Universal standard time.
;    ZONE_OUT:  An optional scalar string specifying the time zone of the 
;        output times.  Supported values are:
;        * 'CHAST':  Standard time in the Chatham Islands, UTC+12:45.
;        * 'CHAT':  Local time in the Chatham Islands, including standard time 
;            and daylight savings time depending on the time of year.
;        * 'NZST':  Standard time in the mainland Aotearoa New Zealand, 
;            UTC+12:00.
;        * 'NZT':  Local time in mainland Aotearoa New Zealand, including 
;            standard time and daylight savings time depending on the time of 
;            year.  This is the default.
;        * 'UTC':  Universal standard time.
;
; USES:
;    correct_date.pro
;    day_of_week.pro
;    month_day.pro
;
; PROCEDURE:
;    This procedure iterates through each input time value, a determines which 
;    of a set of rules is satisfied, and applies the shifts dictacted by the 
;    satisfied rules.
;
; REFERENCES:
;    https://www.statoids.com/tnz.html
;    https://www.legislation.govt.nz/regulation/public/2007/0185/latest/whole.html
;    https://en.wikipedia.org/wiki/Time_in_New_Zealand
;
; EXAMPLE:
;    ; This example demonstrates the shift to/from Daylight Time enacted on 8 
;    ; October 1989 and 18 March 1990.  Here we convert from Standard Time to 
;    ; local time.
;    time_0 = [ '19891008013000', '19891008023000', '19891008033000', $
;        '19900318013000', '19900318023000', '19900318033000' ]
;    time_1 = nz_local_time( time_in, zone_in='NZST', zone_out='NZT' )
;    print, time_1
;    ; The result should be 19891008013000, 19891008033000, 19891008043000, 
;    ; 19900318023000, 19900318023000, and 19900318033000.
;    ; The following converts back to Standard Time.  Note that the function 
;    ; figures out that only the first instance of '19900318023000' should 
;    ; change.
;    time_2 = nz_local_time( time_1, zone_in='NZT', zone_out='NZST' )
;    ; The result should be identical to time_0.
;
; MODIFICATION HISTORY:
;    Written by:  Daithi A. Stone (dastone@runbox.com), 2026-08-13
;-

;***********************************************************************

FUNCTION NZ_LOCAL_TIME, $
    TIME_IN, $
    ZONE_IN=zone_in, ZONE_OUT=zone_out

;***********************************************************************
; Constants and options

; The number of input times
n_time = n_elements( time_in )
if n_time eq 0 then stop
; Copy to updateable input
time_in_use = time_in

; Ensure correct input time format
temp = strlen( time_in )
if min( temp ) ne 14 then stop
if max( temp ) ne 14 then stop

; The default input time zone
if not( keyword_set( zone_in ) ) then zone_in = 'NZST'
; Confirm supported input time zone
temp = [ 'CHAST', 'CHAT', 'NZST', 'NZT', 'UTC' ]
if max( zone_in eq temp ) eq 0 then stop
; Copy as an updateable zone label (for multiple steps)
zone_in_use = zone_in

; The default output time zone
if not( keyword_set( zone_out ) ) then zone_in = 'NZT'
; Confirm supported output time zone
temp = [ 'CHAST', 'CHAT', 'NZST', 'NZT', 'UTC' ]
if max( zone_out eq temp ) eq 0 then stop

; The number of days in each calendar month
month_len = transpose( month_day( indgen( 12 ) ) )
month_len = month_len[*,1] - month_len[*,0] + 1

;***********************************************************************
; Set time zone conversion rules

; These rules are expressed as rows in an array, with each row of the form
;   [ start year, start month, start day, start nth Sunday, start hour, 
;     start minute, start second, end year, end month, end day, end nth Sunday, 
;     end hour, end minute, end second, shift hour, shift minute, shift second ]
; Only one of "start day" and "start nth Sunday" should be entered, with the 
; other being "-1".
; "01" means the first Sunday of the month.
; "06" means the last Sunday of the month.
; Only one of "end day" and "end nth Sunday" should be entered, with the other 
; being "-1".

; If we want 'NZT' to 'UTC'
if ( zone_in_use eq 'NZT' ) and ( zone_out eq 'UTC'  ) then begin
  ; First convert NZT to NZST
  time_in_use = nz_local_time( time_in_use, zone_in='NZT', zone_out='NZST' )
  ; Now set for NZST to NZT conversion
  zone_in_use = 'NZST'
endif

; If we want 'UTC' to 'NZT'
if ( zone_in_use eq 'UTC' ) and ( zone_out eq 'NZT'  ) then begin
  ; First convert UTC to NZST
  time_in_use = nz_local_time( time_in_use, zone_in='UTC', zone_out='NZST' )
  ; Now set for NZST to NZT conversion
  zone_in_use = 'NZST'
endif

; If we want NZST to UTC
if ( zone_in_use eq 'NZST' ) and ( zone_out eq 'UTC' ) then begin
  ; Ensure we do not already have rules set
  if keyword_set( zone_rule ) then stop
  ; Set the rule
  zone_rule = $
      [ 0001, 01, 01, -1, 00, 00, 00, 9999, 12, 31, -1, 00, 00, 00, -12, 0, 0 ]
endif

; If we want UTC to NZST
if ( zone_in_use eq 'UTC' ) and ( zone_out eq 'NZST' ) then begin
  ; Ensure we do not already have rules set
  if keyword_set( zone_rule ) then stop
  ; Set the rule
  zone_rule = $
      [ 0001, 01, 01, -1, 00, 00, 00, 9999, 12, 31, -1, 00, 00, 00, 12, 0, 0 ]
endif

; If we want NZST to/from NZT
if ( ( zone_in_use eq 'NZST'  ) and ( zone_out eq 'NZT' ) ) $
    or ( ( zone_in_use eq 'NZT' ) and ( zone_out eq 'NZST'  ) ) then begin
  ; Ensure we do not already have rules set
  if keyword_set( zone_rule ) then stop
  ; Set the rules
  zone_rule = [ $
      ; If the date is since 2007/09/01.
      ; If the time is at or past 02:00:00NZST on the last Sunday of September 
      ; and before 02:00:00NZST on the first Sunday in April.
      [ 2007, 09, -1, 06, 02, 00, 00, 9999, 04, -1, 01, 02, 00, 00, 1, 0, 0 ], $
      ; If the date is between 1990/10/01 and 2007/03/31.
      ; If the time is at or past 02:00:00NZST on the first Sunday of October 
      ; and before 02:00:00NZST on the third Sunday in March.
      [ 1990, 10, -1, 01, 02, 00, 00, 2007, 03, -1, 03, 02, 00, 00, 1, 0, 0 ], $
      ; If the time is between 1989/10/08 02:00:00 and 1990/03/18 02:00:00
      [ 1989, 10, 08, -1, 02, 00, 00, 1990, 03, 18, -1, 02, 00, 00, 1, 0, 0 ], $
      ; If the date is between 1975/10/01 and 1989/03/31.
      ; If the time is at or past 02:00:00NZST on the last Sunday of October 
      ; and before 02:00:00NZST on the first Sunday in March.
      [ 1975, 10, -1, 06, 02, 00, 00, 1989, 03, -1, 01, 02, 00, 00, 1, 0, 0 ], $
      ; If the time is between 1974/11/03 02:00:00 and 1975/02/23 02:00:00
      [ 1974, 11, 03, -1, 02, 00, 00, 1975, 02, 23, -1, 02, 00, 00, 1, 0, 0 ], $
      ; If the date is between 1934/09/01 and 1940/04/30.
      ; If the time is at or past 02:00:00NZST on the last Sunday of September 
      ; and before 02:00:00NZST on the last Sunday in April.
      [ 1934, 09, -1, 06, 02, 00, 00, 1940, 04, -1, 06, 02, 00, 00, 1, 0, 0 ], $
      ; If the time is between 1933/10/08 02:00:00 and 1934/04/29 02:00:00
      [ 1933, 10, 08, -1, 02, 00, 00, 1934, 04, 29, -1, 02, 00, 00, 1, 0, 0 ], $
      ; If the date is between 1930/10/01 and 1933/04/30.
      ; If the time is at or past 02:00:00NZST on the second Sunday of October 
      ; and before 02:00:00NZST on the third Sunday in April.
      [ 1930, 10, -1, 02, 02, 00, 00, 1933, 04, -1, 03, 02, 00, 00, 1, 0, 0 ], $
      ; If the time is between 1929/10/30 02:00:00 and 1930/03/16 02:00:00
      [ 1929, 10, 30, -1, 02, 00, 00, 1930, 03, 16, -1, 02, 00, 00, 1, 0, 0 ], $
      ; If the time is between 1928/11/04 02:00:00 and 1929/03/03 02:00:00
      [ 1928, 11, 04, -1, 02, 00, 00, 1929, 03, 03, -1, 02, 00, 00, 1, 0, 0 ], $
      ; If the time is between 1927/11/26 02:00:00 and 1928/03/04 02:00:00
      [ 1927, 11, 26, -1, 02, 00, 00, 1928, 03, 04, -1, 02, 00, 00, 1, 0, 0 ] ]
  ; Reverse rules if coming from local time
  if ( zone_in_use eq 'NZT' ) and ( zone_out eq 'NZST' ) then begin
    zone_rule[4,*] = zone_rule[4,*] + zone_rule[14,*]
    zone_rule[11,*] = zone_rule[11,*] + zone_rule[14,*]
    zone_rule[14,*] = -zone_rule[14,*]
  endif
endif

; If we want CHAST to/from CHAT
if ( ( zone_in_use eq 'CHAST'  ) and ( zone_out eq 'CHAT' ) ) $
    or ( ( zone_in_use eq 'CHAT' ) and ( zone_out eq 'CHAST'  ) ) then begin
  ; Ensure we do not already have rules set
  if keyword_set( zone_rule ) then stop
  ; Set the rules
  zone_rule = [ $
      ; If the date is since 2007/09/01.
      ; If the time is at or past 02:45:00CHAST on the last Sunday of September 
      ; and before 02:45:00CHAST on the first Sunday in April.
      [ 2007, 09, -1, 06, 02, 45, 00, 9999, 04, -1, 01, 02, 45, 00, 1, 0, 0 ], $
      ; If the date is between 1990/10/01 and 2007/03/31.
      ; If the time is at or past 02:00:00NZST on the first Sunday of October 
      ; and before 02:00:00NZST on the third Sunday in March.
      [ 1990, 10, -1, 01, 02, 45, 00, 2007, 03, -1, 03, 02, 45, 00, 1, 0, 0 ], $
      ; If the time is between 1989/10/08 02:00:00 and 1990/03/18 02:00:00
      [ 1989, 10, 08, -1, 02, 45, 00, 1990, 03, 18, -1, 02, 45, 00, 1, 0, 0 ], $
      ; If the date is between 1975/10/01 and 1989/03/31.
      ; If the time is at or past 02:00:00NZST on the last Sunday of October 
      ; and before 02:00:00NZST on the first Sunday in March.
      [ 1975, 10, -1, 06, 02, 45, 00, 1989, 03, -1, 01, 02, 45, 00, 1, 0, 0 ], $
      ; If the time is between 1974/11/03 02:00:00 and 1975/02/23 02:00:00
      [ 1974, 11, 03, -1, 02, 45, 00, 1975, 02, 23, -1, 02, 45, 00, 1, 0, 0 ] ]
  ; Reverse rules if coming from local time
  if ( zone_in_use eq 'CHAT' ) and ( zone_out eq 'CHAST' ) then begin
    zone_rule[4,*] = zone_rule[4,*] + zone_rule[14,*]
    zone_rule[11,*] = zone_rule[11,*] + zone_rule[14,*]
    zone_rule[14,*] = -zone_rule[14,*]
  endif
endif

; If no zone rules have been set then something is wrong
if not( keyword_set( zone_rule ) ) then stop

;***********************************************************************
; Calculate the time difference

; Initialise output time vector in array format
time_out = intarr( 6, n_time )
time_out[0,*] = fix( strmid( time_in_use, 0, 4 ) )
time_out[1,*] = fix( strmid( time_in_use, 4, 2 ) )
time_out[2,*] = fix( strmid( time_in_use, 6, 2 ) )
time_out[3,*] = fix( strmid( time_in_use, 8, 2 ) )
time_out[4,*] = fix( strmid( time_in_use, 10, 2 ) )
time_out[5,*] = fix( strmid( time_in_use, 12, 2 ) )

; Iterate through input times
for i_time = 0, n_time - 1 do begin

  ; Copy the current time
  time_working = time_out[*,i_time]

  ; Initialise a zero shift
  shift = [ 0, 0, 0 ]

  ; Identify which rules apply
  temp = time_working[0] * 100l + time_working[1] 
  id_rule = where( ( temp ge zone_rule[0,*] * 100l + zone_rule[1,*] ) $
      and ( temp le zone_rule[7,*] * 100l + zone_rule[8,*] ), n_id_rule )

  ; Iterate through applicable rules
  for i_rule = 0, n_id_rule - 1 do begin
    ; Copy rule
    temp_rule = zone_rule[*,id_rule[i_rule]]
    ; Define the start day of the rule if needed
    if temp_rule[2] eq -1 then begin
      ; Determine date of the relevant Sunday for this rule
      temp = str( time_working[0], length=4, filler='0' ) $
          + str( temp_rule[1], length=2, filler='0' ) $
          + str( indgen( month_len[temp_rule[1]-1] ) + 1, length=2, filler='0' )
      index_sunday = where( day_of_week( temp ) eq 'Sunday' )
      if temp_rule[3] eq 6 then begin
        temp_rule[2] = max( index_sunday ) + 1
      endif else begin
        temp_rule[2] = index_sunday[temp_rule[3]-1] + 1
      endelse
    endif
    ; Define the end day of the rule if needed
    if temp_rule[9] eq -1 then begin
      ; Determine date of the relevant Sunday for this rule
      temp = str( time_working[0], length=4, filler='0' ) $
          + str( temp_rule[8], length=2, filler='0' ) $
          + str( indgen( month_len[temp_rule[8]-1] ) + 1, length=2, filler='0' )
      index_sunday = where( day_of_week( temp ) eq 'Sunday' )
      if temp_rule[10] eq 6 then begin
        temp_rule[9] = max( index_sunday ) + 1
      endif else begin
        temp_rule[9] = index_sunday[temp_rule[10]-1] + 1
      endelse
    endif
    ; Set the start and end times in scalar string format (without the year)
    temp_start = str( temp_rule[1], length=2, filler='0' ) $
        + str( temp_rule[2], length=2, filler='0' ) $
        + str( temp_rule[4], length=2, filler='0' ) $
        + str( temp_rule[5], length=2, filler='0' ) $
        + str( temp_rule[6], length=2, filler='0' )
    temp_end = str( temp_rule[8], length=2, filler='0' ) $
        + str( temp_rule[9], length=2, filler='0' ) $
        + str( temp_rule[11], length=2, filler='0' ) $
        + str( temp_rule[12], length=2, filler='0' ) $
        + str( temp_rule[13], length=2, filler='0' )
    ; Set the input time in scalar string format (without the year)
    temp_time = str( time_working[1], length=2, filler='0' ) $
        + str( time_working[2], length=2, filler='0' ) $
        + str( time_working[3], length=2, filler='0' ) $
        + str( time_working[4], length=2, filler='0' ) $
        + str( time_working[5], length=2, filler='0' )
    ; If the end month is in the same year as the start month
    if temp_rule[1] lt temp_rule[8] then begin
      ; Determine if our time is between the start and end
      ; (Note we will only be here if we are in a year covered by the rule.)
      if ( temp_time ge temp_start ) and ( temp_time lt temp_end ) then begin
        ; If we are at the start time, only shift forward
        if temp_time eq temp_start then begin
          temp = total( float( temp_rule ) / [ 1., 60., 3600. ] )
          if temp gt 0. then shift = shift + temp_rule[14:16]
        ; Otherwise add the shift
        endif else begin
          ; Suppose that if a time equal to or greater than this one occurred 
          ; earlier in the series that it was that one, and not this one, that 
          ; should be given a negative shift
          if ( i_time gt 0 ) and ( min( temp_rule ) lt 0 ) then begin
            if max( time_in[0:i_time-1] ) lt time_in[i_time] then begin
              shift = shift + temp_rule[14:16]
            endif
          endif else begin
            shift = shift + temp_rule[14:16]
          endelse
        endelse
      endif
    ; If the end month is the year after the start month
    endif else if temp_rule[1] gt temp_rule[8] then begin
      ; Determine if our time is between the start and end
      if ( ( time_working[0] ge temp_rule[0] ) $
          and ( time_working[0] lt temp_rule[7] ) $
          and ( temp_time ge temp_start ) ) $
          or ( ( time_working[0] gt temp_rule[0] ) $
          and ( time_working[0] le temp_rule[7] ) $
          and ( temp_time lt temp_end ) ) then begin
        ; If we are at the start time, only shift forward
        if temp_time eq temp_start then begin
          temp = total( float( temp_rule ) / [ 1., 60., 3600. ] )
          if temp gt 0. then shift = shift + temp_rule[14:16]
        ; Otherwise add the shift
        endif else begin
          ; Suppose that if a time equal to or greater than this one occurred 
          ; earlier in the series that it was that one, and not this one, that 
          ; should be given a negative shift
          if ( i_time gt 0 ) and ( min( temp_rule ) lt 0 ) then begin
            if max( time_in[0:i_time-1] ) lt time_in[i_time] then begin
              shift = shift + temp_rule[14:16]
            endif
          endif else begin
            shift = shift + temp_rule[14:16]
          endelse
        endelse
      endif
    ; Not implemented if the start and end are the same month
    endif else begin
      stop
    endelse
  endfor

  ; If we have a shift to perform
  if min( shift eq 0 ) eq 0 then begin
    ; Add the shift to the existing time
    time_working[5] = time_working[5] + shift[2]
    time_working[4] = time_working[4] + shift[1]
    time_working[3] = time_working[3] + shift[0]
    ; Carry seconds to minutes
    if time_working[5] ge 60 then begin
      temp = time_working[5] / 60
      time_working[5] = time_working[5] - temp * 60
      time_working[4] = temp_minute + temp
    endif else if time_working[5] lt 0 then begin
      temp = -time_working[5] / 60 + 1
      time_working[5] = time_working[5] + temp * 60
      time_working[4] = time_working[4] - temp
    endif
    ; Carry minutes to hours
    if time_working[4] ge 60 then begin
      temp = time_working[4] / 60
      time_working[4] = time_working[4] - temp * 60
      time_working[3] = time_working[3] + temp
    endif else if time_working[4] lt 0 then begin
      temp = -time_working[4] / 60 + 1
      time_working[4] = time_working[4] + temp * 60
      time_working[3] = time_working[3] - temp
    endif
    ; Carry hours to days
    if time_working[3] ge 24 then begin
      temp = time_working[3] / 24
      time_working[3] = time_working[3] - temp * 24
      time_working[2] = time_working[2] + temp
    endif else if time_working[3] lt 0 then begin
      temp = -time_working[3] / 24 + 1
      time_working[3] = time_working[3] + temp * 24
      time_working[2] = time_working[2] - temp
    endif
    ; Correct date
    temp_date = correct_date( time_working[0], time_working[1], $
        time_working[2] )
    ; Record updated time
    time_out[*,i_time] = time_working
  endif

endfor

; Convert output time to long string format
time_out = str( time_out[0,*], length=4, filler='0' ) $
    + str( time_out[1,*], length=2, filler='0' ) $
    + str( time_out[2,*], length=2, filler='0' ) $
    + str( time_out[3,*], length=2, filler='0' ) $
    + str( time_out[4,*], length=2, filler='0' ) $
    + str( time_out[5,*], length=2, filler='0' )
time_out = reform( time_out )

;***********************************************************************
; The end

return, time_out
END
