Skip to content

Commit f7dbfbb

Browse files
committed
Singapore holidays
1 parent a331982 commit f7dbfbb

File tree

4 files changed

+429
-8
lines changed

4 files changed

+429
-8
lines changed

.travis.yml

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,45 @@
11
# https://travis-ci.org/dr-prodigy/python-holidays
2-
dist: xenial
2+
dist: bionic
33
language: python
44

55
python:
6-
- 2.7
7-
- 3.4
8-
- 3.5
9-
- 3.6
10-
- 3.7
6+
- '3.8'
7+
- '3.7'
8+
- '3.6'
9+
- '3.5'
10+
- '2.7'
1111
# temporarily commented due to TravisCI issues
1212
# - pypy
1313
# - pypy3
1414

15+
jobs:
16+
# hijri-converter not available < python 3.6
17+
include:
18+
- python: '3.8'
19+
install:
20+
- pip install hijri-converter
21+
- pip install python-dateutil
22+
- python setup.py install
23+
- pip install flake8
24+
- pip install coveralls
25+
- python: '3.7'
26+
install:
27+
- pip install hijri-converter
28+
- pip install python-dateutil
29+
- python setup.py install
30+
- pip install flake8
31+
- pip install coveralls
32+
- python: '3.6'
33+
install:
34+
- pip install hijri-converter
35+
- pip install python-dateutil
36+
- python setup.py install
37+
- pip install flake8
38+
- pip install coveralls
39+
1540
env:
16-
- DATEUTIL=python-dateutil==2.7.0
1741
- DATEUTIL=python-dateutil
42+
- DATEUTIL=python-dateutil==2.7.0
1843

1944
install:
2045
- pip install $DATEUTIL

README.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ Portugal PT None
147147
PortugalExt PTE *Portugal plus extended days most people have off*
148148
Russia RU None
149149
Scotland None
150+
Singapore SG None
150151
Slovakia SK None
151152
Slovenia SI None
152153
South Africa ZA None

holidays.py

Lines changed: 335 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4954,3 +4954,338 @@ def _populate(self, year):
49544954

