A government holiday notice is one page of prose. Turning it into a table is ten minutes of typing. Making the table tell you something the notice does not print takes six assertions, and one of them caught me.

China's 2027 calendar came out on 5 November 2026 (State Council notice): seven holidays, 29 days off, five weekend days that become working days. The notice states each block's length and any make-up days. It never states the split, and the split is the part that matters if you are planning travel, staffing or a deadline.

What the page actually says

Seven numbered paragraphs, one per holiday. Each one gives a first date, a last date, the weekday of each end, the number of days, and any working days attached. That is the whole document, plus a closing sentence asking employers to combine the blocks with paid annual leave and to stagger travel.

Here is 2027 as printed, in a shape I can run checks against:

H = [ ("New Year", "2027-01-01", "2027-01-03", 3, []), ("Spring Festival", "2027-02-05", "2027-02-13", 9, ["2027-01-31", "2027-02-14"]), ("Qingming", "2027-04-03", "2027-04-05", 3, []), ("Labour Day", "2027-05-01", "2027-05-05", 5, ["2027-05-08"]), ("Dragon Boat","2027-06-09", "2027-06-09", 1, []), ("Mid-Autumn", "2027-09-15", "2027-09-15", 1, []), ("National Day","2027-10-01","2027-10-07", 7, ["2027-09-26", "2027-10-09"]), ] 

The first assertion is the boring one

Length has to equal the dates the same paragraph gives. (end - start).days + 1 == stated fails on a typo rather than producing a table with a wrong number in it.

The second assertion is about the weekday labels the notice prints next to each date. Those labels are redundant information, which makes them a free check: if my parse is off by a day, the computed weekday disagrees with the printed one and the run stops.

assert start.strftime("%a") == printed_start_weekday assert end.strftime("%a") == printed_end_weekday 

Where I got it wrong first

My first pass computed borrowed days as days_off - statutory_days. That gives 16. The real number is five.

Eleven of the missing days are ordinary weekend days that happen to fall inside a block. A nine-day Spring Festival is not nine days granted: four are statutory, three are weekend days, two are borrowed. Subtract only the statutory days and you count the weekends as if the government had handed them over.

The correction is a third column — count weekend days inside the block by weekday, then:

borrowed = days_off - statutory - weekend_days_inside 
Holiday Days off Statutory Weekend inside Borrowed
New Year 3 1 2 0
Spring Festival 9 4 3 2
Qingming 3 1 2 0
Labour Day 5 2 2 1
Dragon Boat 1 1 0 0
Mid-Autumn 1 1 0 0
National Day 7 3 2 2
Total 29 13 11 5

Two invariants fall out, and both are assertable: the borrowed column must equal the number of make-up dates the notice lists (five), and the four columns must sum to 29.

There is a wrinkle the arithmetic hides. A statutory day that lands on a Saturday or Sunday is made up on a working day, so the 11 weekend days inside blocks are not extra free days on top of the 13. Labour Day is the clean case: both statutory days fall on the Saturday and Sunday, and reappear as the Monday and Tuesday off. The table still balances, because the statutory days are the days the law names, not the days they end up being taken.

The assertion that produced the interesting number

The holiday measures cap the borrowing: outside exceptional cases, continuous work before or after a statutory holiday should not exceed six days (State Council order 795).

Walking every day of 2027 and marking each one working or not — weekends assumed off unless the notice names them — the longest run is exactly six: Sunday 14 February through Friday 19 February. The second longest is five, at the end of September, where a make-up Sunday runs into the working week before National Day.

That is a real constraint on the calendar, and it explains why the make-up days sit where they do. It also gives a way to check next year's notice in a few seconds: if a run comes out at seven, either the parse is wrong or the document says something unusual.

The year-on-year number that surprised me

Two festivals land on midweek dates in 2027 — Dragon Boat on Wednesday 9 June, Mid-Autumn on Wednesday 15 September — so each is one day with nothing borrowed. In 2026 both were three-day blocks.

Running the same script over the 2026 notice gives 33 days off and six make-up days. So 2027 is four days shorter and one worked weekend cheaper, and both changes come from where two lunar dates fell. Nothing about policy changed between the two documents.

# days off, make-up days print(totals(parse(NOTICE_2027))) # (29, 5) print(totals(parse(NOTICE_2026))) # (33, 6) 

Two things the table still cannot tell you

The first is local holidays. The measures let provincial governments in areas with concentrated ethnic-minority populations set their own days, and those never appear in the national notice. A national total is not a national total.

The second is pay. The labour law sets 300% for work on a statutory holiday and 200% for a rest day that cannot be compensated with time off, but the notice calls the five make-up dates working days, not rest days. Which tier applies to them is a contract-and-practice question, and I have not verified it, so the page says so instead of guessing.

Why bother

Because a calendar is a document people act on, and the parts that get quoted in news summaries are the parts that are easiest to copy wrong. "Nine days off for Spring Festival" is true and misleading at the same time. The version with four columns is not.

If you want the finished page — dates, the toll-free windows for each block, the half-day holidays that never make the calendar — it is here: China's 2027 public holidays.

Drafted with an AI assistant from my own reading of the primary documents; every clause number and link here was checked against the source.