

proc APSMakeFourDigitYearHelp {} {
    return "Usage: APSMakeFourDigitYear <year>
Returns four-digit version of <year>, assuming that the actual year is
later than 1989."
}

# APSMakeFourDigitYear 
# Convert two-digit year to four-digit year, assuming year is
# between 1990 and 2089
#
proc APSMakeFourDigitYear {year} {
    scan $year "%ld" year
    if $year>100 {
        return $year
    }
    if $year<90 {
        incr year 2000
    } else {
        incr year 1900
    }
    return $year
}

proc APSFormatDateHelp {} {
    return "Usage: APSFormatDate 
{-year <year> {-month <month> -day <day> | -julianDay <jDay>} | -dateList <list>}
\[-twoDigitYear 1\] \[-dateFormat {Y-J-MD YMD Y-M-D MDY list}\] \[leadingZeros 0\]
For -dateFormat list, the information is returned in the form <year> <julianDay> <month> <day>."
}

# APSFormatDate 
# Put date into one of several formats 
#
proc APSFormatDate {args} {
    set year ""
    set month ""
    set day ""
    set julianDay ""
    set twoDigitYear 0
    set dateFormat ""
    set dateList {}
    set leadingZeros 1
    APSStrictParseArguments {year month day julianDay dateList twoDigitYear dateFormat leadingZeros}
    foreach var {year month day julianDay} {
        set value [subst \$$var]
        if [string length $value] {
            scan $value %ld $var
        }
    }
    if [llength $dateList] {
        # list: year julian-day month day 
        if [llength $dateList]!=4 {
            return 0
        }
        scan [lindex $dateList 0] %ld year
        scan [lindex $dateList 1] %ld julianDay
        scan [lindex $dateList 2] %ld month
        scan [lindex $dateList 3] %ld day
    } else {
        if ![string length $year] {
            return 0
        }
        if [string length $julianDay] {
            set mmdd [APSMonthDay -julianDay $julianDay -year $year]
            set month [lindex $mmdd 0]
            set day [lindex $mmdd 1]
        } else {
            set julianDay [APSJulianDay -day $day -month $month \
                             -year $year]
        }
    }

    set month [format %02ld $month]
    set day   [format %02ld $day]
    set julianDay [format %03ld $julianDay]

    if !$twoDigitYear {
        set year [APSMakeFourDigitYear $year]
    } else {
        scan $year %ld year
        set year [format %02ld [expr $year - int($year/100)*100]]
    }
    
    switch $dateFormat {
        "Y-J-MD" { 
            return ${year}-${julianDay}-${month}${day}
        }
        "YMD" { 
            return ${year}${month}${day}
        }
        "Y-M-D" {
            return ${year}-${month}-${day}
        }
        "MDY" {
            return ${month}${day}${year}
        }
        "list" {
            if !$leadingZeros {
                scan $month "%ld" month
                scan $day "%ld" day
                scan $julianDay "%ld" julianDay
            }
            return [concat $year $julianDay $month $day]
        }
        default {
            return ${year}${month}${day}
        }
    }
}


proc APSDateBreakDownHelp {} {
    return "Usage: APSDateBreakDown \[-dayVariable <string>\] \[-monthVariable <string>\] \[-yearVariable <string>\]
\[-hourVariable <string>\] \[-julianDayVariable <string>\] \[-twoDigitYear 0\] \[-leadingZeros 0\]
Puts the day, month, year, hour, and/or Julian day for the present date into the given variables."
}
        
