' I added another function... isLeapYear
' Also... I had to redo the jdn2doy function.
' It NOW uses the coding from greg2doy.f90
' (which will make use the isLeapYear function)
' Originally I had found (and used) coding
' that looked virtually identical to David's code,
' BUT it had a flaw. I stumbled onto this flaw
' totally by accident. If memory serves me correctly...
' if you retrieved the day of year for...
'   March 1st, 1700 (1700/03/01)
' it returned day 61, instead of day 60.
' So I rewrote the function to match David's coding.
' (his coding calculates leap years correctly)

' All the functions below deal with a Julian Day Number.
'  Start by getting a Julian Day Number (JDN) with the
'  gdt2jdn function. Then you can pass that JDN, as a
'  parameter, to all the other functions.
'
' What are Julian Day Numbers good for?
'   Finding out how much time "has elapsed" between two dates.
'   Determining how much time "will elapse" between two dates.
'   Ease of storage of a date and time in a single number.
'   Security? Store a Julian Day Number in a file and it's
'   unlikely someone will know that it's a date and time.
'   (unless of course... you leave a clue in that file)
'
' I can not take credit for ANY of these functions.
' I acquired them from NASA.
'  (yep, I'm not kidding... thee... Goddard Space Flight Center)
'  The name of the author is... DAVID G. SIMPSON
'  You can check out his other FREE source codes at...
'  https://caps.gsfc.nasa.gov/simpson/software.html
'
' The accuracy of these functions... are limited.
' The safest lower limit is -0.500000 (aka -4713/01/01 00:00:00)
' The safest upper limit is unknown (I tried, but got mixed results)
' [but it is at least 3654146059.499989 (aka 9999999/12/31 23:59:59)]

' This is the ONLY function NOT ported from the website.
'  I created it to convert a YEAR, MONTH, and DAY into
'   a numeric value that will be used for date checking.
'  You can NOT use the result (from this function) for checking negative years!
'  That's because calendar dates, in negative years, are both forwards AND backwards.
'   (the days of the month go forward but the months and years go backwards)
' Input: YEAR, MONTH, DAY (as numbers)
' Output: A numeric value that combined the year, month, and day
'         (output format: yyyymmdd)
Function CalChk (ByVal year As Integer, ByVal month As Integer, ByVal day As Integer) As Integer
    CalChk = ((ABS(year) * 10000) + (month * 100) + day) * SGN(year)
End Function

' This function was ported from the... greg2jd.f90 link.
' Purpose: Convert a Gregorian Date (and Time) to a Julian Day Number
' Input: YEAR, MONTH, DAY, HOUR, MINUTE, SECOND (as numbers)
'        (HOUR, MINUTE, SECOND are optional)
' Output: Julian Day Number (as a number)
'         The time is stored in the fractional portion of the number.
' NOTE: the "hour" parameter (if used) has to be in military time!
'       (i.e. 12 am is 0, 6 am is 6, 12 pm is 12, 3 pm is 15, 6 pm is 18, 11 pm is 23)
' Also Note: all the parameters are integers. you will not be
'            able to pass a double/floating point/real number.
'            The only parameter that CAN (optionally) BE negative is the year.
Function gdt2jdn (ByVal year As Integer, ByVal month As Integer, ByVal day As Integer, ByVal hour As Integer, ByVal minute As Integer, ByVal second As Integer) As Double
    DEFINT HR = 0, MN = 0, SC = 0, YR = 0, MM = 0, DD = 0
    DEFDBL A = 0.0, B = 0.0, C = 0.0, D = 0.0, JT = 0.0

    IF (year = 0) THEN YR = (-1) ELSE YR = year
    MM = ABS(month)
    DD = ABS(day)
    HR = ABS(hour)
    MN = ABS(minute)
    SC = ABS(second)

    'the dates 1582/10/05 thru 1582/10/14 were removed (by Pope Gregory XIII)
    ' to compensate for an error between the Julian and Gregorian calendars.
    'if these dates are passed to this function,
    ' this will alter them, up or down, to the nearest acceptable date.
    IF (YR = 1582) AND (MM = 10) THEN
      IF (DD >= 10) AND (DD <= 14) THEN DD = 15
      IF (DD >= 5) AND (DD <= 9) THEN DD = 4
    END IF

    IF (SGN(YR) = (-1)) THEN YR = YR + 1

    'convert the hour, minute, and second to a fractional number
    JT = (HR / 24) + (MN / 1440) + (SC / 86400) - .5

    'combine the day and fractional time
    D = DD + JT

    IF (MM <= 2) THEN
      YR = YR - 1
      MM = MM + 12
    END IF

    'the purpose of this variable is to check for dates
    'greater than or equal to Oct.15th,1582
    C = CalChk(YR, MM, DD)

    IF (C >= 15821015) THEN
      A = FIX(YR / 100)
      B = 2 - A + FIX(A / 4)
    ELSE
      B = 0
    END IF

    gdt2jdn = FIX(365.25 * (YR + 4716)) + FIX(30.6001 * (MM + 1)) + B + D - 1524

    JT = 0.0 : D = 0.0 : C = 0.0 : B = 0.0 : A = 0.0
    DD = 0 : MM = 0 : YR = 0 : SC = 0 : MN = 0 : HR = 0
