Skip to content

Commit 7ca9e61

Browse files
nathanielmiller23lauft
authored andcommitted
Add country Singapore (SG)
Add country Singapore with its holidays as laid out in the [1998 Holidays Act](https://sso.agc.gov.sg/Act/HA1998). This takes over the work done by @nathanielmiller23 from #108 and adapts it to the recent changes. Signed-off-by: Thomas Lauf <thomas.lauf@tngtech.com>
1 parent 799d9dc commit 7ca9e61

19 files changed

Lines changed: 2085 additions & 0 deletions

ChangeLog

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
- #143 SG: Add country Singapore (locale 'en-SG')
2+
(thanks to nathanielmiller23)
13
- #141 AU: Fix dates for 'Australia Day' and 'Anzac Day'
24

35
2025.12.0
Lines changed: 338 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,338 @@
1+
from dateutil.easter import EASTER_WESTERN
2+
3+
from holidata.holiday import Country
4+
from holidata.utils import day, date, first
5+
6+
__all__ = [
7+
"SG",
8+
]
9+
10+
"""
11+
Legal sources:
12+
- Holidays Act 1998 https://sso.agc.gov.sg/Act/HA1998
13+
- Parliamentary Elections Act 1954 https://sso.agc.gov.sg/Act/PEA1954
14+
- Presidential Elections Act 1991 https://sso.agc.gov.sg/Act/PrEA1991
15+
16+
- https://www.mom.gov.sg/employment-practices/public-holidays
17+
- https://www.mom.gov.sg/employment-practices/public-holidays-entitlement-and-pay
18+
"""
19+
20+
21+
class SG(Country):
22+
id = "SG"
23+
languages = ["en"]
24+
default_lang = "en"
25+
regions = []
26+
easter_type = EASTER_WESTERN
27+
28+
def __init__(self):
29+
super().__init__()
30+
31+
self.define_holiday() \
32+
.with_name("New Year's Day") \
33+
.on(date(month=1, day=1)) \
34+
.with_flags("NF") \
35+
36+
self.define_holiday() \
37+
.with_name("New Year's Day (in lieu)") \
38+
.on(first("monday").after(date(month=1, day=1))) \
39+
.with_flags("NF") \
40+
.on_condition(date(month=1, day=1).is_a("sunday"))
41+
42+
self.define_holiday() \
43+
.with_name("Chinese New Year") \
44+
.on(SG.chinese_new_year) \
45+
.with_flags("NRV")
46+
47+
self.define_holiday() \
48+
.with_name("Chinese New Year (Second Day)") \
49+
.on(SG.chinese_new_year_day2) \
50+
.with_flags("NRV")
51+
52+
self.define_holiday() \
53+
.with_name("Chinese New Year (in lieu)") \
54+
.on(SG.chinese_new_year_in_lieu) \
55+
.with_flags("NRV")
56+
57+
self.define_holiday() \
58+
.with_name("Good Friday") \
59+
.on(day(2).before(self.easter())) \
60+
.with_flags("NRV")
61+
62+
self.define_holiday() \
63+
.with_name("Labour Day") \
64+
.on(date(month=5, day=1)) \
65+
.with_flags("NF") \
66+
67+
self.define_holiday() \
68+
.with_name("Labour Day (in lieu)") \
69+
.on(first("monday").after(date(month=5, day=1))) \
70+
.with_flags("NF") \
71+
.on_condition(date(month=5, day=1).is_a("sunday"))
72+
73+
self.define_holiday() \
74+
.with_name("Hari Raya Puasa") \
75+
.on(SG.hari_raya_puasa) \
76+
.with_flags("NRV")
77+
78+
self.define_holiday() \
79+
.with_name("Hari Raya Puasa (in lieu)") \
80+
.on(SG.hari_raya_puasa_in_lieu) \
81+
.with_flags("NRV")
82+
83+
self.define_holiday() \
84+
.with_name("Vesak Day") \
85+
.on(SG.vesak_day) \
86+
.with_flags("NRV")
87+
88+
self.define_holiday() \
89+
.with_name("Vesak Day (in lieu)") \
90+
.on(SG.vesak_day_in_lieu) \
91+
.with_flags("NRV")
92+
93+
self.define_holiday() \
94+
.with_name("Hari Raya Haji") \
95+
.on(SG.hari_raya_haji) \
96+
.with_flags("NRV")
97+
98+
self.define_holiday() \
99+
.with_name("Hari Raya Haji (in lieu)") \
100+
.on(SG.hari_raya_haji_in_lieu) \
101+
.with_flags("NRV")
102+
103+
self.define_holiday() \
104+
.with_name("National Day") \
105+
.on(date(month=8, day=9)) \
106+
.with_flags("NF") \
107+
108+
self.define_holiday() \
109+
.with_name("National Day (in lieu)") \
110+
.on(first("monday").after(date(month=8, day=9))) \
111+
.with_flags("NF") \
112+
.on_condition(date(month=8, day=9).is_a("sunday"))
113+
114+
self.define_holiday() \
115+
.with_name("Deepavali") \
116+
.on(SG.deepavali) \
117+
.with_flags("NRV")
118+
119+
self.define_holiday() \
120+
.with_name("Deepavali (in lieu)") \
121+
.on(SG.deepavali_in_lieu) \
122+
.with_flags("NRV")
123+
124+
self.define_holiday() \
125+
.with_name("Christmas Day") \
126+
.on(date(month=12, day=25)) \
127+
.with_flags("NRF") \
128+
129+
self.define_holiday() \
130+
.with_name("Christmas Day (in lieu)") \
131+
.on(first("monday").after(date(month=12, day=25))) \
132+
.with_flags("NRF") \
133+
.on_condition(date(month=12, day=25).is_a("sunday"))
134+
135+
"""
136+
General Election day
137+
Holiday according to Section 35 of the Parliamentary Elections Act
138+
"""
139+
self.define_holiday() \
140+
.with_name("General Election") \
141+
.on(SG.date_of_general_election) \
142+
.with_flags("NF")
143+
144+
"""
145+
Presidential Election day
146+
Holiday according to Section 17 of the Presidential Elections Act
147+
"""
148+
self.define_holiday() \
149+
.with_name("Presidential Election") \
150+
.on(SG.date_of_presidential_election) \
151+
.with_flags("NF")
152+
153+
"""
154+
SG50 Public holiday
155+
Celebrating 50 years of independence from Malaysia
156+
"""
157+
self.define_holiday() \
158+
.with_name("SG50 Public Holiday") \
159+
.on(date(month=8, day=7)) \
160+
.in_years([2015]) \
161+
.with_flags("NF")
162+
163+
@staticmethod
164+
def date_of_general_election(year):
165+
dates = {
166+
2011: date(month=5, day=7),
167+
2015: date(month=9, day=11),
168+
2020: date(month=7, day=10),
169+
2025: date(month=5, day=3),
170+
}
171+
return dates.get(year)(year) if year in dates else None
172+
173+
@staticmethod
174+
def date_of_presidential_election(year):
175+
dates = {
176+
2011: date(month=8, day=27),
177+
2017: date(month=9, day=13),
178+
2023: date(month=9, day=1),
179+
}
180+
return dates.get(year)(year) if year in dates else None
181+
182+
@staticmethod
183+
def chinese_new_year(year):
184+
"""Return the date of Chinese New Year for the given year."""
185+
dates = {
186+
2011: date(2, 3),
187+
2012: date(1, 23),
188+
2013: date(2, 10),
189+
2014: date(1, 31),
190+
2015: date(2, 19),
191+
2016: date(2, 8),
192+
2017: date(1, 28),
193+
2018: date(2, 16),
194+
2019: date(2, 5),
195+
2020: date(1, 25),
196+
2021: date(2, 12),
197+
2022: date(2, 1),
198+
2023: date(1, 22),
199+
2024: date(2, 10),
200+
2025: date(1, 29),
201+
2026: date(2, 17)
202+
}
203+
return dates.get(year)(year) if year in dates else None
204+
205+
@staticmethod
206+
def chinese_new_year_day2(year):
207+
"""Return the date of the second day of Chinese New Year."""
208+
first_day = SG.chinese_new_year(year)
209+
return first_day.shift(days=1) if first_day is not None else None
210+
211+
@staticmethod
212+
def chinese_new_year_in_lieu(year):
213+
holiday_date = SG.chinese_new_year(year)
214+
if holiday_date is not None and holiday_date.weekday() == "saturday":
215+
return holiday_date.shift_to_weekday("monday")
216+
elif holiday_date is not None and holiday_date.weekday() == "sunday":
217+
return holiday_date.shift_to_weekday("tuesday")
218+
return None
219+
220+
@staticmethod
221+
def hari_raya_puasa(year):
222+
"""Return the date of Hari Raya Puasa for the given year."""
223+
dates = {
224+
2011: date(8, 30),
225+
2012: date(8, 19),
226+
2013: date(8, 8),
227+
2014: date(7, 28),
228+
2015: date(7, 17),
229+
2016: date(7, 6),
230+
2017: date(6, 25),
231+
2018: date(6, 15),
232+
2019: date(6, 5),
233+
2020: date(5, 24),
234+
2021: date(5, 13),
235+
2022: date(5, 3),
236+
2023: date(4, 22),
237+
2024: date(4, 10),
238+
2025: date(3, 31),
239+
2026: date(3, 21)
240+
}
241+
return dates.get(year)(year) if year in dates else None
242+
243+
@staticmethod
244+
def hari_raya_puasa_in_lieu(year):
245+
holiday_date = SG.hari_raya_puasa(year)
246+
if holiday_date is not None and holiday_date.weekday() == "sunday":
247+
return holiday_date.shift_to_weekday("monday")
248+
return None
249+
250+
@staticmethod
251+
def vesak_day(year):
252+
"""Return the date of Vesak Day for the given year."""
253+
dates = {
254+
2011: date(5, 17),
255+
2012: date(5, 5),
256+
2013: date(5, 24),
257+
2014: date(5, 13),
258+
2015: date(6, 1),
259+
2016: date(5, 21),
260+
2017: date(5, 10),
261+
2018: date(5, 29),
262+
2019: date(5, 19),
263+
2020: date(5, 7),
264+
2021: date(5, 26),
265+
2022: date(5, 15),
266+
2023: date(6, 2),
267+
2024: date(5, 22),
268+
2025: date(5, 12),
269+
2026: date(5, 31)
270+
}
271+
return dates.get(year)(year) if year in dates else None
272+
273+
@staticmethod
274+
def vesak_day_in_lieu(year):
275+
holiday_date = SG.vesak_day(year)
276+
if holiday_date is not None and holiday_date.weekday() == "sunday":
277+
return holiday_date.shift_to_weekday("monday")
278+
return None
279+
280+
@staticmethod
281+
def hari_raya_haji(year):
282+
"""Return the date of Hari Raya Haji for the given year."""
283+
dates = {
284+
2011: date(11, 6),
285+
2012: date(10, 26),
286+
2013: date(10, 15),
287+
2014: date(10, 5),
288+
2015: date(9, 24),
289+
2016: date(9, 12),
290+
2017: date(9, 1),
291+
2018: date(8, 22),
292+
2019: date(8, 11),
293+
2020: date(7, 31),
294+
2021: date(7, 20),
295+
2022: date(7, 10),
296+
2023: date(6, 29),
297+
2024: date(6, 17),
298+
2025: date(6, 7),
299+
2026: date(5, 27)
300+
}
301+
return dates.get(year)(year) if year in dates else None
302+
303+
@staticmethod
304+
def hari_raya_haji_in_lieu(year):
305+
holiday_date = SG.hari_raya_haji(year)
306+
if holiday_date is not None and holiday_date.weekday() == "sunday":
307+
return holiday_date.shift_to_weekday("monday")
308+
return None
309+
310+
@staticmethod
311+
def deepavali(year):
312+
"""Return the date of Deepavali for the given year."""
313+
dates = {
314+
2011: date(10, 26),
315+
2012: date(11, 13),
316+
2013: date(11, 2),
317+
2014: date(10, 22),
318+
2015: date(11, 10),
319+
2016: date(10, 29),
320+
2017: date(10, 18),
321+
2018: date(11, 6),
322+
2019: date(10, 27),
323+
2020: date(11, 14),
324+
2021: date(11, 4),
325+
2022: date(10, 24),
326+
2023: date(11, 12),
327+
2024: date(10, 31),
328+
2025: date(10, 20),
329+
2026: date(11, 8)
330+
}
331+
return dates.get(year)(year) if year in dates else None
332+
333+
@staticmethod
334+
def deepavali_in_lieu(year):
335+
holiday_date = SG.deepavali(year)
336+
if holiday_date is not None and holiday_date.weekday() == "sunday":
337+
return holiday_date.shift_to_weekday("monday")
338+
return None

src/holidata/holidays/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from .PT import *
2626
from .RU import *
2727
from .SE import *
28+
from .SG import *
2829
from .SI import *
2930
from .SK import *
3031
from .TR import *

0 commit comments

Comments
 (0)