# APSDateBreakDown
# Puts the day, month, and year into variables
#
proc APSDateBreakDown {args} {
    set dayVariable ""
    set monthVariable ""
    set yearVariable ""
    set hourVariable ""
    set julianDayVariable ""
    set twoDigitYear 1
    set leadingZeros 1
    APSStrictParseArguments \
      {julianDayVariable dayVariable monthVariable yearVariable \
         hourVariable twoDigitYear leadingZeros}
    catch {clock format [clock seconds] -format %d:%m:%y:%j:%H:%M:%S} result
    set dateList [split $result :]
    if !$leadingZeros {
        scan [lindex $dateList 0] "%ld" day  
        scan [lindex $dateList 1] "%ld" month
        scan [lindex $dateList 2] "%ld" year 
        scan [lindex $dateList 3] "%ld" jday 
    } else {
        set day   [lindex $dateList 0]
        set month [lindex $dateList 1]
        set year  [lindex $dateList 2]
        set jday  [lindex $dateList 3]
    }
    scan [lindex $dateList 4] "%ld" hour
    scan [lindex $dateList 5] "%ld" min
    scan [lindex $dateList 6] "%ld" sec

    if [string length $dayVariable] {
        uplevel "set $dayVariable $day"
    }
    if [string length $monthVariable] {
        uplevel "set $monthVariable $month"
    }
    if [string length $julianDayVariable] {
        uplevel "set $julianDayVariable $jday"
    }
    if [string length $yearVariable] {
        if !$twoDigitYear {
            set year [APSMakeFourDigitYear $year]
        }
        uplevel "set $yearVariable $year"
    }
    if [string length $hourVariable] {
        uplevel "set $hourVariable [expr ($sec/60.0+$min)/60.0+$hour]"
    }
}


proc APSLeapYearHelp {} {
    return "Usage: APSLeapYear <year>
Returns 1 (0) if <year> is (is not) a leap year."
}

# APSLeapYear:
# Returns 0/1 for nonleap/leap year
# Returns -1 on error
#
proc APSLeapYear {year} {
    if {$year<0} {
        return -1
    }
    set year [APSMakeFourDigitYear $year]
    return [expr $year%4==0 && $year%100!=0 || $year%400==0]
}

proc APSDefineDaysInMonths {} {
    # days in each month for leap years, nonleap years
    global apsDaysInMonths
    if {![info exists apsDaysInMonths]} {
        set apsDaysInMonths(0,1) 31 
        set apsDaysInMonths(0,2) 28 
        set apsDaysInMonths(0,3) 31 
        set apsDaysInMonths(0,4) 30 
        set apsDaysInMonths(0,5) 31 
        set apsDaysInMonths(0,6) 30 
        set apsDaysInMonths(0,7) 31 
        set apsDaysInMonths(0,8) 31 
        set apsDaysInMonths(0,9) 30 
        set apsDaysInMonths(0,10) 31 
        set apsDaysInMonths(0,11) 30 
        set apsDaysInMonths(0,12) 31
        set apsDaysInMonths(1,1) 31 
        set apsDaysInMonths(1,2) 29
        set apsDaysInMonths(1,3) 31 
        set apsDaysInMonths(1,4) 30 
        set apsDaysInMonths(1,5) 31 
        set apsDaysInMonths(1,6) 30 
        set apsDaysInMonths(1,7) 31 
        set apsDaysInMonths(1,8) 31 
        set apsDaysInMonths(1,9) 30 
        set apsDaysInMonths(1,10) 31 
        set apsDaysInMonths(1,11) 30 
        set apsDaysInMonths(1,12) 31
    }
}

proc APSJulianDayHelp {} {
    return "Usage: APSJulianDay -day <number> -month <number> -year <number>
Returns the Julian day (day of year between 1 and 366) for the given date."
}

# APSJulianDay:
# Computes the Julian day from the year,month, and day
#
proc APSJulianDay {args} {
    set day 0
    set month 0
    set year 0
    APSStrictParseArguments {day month year}
    if [llength $args] { return 0 }
    if {[string length $day]==0 || [string length $month]==0 || [string length $year]==0} {
        return 0
    }
    scan $day "%ld" day
    scan $month "%ld" month
    scan $year "%ld" year
    if {$day<1 || $day>31 || $month<1 || $month>12 || $year<0} {
        return 0
    }
    set leapYear [APSLeapYear $year]

    APSDefineDaysInMonths
    global apsDaysInMonths 
    set daysInMonth [subst \$apsDaysInMonths($leapYear,$month)]
    if {$day>$daysInMonth} {
        set day $daysInMonth
    }

    set jday 0
    incr jday $day
    for {set i 1} {$i<$month} {incr i} {
        incr jday [subst \$apsDaysInMonths($leapYear,$i)]
    }
    return $jday
}