End Function

' This function was ported from the... jd2greg.f90 link.
' Purpose: Convert a Julian Day Number to a Gregorian Date and Time
' Input: Julian Day Number (as a number)
' Output: YEAR/MONTH/DAY HOUR:MINUTE:SECOND (as a string)
' Special Note: This function does NOT follow the standard
'               Julian Day Number to Gregorian Date conversion
'               rule. The rule being...
'               A new calendar date starts at noon (12:00:00)
'               and continues "up until" noon of the following day.
'               This is done to accommodate astronomer.
'               So I modified it to accommodate every one else.
' Footnote...
'   DO NOT modify the line of code for the result string!!!
'   The jdn2doy function AND isLeapYear function (below)
'    will NOT work if you change the format.
'  
'   Instead... just add coding to your main code that's something like...
'     DefStr gdt = jdn2gdt(1721424.000000000) 'date and time
'     DefStr gd = Left$(gdt, Instr(gdt, " ") - 1) 'date
'     DefStr gt = Mid$(gdt, Instr(gdt, " ") + 1)  'time
'     DefStr dg = Field$(gd, "/", 2) + "-" + Field$(gd, "/", 3) + "-" + Field$(gd, "/", 1)
'     gd contains YYYY/MM/DD and now dg will contain MM-DD-YYYY
'     (just so you're aware: for negative years it will look like... MM-DD--YYYY)
Function jdn2gdt (ByVal JDN As Double) As String
    DEFSTR sPAD = "0000", YRs = "", MMs = "", DDs = "", HRs = "", MNs = "", SCs = ""
    DEFINT YR = 0, MM = 0, DD = 0, HR = 0, MN = 0, SC = 0
    DEFDBL A = 0.0, B = 0.0, C = 0.0, D = 0.0, E = 0.0, F = 0.0, G = 0.0, JT = 0.0

    'calculate the hour, minute, and second
    '(from the fractional portion)
    JT = (JDN - FIX(JDN)) + 0.5
    IF (JT >= 1.0) THEN JT = JT - 1.0
    JT = JT * 24
    HR = FIX(JT)
    JT = (JT - HR) * 60
    MN = FIX(JT)
    JT = (JT - MN) * 60
    SC = ROUND(JT)
    IF (SC > 59) THEN
      MN = MN + 1
      SC = 0
    END IF
    IF (MN > 59) THEN
      HR = HR + 1
      MN = 0
    END IF

    'calculate the year, month, and day
    '(from the integer portion)
    A = FIX(JDN)
    B = JDN - A
    IF (B >= 0.5) THEN A = A + 1
    IF (A < 2299161) THEN
      B = A
    ELSE
      C = FIX((A - 1867216.25) / 36524.25)
      B = A + 1 + C - FIX(C / 4)
    END IF
    D = B + 1524
    E = FIX((D - 122.1) / 365.25)
    F = FIX(365.25 * E)
    G = FIX((D - F) / 30.6001)
    DD = D - F - FIX(30.6001 * G)
    IF (G < 14) THEN MM = G - 1 ELSE MM = G - 13
    IF (MM > 2) THEN YR = E - 4716 ELSE YR = E - 4715
    IF (YR < 1) THEN YR = YR - 1

    'convert the numeric variables to string variables
    YRs = LEFT$(sPAD, LEN(sPAD) - LEN(STRF$(ABS(YR),2,15,0))) + STRF$(ABS(YR),2,15,0)
    IF (SGN(YR) = (-1)) THEN YRs = "-" + YRs
    IF (MM < 10) THEN MMs = "0"+STR$(MM) ELSE MMs = STR$(MM)
    IF (DD < 10) THEN DDs = "0"+STR$(DD) ELSE DDs = STR$(DD)
    IF (HR < 10) THEN HRs = "0"+STR$(HR) ELSE HRs = STR$(HR)
    IF (MN < 10) THEN MNs = "0"+STR$(MN) ELSE MNs = STR$(MN)
    IF (SC < 10) THEN SCs = "0"+STR$(SC) ELSE SCs = STR$(SC)

    ' DO NOT modify this line of code for the result string!!!
    ' The jdn2doy function AND isLeapYear function (below)
    '  will NOT work if you change the format.
    jdn2gdt = YRs + "/" + MMs + "/" + DDs + " " + HRs + ":" + MNs + ":" + SCs

    JT = 0.0 : G = 0.0 : F = 0.0 : E = 0.0 : D = 0.0 : C = 0.0 : B = 0.0 : A = 0.0
    SC = 0 : MN = 0 : HR = 0 : DD = 0 : MM = 0 : YR = 0
    SCs = "" : MNs = "" : HRs = "" : DDs = "" : MMs = "" : YRs = "" : sPAD = ""