49554955
class PE(Peru):
49564956
pass
4957+
4958+
4959+
class Singapore(HolidayBase):
4960+
4961+
# Holidays Act: https://sso.agc.gov.sg/Act/HA1998
4962+
# https://www.mom.gov.sg/employment-practices/public-holidays
4963+
# https://en.wikipedia.org/wiki/Public_holidays_in_Singapore
4964+
4965+
# Holidays prior to 1969 (Act 24 of 1968—Holidays (Amendment) Act 1968)
4966+
# are estimated.
4967+
4968+
# Holidays prior to 2000 may not be accurate.
4969+
4970+
# Holidays after 2020: the following four annual scheduled but moving date
4971+
# holidays (announced yearly) are estimated:
4972+
# - Hari Raya Puasa*
4973+
# - Hari Raya Haji*
4974+
# - Vesak Day
4975+
# - Deepavali
4976+
# *only if hijri-converter library is installed, otherwise a warning is
4977+
# raised that this holiday is missing. hjiri-converter requires
4978+
# Python >= 3.6
4979+
4980+
def __init__(self, **kwargs):
4981+
self.country = "SG"
4982+
HolidayBase.__init__(self, **kwargs)
4983+
4984+
def _populate(self, year):
4985+
if year < 1969:
4986+
return
4987+
4988+
def storeholiday(self, hol_date, hol_name):
4989+
"""
4990+
Function to store the holiday name in the appropriate
4991+
date and to implement Section 4(2) of the Holidays Act:
4992+
'if any day specified in the Schedule falls on a Sunday,
4993+
the day next following not being itself a public holiday
4994+
is declared a public holiday in Singapore.'
4995+
"""
4996+
if hol_date.weekday() == SUN:
4997+
self[hol_date] = hol_name + " [Sunday]"
4998+
self[hol_date + rd(days=+1)] = "Monday following " + hol_name
4999+
else:
5000+
self[hol_date] = hol_name
5001+
5002+
# New Year's Day
5003+
storeholiday(self, date(year, JAN, 1), "New Year's Day")
5004+
5005+
# Chinese New Year (two days)
5006+
hol_date = self.get_lunar_n_y_date(year)
5007+
self[hol_date] = "Chinese New Year"
5008+
storeholiday(self, hol_date + rd(days=+1), "Chinese New Year")
5009+
5010+
# Hari Raya Puasa
5011+
# aka Eid al-Fitr
5012+
# date of observance is announced yearly
5013+
dates_obs = {2001: [(DEC, 16)], 2002: [(DEC, 6)],
5014+
2003: [(NOV, 25)], 2004: [(NOV, 14)], 2005: [(NOV, 3)],
5015+
2006: [(OCT, 24)], 2007: [(OCT, 13)], 2008: [(OCT, 1)],
5016+
2009: [(SEP, 20)], 2010: [(SEP, 10)], 2011: [(AUG, 30)],
5017+
2012: [(AUG, 19)], 2013: [(AUG, 8)], 2014: [(JUL, 28)],
5018+
2015: [(JUL, 17)], 2016: [(JUL, 6)], 2017: [(JUN, 25)],
5019+
2018: [(JUN, 15)], 2019: [(JUN, 5)], 2020: [(MAY, 24)]}
5020+
if year in dates_obs:
5021+
for date_obs in dates_obs[year]:
5022+
hol_date = date(year, *date_obs)
5023+
storeholiday(self, hol_date, "Hari Raya Puasa")
5024+
# Second day of Hari Raya Puasa (up to and including 1968)
5025+
if year <= 1968:
5026+
storeholiday(self, hol_date + rd(days=+1),
5027+
"Second day of Hari Raya Puasa")
5028+
else:
5029+
for date_obs in self.get_hrp_date(year):
5030+
hol_date = date_obs
5031+
storeholiday(self, hol_date,
5032+
"Hari Raya Puasa* (*estimated)")
5033+
# Second day of Hari Raya Puasa (up to and including 1968)
5034+
if year <= 1968:
5035+
storeholiday(self, hol_date + rd(days=+1),
5036+
"Second day of Hari Raya Puasa* (*estimated)")
5037+
5038+
# Hari Raya Haji
5039+
# aka Eid al-Adha
5040+
# date of observance is announced yearly
5041+
dates_obs = {2001: [(MAR, 6)], 2002: [(FEB, 23)],
5042+
2003: [(FEB, 12)], 2004: [(FEB, 1)], 2005: [(JAN, 21)],
5043+
2006: [(JAN, 10)], 2007: [(DEC, 20)], 2008: [(DEC, 8)],
5044+
2009: [(NOV, 27)], 2010: [(NOV, 17)], 2011: [(NOV, 6)],
5045+
2012: [(OCT, 26)], 2013: [(OCT, 15)], 2014: [(OCT, 5)],
5046+
2015: [(SEP, 24)], 2016: [(SEP, 12)], 2017: [(SEP, 1)],
5047+
2018: [(AUG, 22)], 2019: [(AUG, 11)], 2020: [(JUL, 31)]}
5048+
if year in dates_obs:
5049+
for date_obs in dates_obs[year]:
5050+
hol_date = date(year, *date_obs)
5051+
storeholiday(self, hol_date,
5052+
"Hari Raya Haji")
5053+
else:
5054+
for date_obs in self.get_hrh_date(year):
5055+
hol_date = date_obs
5056+
storeholiday(self, hol_date,
5057+
"Hari Raya Haji* (*estimated)")
5058+
5059+
# Holy Saturday (up to and including 1968)
5060+
if year <= 1968:
5061+
self[easter(year) + rd(weekday=SA(-1))] = "Holy Saturday"
5062+
5063+
# Good Friday
5064+
self[easter(year) + rd(weekday=FR(-1))] = "Good Friday"
5065+
5066+
# Easter Monday
5067+
if year <= 1968:
5068+
self[easter(year) + rd(weekday=MO(1))] = "Easter Monday"
5069+
5070+
# Labour Day
5071+
storeholiday(self, date(year, MAY, 1), "Labour Day")
5072+
5073+
# Vesak Day
5074+
# date of observance is announced yearly
5075+
# https://en.wikipedia.org/wiki/Vesak#Dates_of_observance
5076+
dates_obs = {2001: (MAY, 7), 2002: (MAY, 27),
5077+
2003: (MAY, 15), 2004: (JUN, 2), 2005: (MAY, 23),
5078+
2006: (MAY, 12), 2007: (MAY, 31), 2008: (MAY, 19),
5079+
2009: (MAY, 9), 2010: (MAY, 28), 2011: (MAY, 17),
5080+
2012: (MAY, 5), 2013: (MAY, 24), 2014: (MAY, 13),
5081+
2015: (JUN, 1), 2016: (MAY, 20), 2017: (MAY, 10),
5082+
2018: (MAY, 29), 2019: (MAY, 19), 2020: (MAY, 7)}
5083+
if year in dates_obs:
5084+
hol_date = date(year, *dates_obs[year])
5085+
storeholiday(self, hol_date, "Vesak Day")
5086+
else:
5087+
storeholiday(self, self.get_vesak_date(year),
5088+
"Vesak Day* (*estimated; ~10% chance +/- 1 day)")
5089+
5090+
# National Day
5091+
storeholiday(self, date(year, AUG, 9), "National Day")
5092+
5093+
# Deepavali
5094+
# aka Diwali
5095+
# date of observance is announced yearly
5096+
dates_obs = {2001: (NOV, 14), 2002: (NOV, 3),
5097+
2003: (OCT, 23), 2004: (NOV, 11), 2005: (NOV, 1),
5098+
2006: (OCT, 21), 2007: (NOV, 8), 2008: (OCT, 27),
5099+
2009: (OCT, 17), 2010: (NOV, 5), 2011: (OCT, 26),
5100+
2012: (NOV, 13), 2013: (NOV, 2), 2014: (OCT, 22),
5101+
2015: (NOV, 10), 2016: (OCT, 29), 2017: (OCT, 18),
5102+
2018: (NOV, 6), 2019: (OCT, 27), 2020: (NOV, 14)}
5103+
if year in dates_obs:
5104+
hol_date = date(year, *dates_obs[year])
5105+
storeholiday(self, hol_date, "Deepavali")
5106+
else:
5107+
storeholiday(self, self.get_s_diwali_date(year),
5108+
"Deepavali* (*estimated; rarely on day after)")
5109+
5110+
# Christmas Day
5111+
storeholiday(self, date(year, DEC, 25), "Christmas Day")
5112+
5113+
# Boxing day (up to and including 1968)
5114+
if year <= 1968:
5115+
storeholiday(self, date(year, DEC, 26), "Boxing Day")
5116+
5117+
# Polling Day
5118+
dates_obs = {2001: (NOV, 3), 2006: (MAY, 6),
5119+
2011: (MAY, 7), 2015: (SEP, 11)}
5120+
if year in dates_obs:
5121+
self[date(year, *dates_obs[year])] = "Polling Day"
5122+
5123+
# SG50 Public holiday
5124+
# Announced on 14 March 2015
5125+
# https://www.mom.gov.sg/newsroom/press-releases/2015/sg50-public-holiday-on-7-august-2015
5126+
if year == 2015:
5127+
self[date(2015, AUG, 7)] = "SG50 Public Holiday"
5128+
5129+
# The below is used to calcluate lunar new year (i.e. Chinese new year)
5130+
# Code borrowed from Hong Kong entry as of 16-Nov-19
5131+
# Should probably be a function available to multiple countries
5132+
5133+
# Store the number of days per year from 1901 to 2099, and the number of
5134+
# days from the 1st to the 13th to store the monthly (including the month
5135+
# of the month), 1 means that the month is 30 days. 0 means the month is
5136+
# 29 days. The 12th to 15th digits indicate the month of the next month.
5137+
# If it is 0x0F, it means that there is no leap month.
5138+
g_lunar_month_days = [
5139+
0xF0EA4, 0xF1D4A, 0x52C94, 0xF0C96, 0xF1536,
5140+
0x42AAC, 0xF0AD4, 0xF16B2, 0x22EA4, 0xF0EA4, # 1901-1910
5141+
0x6364A, 0xF164A, 0xF1496, 0x52956, 0xF055A,
5142+
0xF0AD6, 0x216D2, 0xF1B52, 0x73B24, 0xF1D24, # 1911-1920
5143+
0xF1A4A, 0x5349A, 0xF14AC, 0xF056C, 0x42B6A,
5144+
0xF0DA8, 0xF1D52, 0x23D24, 0xF1D24, 0x61A4C, # 1921-1930
5145+
0xF0A56, 0xF14AE, 0x5256C, 0xF16B4, 0xF0DA8,
5146+
0x31D92, 0xF0E92, 0x72D26, 0xF1526, 0xF0A56, # 1931-1940
5147+
0x614B6, 0xF155A, 0xF0AD4, 0x436AA, 0xF1748,
5148+
0xF1692, 0x23526, 0xF152A, 0x72A5A, 0xF0A6C, # 1941-1950
5149+
0xF155A, 0x52B54, 0xF0B64, 0xF1B4A, 0x33A94,
5150+
0xF1A94, 0x8152A, 0xF152E, 0xF0AAC, 0x6156A, # 1951-1960
5151+
0xF15AA, 0xF0DA4, 0x41D4A, 0xF1D4A, 0xF0C94,
5152+
0x3192E, 0xF1536, 0x72AB4, 0xF0AD4, 0xF16D2, # 1961-1970
5153+
0x52EA4, 0xF16A4, 0xF164A, 0x42C96, 0xF1496,
5154+
0x82956, 0xF055A, 0xF0ADA, 0x616D2, 0xF1B52, # 1971-1980
5155+
0xF1B24, 0x43A4A, 0xF1A4A, 0xA349A, 0xF14AC,
5156+
0xF056C, 0x60B6A, 0xF0DAA, 0xF1D92, 0x53D24, # 1981-1990
5157+
0xF1D24, 0xF1A4C, 0x314AC, 0xF14AE, 0x829AC,
5158+
0xF06B4, 0xF0DAA, 0x52D92, 0xF0E92, 0xF0D26, # 1991-2000
5159+
0x42A56, 0xF0A56, 0xF14B6, 0x22AB4, 0xF0AD4,
5160+
0x736AA, 0xF1748, 0xF1692, 0x53526, 0xF152A, # 2001-2010
5161+
0xF0A5A, 0x4155A, 0xF156A, 0x92B54, 0xF0BA4,
5162+
0xF1B4A, 0x63A94, 0xF1A94, 0xF192A, 0x42A5C, # 2011-2020
5163+
0xF0AAC, 0xF156A, 0x22B64, 0xF0DA4, 0x61D52,
5164+
0xF0E4A, 0xF0C96, 0x5192E, 0xF1956, 0xF0AB4, # 2021-2030
5165+
0x315AC, 0xF16D2, 0xB2EA4, 0xF16A4, 0xF164A,
5166+
0x63496, 0xF1496, 0xF0956, 0x50AB6, 0xF0B5A, # 2031-2040
5167+
0xF16D4, 0x236A4, 0xF1B24, 0x73A4A, 0xF1A4A,
5168+
0xF14AA, 0x5295A, 0xF096C, 0xF0B6A, 0x31B54, # 2041-2050
5169+
0xF1D92, 0x83D24, 0xF1D24, 0xF1A4C, 0x614AC,
5170+
0xF14AE, 0xF09AC, 0x40DAA, 0xF0EAA, 0xF0E92, # 2051-2060
5171+
0x31D26, 0xF0D26, 0x72A56, 0xF0A56, 0xF14B6,
5172+
0x52AB4, 0xF0AD4, 0xF16CA, 0x42E94, 0xF1694, # 2061-2070
5173+
0x8352A, 0xF152A, 0xF0A5A, 0x6155A, 0xF156A,
5174+
0xF0B54, 0x4174A, 0xF1B4A, 0xF1A94, 0x3392A, # 2071-2080
5175+
0xF192C, 0x7329C, 0xF0AAC, 0xF156A, 0x52B64,
5176+
0xF0DA4, 0xF1D4A, 0x41C94, 0xF0C96, 0x8192E, # 2081-2090
5177+
0xF0956, 0xF0AB6, 0x615AC, 0xF16D4, 0xF0EA4,
5178+
0x42E4A, 0xF164A, 0xF1516, 0x22936, # 2090-2099
5179+
]
5180+
# Define range of years
5181+
START_YEAR, END_YEAR = 1901, 1900 + len(g_lunar_month_days)
5182+
# 1901 The 1st day of the 1st month of the Gregorian calendar is 1901/2/19
5183+
LUNAR_START_DATE, SOLAR_START_DATE = (1901, 1, 1), date(1901, 2, 19)
5184+
# The Gregorian date for December 30, 2099 is 2100/2/8
5185+
LUNAR_END_DATE, SOLAR_END_DATE = (2099, 12, 30), date(2100, 2, 18)
5186+
5187+
def get_leap_month(self, lunar_year):
5188+
return (self.g_lunar_month_days[lunar_year-self.START_YEAR] >> 16) \
5189+
& 0x0F
5190+
5191+
def lunar_month_days(self, lunar_year, lunar_month):
5192+
return 29 + ((self.g_lunar_month_days[lunar_year-self.START_YEAR] >>
5193+
lunar_month) & 0x01)
5194+
5195+
def lunar_year_days(self, year):
5196+
days = 0
5197+
months_day = self.g_lunar_month_days[year - self.START_YEAR]
5198+
for i in range(1, 13 if self.get_leap_month(year) == 0x0F else 14):
5199+
day = 29 + ((months_day >> i) & 0x01)
5200+
days += day
5201+
return days
5202+
5203+
# Calculate Gregorian date of lunar new year
5204+
def get_lunar_n_y_date(self, year):
5205+
span_days = 0
5206+
for y in range(self.START_YEAR, year):
5207+
span_days += self.lunar_year_days(y)
5208+
leap_month = self.get_leap_month(year)
5209+
for m in range(1, 1 + (1 > leap_month)):
5210+
span_days += self.lunar_month_days(year, m)
5211+
return self.SOLAR_START_DATE + timedelta(span_days)
5212+
5213+
# Estimate Gregorian date of Vesak
5214+
def get_vesak_date(self, year):
5215+
span_days = 0
5216+
for y in range(self.START_YEAR, year):
5217+
span_days += self.lunar_year_days(y)
5218+
leap_month = self.get_leap_month(year)
5219+
for m in range(1, 4 + (4 > leap_month)):
5220+
span_days += self.lunar_month_days(year, m)
5221+
span_days += 14
5222+
return (self.SOLAR_START_DATE + timedelta(span_days))
5223+
5224+
# Estimate Gregorian date of Southern India Diwali
5225+
def get_s_diwali_date(self, year):
5226+
span_days = 0
5227+
for y in range(self.START_YEAR, year):
5228+
span_days += self.lunar_year_days(y)
5229+
leap_month = self.get_leap_month(year)
5230+
for m in range(1, 10 + (10 > leap_month)):
5231+
span_days += self.lunar_month_days(year, m)
5232+
span_days -= 2
5233+
return (self.SOLAR_START_DATE + timedelta(span_days))
5234+
5235+
# Estimate Gregorian date(s) of Hara Rasa Puasa
5236+
def get_hrp_date(self, year):
5237+
try:
5238+
from hijri_converter import convert
5239+
except ImportError:
5240+
import warnings
5241+
5242+
def warning_on_one_line(message, category, filename,
5243+
lineno, file=None, line=None):
5244+
return filename + ': ' + str(message) + '\n'
5245+
warnings.formatwarning = warning_on_one_line
5246+
warnings.warn("Hari Raja Puasa is missing." +
5247+
"To estimate, install hijri-converter library")
5248+
warnings.warn("pip install -U hijri-converter")
5249+
warnings.warn("(see https://hijri-converter.readthedocs.io/ )")
5250+
return []
5251+
Hyear = convert.Gregorian(year, 1, 1).to_hijri().datetuple()[0]
5252+
hrps = []
5253+
hrps.append(convert.Hijri(Hyear - 1, 10, 1).to_gregorian())
5254+
hrps.append(convert.Hijri(Hyear, 10, 1).to_gregorian())
5255+
hrps.append(convert.Hijri(Hyear + 1, 10, 1).to_gregorian())
5256+
hrp_dates = []
5257+
for hrp in hrps:
5258+
if hrp.year == year:
5259+
hrp_dates.append(date(*hrp.datetuple()))
5260+
return hrp_dates
5261+
5262+
# Estimate Gregorian date(s) of Hara Rasa Haji
5263+
def get_hrh_date(self, year):
5264+
try:
5265+
from hijri_converter import convert
5266+
except ImportError:
5267+
import warnings
5268+
5269+
def warning_on_one_line(message, category, filename, lineno,
5270+
file=None, line=None):
5271+
return filename + ': ' + str(message) + '\n'
5272+
warnings.formatwarning = warning_on_one_line
5273+
warnings.warn("Hari Raja Haji is missing." +
5274+
"To estimate, install hijri-converter library")
5275+
warnings.warn("pip install -U hijri-converter")
5276+
warnings.warn("(see https://hijri-converter.readthedocs.io/ )")
5277+
return []
5278+
Hyear = convert.Gregorian(year, 1, 1).to_hijri().datetuple()[0]
5279+
hrhs = []
5280+
hrhs.append(convert.Hijri(Hyear - 1, 12, 10).to_gregorian())
5281+
hrhs.append(convert.Hijri(Hyear, 12, 10).to_gregorian())
5282+
hrhs.append(convert.Hijri(Hyear + 1, 12, 10).to_gregorian())
5283+
hrh_dates = []
5284+
for hrh in hrhs:
5285+
if hrh.year == year:
5286+
hrh_dates.append(date(*hrh.datetuple()))
5287+
return hrh_dates
5288+
5289+
5290+
class SG(Singapore):
5291+
pass

0 commit comments

Comments
 (0)