proc APSMonthDayHelp {} {
    return "Usage: APSMonthDay -year <number> -julianDay <number>
Returns a list of the month and day for the given year and day-of-year."
}

# APSMonthDay
# Computes day of month and month from julian day and year 
#

proc APSMonthDay {args} {
    set julianDay 0
    set year 0
    APSStrictParseArguments {julianDay year}
    if {[llength $julianDay]==0 || [llength $year]==0} {
        return 0
    }
    scan $julianDay "%ld" julianDay
    scan $year "%ld" year
    if {$julianDay<0 || $julianDay>366 || $year<0} {
        return 0
    }
    set leapYear [APSLeapYear  $year]
    if {$leapYear==0 && $julianDay>=365} {
        return {12 31}
    }
    set sum 0
    APSDefineDaysInMonths
    global apsDaysInMonths 
    for {set i 1} {$i<=12} {incr i} {
        set days [subst \$apsDaysInMonths($leapYear,$i)]
        if {[expr $sum+$days]<$julianDay} {
            incr sum $days
        } else {
            break
        }
    }
    set dayOfMonth [expr $julianDay-$sum]
    return [list $i $dayOfMonth]
}


proc APSDateConversionTest {} {
    set errors 0
    for {set year 1990} {$year<2010} {incr year} {
        if {[APSLeapYear $year]==1} {
            set dlimit 367
        } else {
            set dlimit 366
        }
        for {set jd 1} {$jd<$dlimit} {incr jd} {
            set md [APSMonthDay -julianDay $jd -year $year]
            set jd1 [APSJulianDay -day [lindex $md 1] -month [lindex $md 0] -year $year]
            if {$jd!=$jd1} {
                puts "$jd $jd1 $year [lindex $md 0] [lindex $md 1]"
                incr errors
            }
        }
        puts "Done with $year"
    }
    puts "$errors errors"
}

proc APSTodaysDateInfoHelp {} {
    return "Usage: APSTodaysDateInfo \[-dateFormat {Y-J-MD YMD Y-M-D MDY list}\] \[-twoDigitYear 1\]
Obsolete. Returns date information for today.  APSOffsetDateInfo is the preferred routine.
For -dateFormat list, the information is returned in the form <year> <julianDay> <month> <day>."
}


# APSTodaysDateInfo
# Returns date information for today
#
proc APSTodaysDateInfo {args} {
    set twoDigitYear 0
    set dateFormat list
    APSStrictParseArguments {dateFormat twoDigitYear}
    set year 0
    set julianDay 0
    APSDateBreakDown \
      -yearVariable year -julianDayVariable julianDay
    return [APSFormatDate -year $year -julianDay $julianDay \
              -dateFormat $dateFormat]
}

proc APSOffsetDateInfoHelp {} {
    return "Usage: APSOffsetDateInfo \[-offset <integer>\]
{-today 1 | -year <year> {-month <month> -day <day> | -julianDay <jDay>} | -dateList <list>}
\[-twoDigitYear 1\] \[-dateFormat {Y-J-MD YMD Y-M-D MDY list}\] \[leadingZeros 0\]
For -dateFormat list, the information is returned in the form <year> <julianDay> <month> <day>."
}