End Function

' This function was ported from the... leapyr.f90 link.
' (but... modified to work with a Julian Day Number instead of a Gregorian Date)
' Purpose: Determine if a Julian Day Number falls on a Leap Year
' Input: Julian Day Number (as a number)
' Output: True or False (as a number)
'         (true: 1, false: 0)
' Note: This function will NOT work if you change the format
'        of the (above) jdn2gdt function result string!
Function isLeapYear (ByVal JDN As Double) As Integer
    DEFSTR GDT = "", GD = ""
    DEFINT YEAR = 0, MONTH = 0, DAY = 0
    DEFINT LEAP = FALSE, C = 0

    'first convert the Julian Day Number back to a Gregorian Date
    GDT   = jdn2gdt(JDN)
    GD    = LEFT$(GDT, INSTR(GDT, " ") - 1)
    YEAR  = VAL(FIELD$(GD, "/", 1))
    MONTH = VAL(FIELD$(GD, "/", 2))
    DAY   = VAL(FIELD$(GD, "/", 3))

    'the standard rule for determining whether a year "is a leap year"
    IF ((YEAR MOD 4) = 0) THEN LEAP = TRUE

    'the purpose of this variable is to check for dates
    'greater than or equal to Oct.15th,1582
    C = CalChk(YEAR, MONTH, DAY)

    'the rule for determining whether a year "is a leap year" was
    ' "modified" to improve the accuracy of the Gregorian calendar.
    ' (after Pope Gregory XIII removed the 10 days in 1582)
    'another modification is being considered, and if accepted...
    ' will to take place in the year 4000.
    IF (C >= 15821015) THEN
      IF ((YEAR MOD 100) = 0) THEN LEAP = FALSE
      IF ((YEAR MOD 400) = 0) THEN LEAP = TRUE
    END IF

    isLeapYear = LEAP

    C = 0 : LEAP = 0
    DAY = 0 : MONTH = 0 : YEAR = 0
    GD = "" : GDT = ""
End Function

' This function was ported from the... greg2doy.f90 link.
' (but... modified to work with a Julian Day Number instead of a Gregorian Date)
' Purpose: Convert a Julian Day Number to a Day Of Year number
' Input: Julian Day Number (as a number)
' Output: the Day Of the Year (as a number)
'         (non-leap years: 1 to 365, leap years: 1 to 366)
' Note: This function will NOT work if you change the format
'        of the (above) jdn2gdt function result string!
Function jdn2doy (ByVal JDN As Double) As Integer
    DEFSTR GDT = "", GD = ""
    DEFINT YEAR = 0, MONTH = 0, DAY = 0
    DEFINT LEAP = FALSE, C = 0, K = 0, DOY = 0

    'first convert the Julian Day Number back to a Gregorian Date
    GDT   = jdn2gdt(JDN)
    GD    = LEFT$(GDT, INSTR(GDT, " ") - 1)
    YEAR  = VAL(FIELD$(GD, "/", 1))
    MONTH = VAL(FIELD$(GD, "/", 2))
    DAY   = VAL(FIELD$(GD, "/", 3))

    LEAP = isLeapYear(JDN)
    IF (LEAP = TRUE) THEN K = 1 ELSE K = 2

    DOY = FLOOR((275 * MONTH) / 9) - (K * FLOOR((MONTH + 9) / 12)) + DAY - 30

    'the purpose of this variable is to check for dates
    'greater than or equal to Oct.15th,1582 (AND) less than or equal to Dec.31st,1582
    C = CalChk(YEAR, MONTH, DAY)

    'trivia: 1582 is the ONLY year in recorded history to only have
    '        355 days (all others have either 365 or 366 days)
    IF ((C >= 15821015) AND (C <= 15821231)) THEN DOY = DOY - 10

    jdn2doy = DOY

    DOY = 0 : K = 0  : C = 0 : LEAP = 0
    DAY = 0 : MONTH = 0 : YEAR = 0
    GD = "" : GDT = ""
End Function

' This function was ported from the... greg2dow_f90 link.
' (but... modified to work with a Julian Day Number instead of a Gregorian Date)
' Purpose: Convert a Julian Day Number to a Day Of Week name
' Input: Julian Day Number (as a number)
' Output: name of the Day Of the Week (as a string)
Function jdn2dow (ByVal JDN As Double) As String
    DEFINT WD = 0
    DEFSTR DOW(7) = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}

    WD = (JDN + 1.5) - (FIX((JDN + 1.5) / 7) * 7)
    jdn2dow = DOW(WD)

    FOR WD = 0 TO UBOUND(DOW) : DOW(WD) = "" : NEXT WD
    WD = 0
End Function

