' Gambas module file

Export

Private $aDay As String[]
Private $aMonth As String[]
Private $dEpoch As Date

Public Sub ToUnixTime({Date} As Date) As Long

  If Not $dEpoch Then $dEpoch = DateAdd(Date(1970, 1, 1), gb.Second, -System.TimeZone)
  Return DateDiff($dEpoch, {Date}, gb.Second)

End

Public Sub FromUnixTime(UnixTime As Long) As Date

  If Not $dEpoch Then $dEpoch = DateAdd(Date(1970, 1, 1), gb.Second, -System.TimeZone)
  Return $dEpoch + UnixTime / 86400

End

' Gregorian algorithm by Aloysius Lilius And Christophorus Clavius.
' Valid For any year since 1583. 

Public Sub EasterDay(Year As Integer) As Date

  Dim A, B, C, D, E, F, G As Integer
  
  If Year < 1583 Then Error.Raise("Year must be greater or equal than 1583")
  
  A = Year Mod 19 + 1
  B = Year Div 100 + 1
  C = (3 * B) Div 4 - 12
  D = (8 * B + 5) Div 25 - 5
  E = (Year * 5) Div 4 - 10 - C
  F = ((11 * A + 20 + D - C) Mod 30 + 30) Mod 30
  If F = 24 Or (F = 25 And A > 11) Then F = F + 1
  G = 44 - F
  If G < 21 Then G = G + 30
  Return DateAdd(Date(Year, 3, 1), gb.Day, G + 7 - (E + G) Mod 7 - 1)
  
End

Private Sub InitDaysMonths()

  If Not $aDay Then 
    $aDay = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
    $aMonth = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
  Endif

End

Public Sub ToUTC({Date} As Date) As Date

  Return {Date} + System.TimeZone / 86400

End

Public Sub FromUTC({Date} As Date) As Date

  Return {Date} - System.TimeZone / 86400

End

Private Sub GetRFC822Zone(sZone As String) As Float
  
  Dim fZone As Float
  
  Select Case sZone
    Case "UT", "GMT", "Z"
      fZone = 0
    Case "EDT"
      fZone = -4
    Case "EST", "CDT"
      fZone = -5
    Case "CST", "MDT"
      fZone = -6
    Case "MST", "PDT"
      fZone = -7
    Case "PST"
      fZone = -8
    Case Like "[A-I]"
      fZone = -(Asc(sZone) - 64)
    Case Like "[J-M]"
      fZone = -(Asc(sZone) - 65)
    Case Like "[N-Y]"
      fZone = Asc(sZone) - 77
    Case Like "[+-][0-1][0-9][0-5][0-9]"
      fZone = CInt(Left(sZone, 3)) + CInt(Mid$(sZone, 4)) / 60
    Case Else
      Error.Raise(Subst$("Unknown timezone '&1'", sZone))
  End Select
  
  Return fZone / 24
  
End

Public Sub ToRFC822({Date} As Date, Optional TimeZone As String = "GMT") As String
  
  InitDaysMonths
  {Date} = ToUTC({Date}) + GetRFC822Zone(TimeZone)
  Return $aDay[WeekDay({Date})] & ", " & Format(Day({Date}), "00") & " " & $aMonth[Month({Date}) - 1] & " " & Year({Date}) & " " & Format(Hour({Date}), "00") & ":" & Format(Minute({Date}), "00") & ":" & Format(Second({Date}), "00") & " " & TimeZone
  
End

Public Sub FromRFC822(Value As String, Optional ByRef TimeZone As String) As Date

  Dim aDate As String[]
  Dim dDate As Date
  Dim iPos As Integer
  Dim iWeekDay As Integer
  Dim iYear As Integer
  
  InitDaysMonths
  Value = Trim(Value)
  
  iPos = InStr(Value, ", ")
  If iPos Then
    iWeekDay = $aDay.Find(Trim(Left(Value, iPos - 1)))
    If iWeekDay < 0 Then Error.Raise("Unknown week day")
    Value = Trim(Mid$(Value, iPos + 2))
  Else
    iWeekDay = -1
  Endif
  
  aDate = Scan(Value, "* * * *:* *")
  If aDate.Count <> 6 Then Return
  
  iPos = InStr(aDate[4], ":")
  If iPos Then
    aDate.Add(Mid$(aDate[4], iPos + 1), 5)
    aDate[4] = Left(aDate[4], iPos - 1)
  Else
    aDate.Add("0", 5)
  Endif
  
  iYear = CInt(aDate[2])
  If iYear >= 0 And If iYear <= 99 Then iYear += 1900
  dDate = Date(iYear, $aMonth.Find(aDate[1]) + 1, CInt(aDate[0]), CInt(aDate[3]), CInt(aDate[4]), CInt(aDate[5]))
  If iWeekDay >= 0 And If WeekDay(dDate) <> iWeekDay Then Error.Raise("Incorrect week day")
  
  dDate = FromUTC(dDate) - GetRFC822Zone(aDate[6])
  
  TimeZone = aDate[6]
  Return dDate

Catch
  
  If Error.Class.Name = "Date" Then Error.Propagate
  Error.Raise("Not a RFC822 date format")
  
End

Public Sub IsWeekEnd({Date} As Date) As Boolean
  
  Dim iDay As Integer = WeekDay({Date})
  If iDay = gb.Sunday Or If iDay = gb.Saturday Then Return True
  
End

'' @{since 3.15}
''
'' Convert a date to the ISO-8601 format.
''
'' - ~Date~ is the date to convert.
'' - ~Timezone~ is the timezone of the date in RFC822 format. By default, the local timezone is used.

Public Sub ToISO8601({Date} As Date, Optional Timezone As String) As String

  Dim iYear As Integer
  Dim sResult As String
  Dim sMicroseconds As String
  Dim fTimeZone As Float
  
  If Not IsMissing(TimeZone) Then
    fTimeZone = GetRFC822Zone(TimeZone)
  Else
    fTimeZone = -System.TimeZone / 86400
  Endif
  
  {Date} = ToUTC({Date}) + fTimeZone

  If fTimeZone > 0 Then
    TimeZone = "+" & Format(CDate(fTimeZone), "hhnn")
  Else If fTimeZone < 0 Then
    TimeZone = "-" & Format(CDate(-fTimeZone), "hhnn")
  Else
    TimeZone = "Z"
  Endif
  
  iYear = Year({Date})
  
  If iYear Then
    
    If iYear < 0 Then sResult = "B"
    sResult &= Format(iYear, "0000") & "-" & Format(Month({Date}), "00") & "-" & Format(Day({Date}), "00")
    
  Endif
  
  sResult &= "T" & Format({Date}, "hhnnss")
  
  sMicroseconds = Format({Date}, "uuu")
  If sMicroseconds <> "000" Then sResult &= "." & sMicroseconds
  
  Return sResult & TimeZone
  
End

'' Return the number of days in a month.
''
'' - ~Month~: The month between '1' (January) and '12' (December).
'' - ~Year~: The year (used for leap years). If not specified, the current year is used.

Public Sub DaysInMonth(Month As Integer, Optional Year As Integer = Year(Now)) As Integer
  
  If Month = 12 Then Return 31
  Return Day(DateAdd(Date(Year, Month + 1, 1), gb.Day, -1))
  
End
