ccalendar.pyx 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. # cython: boundscheck=False
  2. """
  3. Cython implementations of functions resembling the stdlib calendar module
  4. """
  5. cimport cython
  6. from numpy cimport (
  7. int32_t,
  8. int64_t,
  9. )
  10. # ----------------------------------------------------------------------
  11. # Constants
  12. # Slightly more performant cython lookups than a 2D table
  13. # The first 12 entries correspond to month lengths for non-leap years.
  14. # The remaining 12 entries give month lengths for leap years
  15. cdef int32_t* days_per_month_array = [
  16. 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31,
  17. 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
  18. cdef int* em = [0, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334]
  19. # The first 13 entries give the month days elapsed as of the first of month N
  20. # (or the total number of days in the year for N=13) in non-leap years.
  21. # The remaining 13 entries give the days elapsed in leap years.
  22. cdef int32_t* month_offset = [
  23. 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365,
  24. 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366]
  25. # Canonical location for other modules to find name constants
  26. MONTHS = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL",
  27. "AUG", "SEP", "OCT", "NOV", "DEC"]
  28. # The first blank line is consistent with calendar.month_name in the calendar
  29. # standard library
  30. MONTHS_FULL = ["", "January", "February", "March", "April", "May", "June",
  31. "July", "August", "September", "October", "November",
  32. "December"]
  33. MONTH_NUMBERS = {name: num for num, name in enumerate(MONTHS)}
  34. cdef dict c_MONTH_NUMBERS = MONTH_NUMBERS
  35. MONTH_ALIASES = {(num + 1): name for num, name in enumerate(MONTHS)}
  36. MONTH_TO_CAL_NUM = {name: num + 1 for num, name in enumerate(MONTHS)}
  37. DAYS = ["MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"]
  38. DAYS_FULL = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday",
  39. "Saturday", "Sunday"]
  40. int_to_weekday = {num: name for num, name in enumerate(DAYS)}
  41. weekday_to_int = {int_to_weekday[key]: key for key in int_to_weekday}
  42. # ----------------------------------------------------------------------
  43. @cython.wraparound(False)
  44. @cython.boundscheck(False)
  45. cpdef int32_t get_days_in_month(int year, Py_ssize_t month) nogil:
  46. """
  47. Return the number of days in the given month of the given year.
  48. Parameters
  49. ----------
  50. year : int
  51. month : int
  52. Returns
  53. -------
  54. days_in_month : int
  55. Notes
  56. -----
  57. Assumes that the arguments are valid. Passing a month not between 1 and 12
  58. risks a segfault.
  59. """
  60. return days_per_month_array[12 * is_leapyear(year) + month - 1]
  61. @cython.wraparound(False)
  62. @cython.boundscheck(False)
  63. @cython.cdivision(True)
  64. cdef long quot(long a , long b) noexcept nogil:
  65. cdef long x
  66. x = a/b
  67. if (a < 0):
  68. x -= (a % b != 0)
  69. return x
  70. @cython.wraparound(False)
  71. @cython.boundscheck(False)
  72. @cython.cdivision(True)
  73. cdef int dayofweek(int y, int m, int d) noexcept nogil:
  74. """
  75. Find the day of week for the date described by the Y/M/D triple y, m, d
  76. using Gauss' method, from wikipedia.
  77. 0 represents Monday. See [1]_.
  78. Parameters
  79. ----------
  80. y : int
  81. m : int
  82. d : int
  83. Returns
  84. -------
  85. weekday : int
  86. Notes
  87. -----
  88. Assumes that y, m, d, represents a valid date.
  89. See Also
  90. --------
  91. [1] https://docs.python.org/3/library/calendar.html#calendar.weekday
  92. [2] https://en.wikipedia.org/wiki/\
  93. Determination_of_the_day_of_the_week#Gauss's_algorithm
  94. """
  95. # Note: this particular implementation comes from
  96. # http://berndt-schwerdtfeger.de/wp-content/uploads/pdf/cal.pdf
  97. cdef:
  98. long c
  99. int g
  100. int f
  101. int e
  102. if (m < 3):
  103. y -= 1
  104. c = quot(y, 100)
  105. g = y - c * 100
  106. f = 5 * (c - quot(c, 4) * 4)
  107. e = em[m]
  108. if (m > 2):
  109. e -= 1
  110. return (-1 + d + e + f + g + g/4) % 7
  111. cdef bint is_leapyear(int64_t year) nogil:
  112. """
  113. Returns 1 if the given year is a leap year, 0 otherwise.
  114. Parameters
  115. ----------
  116. year : int
  117. Returns
  118. -------
  119. is_leap : bool
  120. """
  121. return ((year & 0x3) == 0 and # year % 4 == 0
  122. ((year % 100) != 0 or (year % 400) == 0))
  123. @cython.wraparound(False)
  124. @cython.boundscheck(False)
  125. cpdef int32_t get_week_of_year(int year, int month, int day) nogil:
  126. """
  127. Return the ordinal week-of-year for the given day.
  128. Parameters
  129. ----------
  130. year : int
  131. month : int
  132. day : int
  133. Returns
  134. -------
  135. week_of_year : int32_t
  136. Notes
  137. -----
  138. Assumes the inputs describe a valid date.
  139. """
  140. return get_iso_calendar(year, month, day)[1]
  141. @cython.wraparound(False)
  142. @cython.boundscheck(False)
  143. cpdef iso_calendar_t get_iso_calendar(int year, int month, int day) nogil:
  144. """
  145. Return the year, week, and day of year corresponding to ISO 8601
  146. Parameters
  147. ----------
  148. year : int
  149. month : int
  150. day : int
  151. Returns
  152. -------
  153. year : int32_t
  154. week : int32_t
  155. day : int32_t
  156. Notes
  157. -----
  158. Assumes the inputs describe a valid date.
  159. """
  160. cdef:
  161. int32_t doy, dow
  162. int32_t iso_year, iso_week
  163. doy = get_day_of_year(year, month, day)
  164. dow = dayofweek(year, month, day)
  165. # estimate
  166. iso_week = (doy - 1) - dow + 3
  167. if iso_week >= 0:
  168. iso_week = iso_week // 7 + 1
  169. # verify
  170. if iso_week < 0:
  171. if (iso_week > -2) or (iso_week == -2 and is_leapyear(year - 1)):
  172. iso_week = 53
  173. else:
  174. iso_week = 52
  175. elif iso_week == 53:
  176. if 31 - day + dow < 3:
  177. iso_week = 1
  178. iso_year = year
  179. if iso_week == 1 and month == 12:
  180. iso_year += 1
  181. elif iso_week >= 52 and month == 1:
  182. iso_year -= 1
  183. return iso_year, iso_week, dow + 1
  184. @cython.wraparound(False)
  185. @cython.boundscheck(False)
  186. cpdef int32_t get_day_of_year(int year, int month, int day) nogil:
  187. """
  188. Return the ordinal day-of-year for the given day.
  189. Parameters
  190. ----------
  191. year : int
  192. month : int
  193. day : int
  194. Returns
  195. -------
  196. day_of_year : int32_t
  197. Notes
  198. -----
  199. Assumes the inputs describe a valid date.
  200. """
  201. cdef:
  202. bint isleap
  203. int32_t mo_off
  204. int day_of_year
  205. isleap = is_leapyear(year)
  206. mo_off = month_offset[isleap * 13 + month - 1]
  207. day_of_year = mo_off + day
  208. return day_of_year
  209. # ---------------------------------------------------------------------
  210. # Business Helpers
  211. cpdef int get_lastbday(int year, int month) nogil:
  212. """
  213. Find the last day of the month that is a business day.
  214. Parameters
  215. ----------
  216. year : int
  217. month : int
  218. Returns
  219. -------
  220. last_bday : int
  221. """
  222. cdef:
  223. int wkday, days_in_month
  224. wkday = dayofweek(year, month, 1)
  225. days_in_month = get_days_in_month(year, month)
  226. return days_in_month - max(((wkday + days_in_month - 1) % 7) - 4, 0)
  227. cpdef int get_firstbday(int year, int month) nogil:
  228. """
  229. Find the first day of the month that is a business day.
  230. Parameters
  231. ----------
  232. year : int
  233. month : int
  234. Returns
  235. -------
  236. first_bday : int
  237. """
  238. cdef:
  239. int first, wkday
  240. wkday = dayofweek(year, month, 1)
  241. first = 1
  242. if wkday == 5: # on Saturday
  243. first = 3
  244. elif wkday == 6: # on Sunday
  245. first = 2
  246. return first