#
# APSOffsetDateInfo
# Returns date info for a day offset by N days from given day
#
proc APSOffsetDateInfo {args} {
    set julianDay ""
    set year ""
    set month ""
    set day ""
    set dateList {}
    set twoDigitYear 0
    set dateFormat list
    set offset 0
    set today 0
    set leadingZeros 1
    APSStrictParseArguments {julianDay year month day dateFormat twoDigitYear dateList offset today leadingZeros}
    if $today {
        APSDateBreakDown -yearVariable year -julianDayVariable julianDay -twoDigitYear 1
    } else {
        set dateListLen [llength $dateList]
        if $dateListLen {
            if {$dateListLen!=2 && $dateListLen!=4} {
                if [string compare $dateFormat list]==0 {
                    return {}
                }
                return 0
            }
            set year [lindex $dateList 0]
            set julianDay [lindex $dateList 1]
            if $dateListLen==4 {
                set month [lindex $dateList 2]
                set day [lindex $dateList 3]
            }
        }
    }

    if ![string length $year] {
        puts stderr "Empty year string (APSOffsetDateInfo)"
        if [string compare $dateFormat list]==0 {
            return {}
        }
        return 0
    }
    set year [APSMakeFourDigitYear $year]
    if [string length $julianDay]==0 {
        set julianDay [APSJulianDay -day $day -month $month -year $year]
        if !$julianDay {
            puts stderr "Bad month/day (APSOffsetDateInfo)"
            if [string compare $dateFormat list]==0 {
                return {}
            }
            return 0
        }
    }

    scan $julianDay %ld julianDay
    if $offset<0 {
        while {$offset} {
            set newJDay [expr $offset+$julianDay]
            if $newJDay<=0 {
                incr year -1
                set daysInYear 365
                if [APSLeapYear $year] {
                    set daysInYear 366
                }
                incr offset $julianDay
                set julianDay $daysInYear
            } else {
                set julianDay $newJDay
                set offset 0
            }
        }
    } else {
        set daysInYear 365
        if [APSLeapYear $year] {
            set daysInYear 366
        }
        while {$offset} {
            set newJDay [expr $offset+$julianDay]
            if $newJDay>$daysInYear {
                incr year 1
                incr offset [expr -($daysInYear-$julianDay)-1]
                set julianDay 1
                set daysInYear 365
                if [APSLeapYear $year] {
                    set daysInYear 366
                }
            } else {
                set julianDay $newJDay
                set offset 0
            }
        }
    }
        
    return [APSFormatDate -year $year -julianDay $julianDay \
              -dateFormat $dateFormat -leadingZeros $leadingZeros ]
}

proc APSDateOffsetTest {} {
    for {set year 1990} {$year<=2010} {incr year} {
        set daysInYear 365
        if [APSLeapYear $year] {
            set daysInYear 366 
        }
        for {set jday 1} {$jday<=$daysInYear} {incr jday} {
            puts "Working on $jday of $year"
            set refDate [APSFormatDate -dateFormat Y-J-MD -year $year -julianDay $jday]
            for {set offset -1000} {$offset<=1000} {incr offset 500} {
                set offDateList [APSOffsetDateInfo -dateFormat list -year $year -julianDay $jday -offset $offset]
                set compDate [APSOffsetDateInfo -dateFormat Y-J-MD -dateList $offDateList -offset [expr -$offset]]
                if [string compare $refDate $compDate] {
                    puts "Error: $compDate (offset $offset) does not match $refDate"
                }
            }
        }
    }
}

proc APSIncrementDateVariablesHelp {} {
    return "APSIncrementDateVariables\n\[-dayVariable <name>\]
\[-monthVariable <name>\] \[-yearVariable <name>\]
\[-offset <number>\] \[-units {day | month | year}\]"
}

proc APSIncrementDateVariables {args} {
    set dayVariable ""
    set monthVariable ""
    set yearVariable ""
    set offset 0
    set unit day

    APSStrictParseArguments {dayVariable monthVariable yearVariable offset unit}
    if !$offset {
        return 1
    }
    global $dayVariable $monthVariable $yearVariable
    if [string compare $unit day]==0 {
        set newDateList \
          [APSOffsetDateInfo \
             -year [subst \$$yearVariable] \
             -month [subst \$$monthVariable] \
             -day [subst \$$dayVariable] \
             -offset $offset -twoDigitYear 0 \
             -dateFormat list -leadingZeros 0]
        if [llength $newDateList]==4 {
            set $yearVariable [lindex $newDateList 0]
            set $monthVariable [lindex $newDateList 2]
            set $dayVariable [lindex $newDateList 3]
        } else {
            return 0
        }
    } else {
        scan [subst \$$yearVariable] "%ld" year
        scan [subst \$$monthVariable] "%ld" month
        switch $unit {
            "month" {
                incr month $offset
                if $month<1 {
                    set month 12
                    incr year -1
                } elseif $month>12 {
                    set month 1
                    incr year 1
                }
            }
            "year" {
                incr year $offset
            }
        }
        set $yearVariable $year
        set $monthVariable $month
    }
}
