[Tex/LaTex] Make a holiday calendar with automatic “school week numbering” in tikz

automationcalendarloopstikz-pgf

I am trying to make a two sided holiday calendar (DIN A4, two pages for one school year). For this I started with an example from Robert Krause and tried to modify it. Here is how it looks so far:

% DIN-A4 doublesided year calendar
% Author: Robert Krause
% License : Creative Commons attribution license
% Submitted to TeXample.net on 13 July 2018

% Modified by julia 2018

\documentclass[a4paper, ngerman, 10pt]{scrartcl}
\usepackage[utf8]{inputenc}
\usepackage[ngerman]{babel}
\usepackage[T1]{fontenc}
\usepackage{tikz,xparse}            % Use the calendar.sty style

\usepackage{translator} % German Month and Day names
\usepackage{fancyhdr}       % header and footer
\usepackage{fix-cm}     % Large year in header

\usepackage[ headheight = 0.8cm, hmargin=.5cm,
  top = 1.7cm, nofoot,bottom=0cm]{geometry}
\usetikzlibrary{calc}
\usetikzlibrary{calendar}
\renewcommand*\familydefault{\sfdefault}


\makeatletter
\long\def\ifnodedefined#1#2#3{%
    \@ifundefined{pgf@sh@ns@#1}{#3}{#2}%
}
\makeatother

% Names of Holidays are inserted by employing this macro
\def\termin#1#2{
  \ifnodedefined{cal-#1}{
  \node [anchor=north west, text width= 3.4cm] at
  ($(cal-#1.north west)+(3em, 0em)$) {\tiny{#2}};
  }{}
}


\newcounter{week}
\setcounter{week}{1}
\newcommand\woche[2]{
  \node [anchor=north east, align=right] at
    ($(#1.north east)+(0em, 0em)$) {\tiny{SW #2}.};}


\newcommand{\holidays}{
between=2018-10-29 and 2018-11-04,
between=2019-02-28 and 2019-02-28,
between=2019-03-01 and 2019-03-08,
between=2019-04-15 and 2019-04-26,
between=2019-06-10 and 2019-06-02,
between=2019-07-29 and 2019-09-01,
}

%Header
\renewcommand{\headrulewidth}{0.0pt}
\setlength{\headheight}{0.8cm}
\chead{
 \Huge 2018/2019
 \Large\textbf{Termine}\hfill
}
\cfoot{}

\newcommand{\kheight}{0.82}
\newcommand{\kwidth}{3.0}
\newcommand{\kshift}{3.4}
\newcommand{\calstartdate}{2018-09-10}

\newcommand{\kal}[2]{
\vspace*{-1cm}
\begin{tikzpicture}[every day/.style={anchor = north}]
\calendar[
  dates=#1,
  name=cal,
  day yshift = 3em,
  day code=
  {
    \node[name=\pgfcalendarsuggestedname,every day,shape=rectangle,
    minimum height= \kheight cm, text width = \kwidth cm, draw =
    gray]{\tikzdaytext \enskip
      \pgfcalendarweekdayshortname{\pgfcalendarcurrentweekday}};
    \ifdate{Monday}{          
    }{}
  },
  execute before day scope=
  {
    \ifdate{day of month=1}
    {
      % Shift right
      \pgftransformxshift{\kshift cm}
      % Print month name 
      \draw (0,0)node [shape=rectangle, minimum height= \kheight cm,
      text width = \kwidth cm, fill = red, text= white, draw = red, text centered]
      {\textbf{\pgfcalendarmonthname{\pgfcalendarcurrentmonth}}};
    }{}
    \ifdate{workday}
    {
      % normal days are white
      \tikzset{every day/.style={fill=white}}
      % Vacation (Germany, Baden-Wuerrtemberg) gray background
      \ifdate{#2}{%
        \tikzset{every day/.style={fill=gray!30}}
      }{}
    }{}
    % Saturdays and half holidays (Christma's and New year's eve)
    \ifdate{Saturday}{\tikzset{every day/.style={fill=red!10}}}{}
    % % Sundays and full holidays
    \ifdate{Sunday}{\tikzset{every day/.style={fill=red!20}}}{}
    %Tag der Arbeit
    \ifdate{equals=2018-10-03}{\tikzset{every
        day/.style={fill=red!20}}}{}
    % Christi Himmelfahrt
    \ifdate{equals=2019-05-30}{\tikzset{every
        day/.style={fill=red!20}}}{}
  },
 execute at begin day scope=
  {
    % each day is shifted down according to the day of month
    \pgftransformyshift{-\kheight*\pgfcalendarcurrentday cm}
  }
  ];
  % % Some Dates
  \termin{2018-10-03}{Tag der dt. Einheit}
  \termin{2019-01-01}{Neujahr}
  \termin{2019-01-06}{Heilige Drei Könige}
  \termin{2019-04-19}{Karfreitag}
  \termin{2019-04-21}{Ostersonntag}
  \termin{2019-04-22}{Ostermontag}
  \termin{2019-05-01}{Tag der Arbeit}
  \termin{2019-05-30}{Christi Himmelfahrt}
  \termin{2019-06-09}{Pfingstsonntag}
  \termin{2019-06-20}{Pfingstmontag}

\end{tikzpicture}
}

\begin{document}
\pagestyle{fancy}
\begin{center}
\kal{2018-09-10 to 2019-02-28}{
  between=2018-10-03 and 2018-10-05,
  between=2018-10-29 and 2018-11-04,
  between=2019-02-28 and 2019-02-28,
  between=2019-03-01 and 2019-03-08,
  between=2019-04-15 and 2019-04-26,
  between=2019-06-10 and 2019-06-02,
  between=2019-07-29 and 2019-09-01,
}
\kal{2019-03-01 to 2019-08-30}{
  between=2018-10-03 and 2018-10-05,
  between=2018-10-29 and 2018-11-04,
  between=2019-02-28 and 2019-02-28,
  between=2019-03-01 and 2019-03-08,
  between=2019-04-15 and 2019-04-26,
  between=2019-06-10 and 2019-06-02,
  between=2019-07-29 and 2019-09-01,
}
\pagebreak
%\kal{2019-03-01 to 2019-08-30}{\holidays}
\end{center}

\end{document}

There are some points where I need help:

  1. The school weeks should be numbered automatically, so in my example on Mo. 2018-09-10 there should be a small "SW 1" in the upper right corner of the day node. In Mo. 2018-09-17 there should be "SW 2" etc, Mo. 2018-10-01 would be "SW 4" and on Mo. 2018-10-29 there should be no mark since it is in the holidays, so "SW 8" would be on Mo. 2018-11-05 (and not "SW 9" since the SW counter shouldn't increment in the holidays).

If at least one day in a week is not a holiday day it should count as a "school week" and a particular "SW x" should be placed in the upper right corner of the first day in this week which is not a holiday day (in my example this would be always a Monday, but for a general solution it might be the case that for example Monday and Tuesday are holiday days but Wednesday, Thursday and Friday are not, in this case "SW x" should be printed on the Wednesday).

I tried the following code inside the day code, but it seems to be nonsense.

\ifdate{Monday}{
       \tikzset{
    loop over item/.code args={####1/####2/####3}{%
      \ifdate{between=####1 and ####2}{%
      }{
        \woche{\pgfcalendarsuggestedname}{\theweek}
        \stepcounter{week}    
      }},
    loop over item/.list/.expanded=\ferien
  }          
}{}

where the week command is defined as:

\newcounter{week}
\setcounter{week}{1}
\newcommand\woche[2]{
  \node [anchor=north east, align=right] at
    ($(#1.north east)+(0em, 0em)$) {\tiny{SW #2}.};}
  1. The calender week number should be on each monday in the right lower corner.

  2. The code redundancy should be reduced and the syntax for giving dates and holidays simplified as much as possible, in particular I want to give the list of holidays globally in just one place and another list of special dates in another global place (instead of writing \termin each time). However it should be possible to define several categories of dates (with different lists and styles).

You may have noticed that for example the date "Heilige Drei Könige" is printed on 2019-01-06 and as well in 2019-06-06 (which is wrong) since I specified the date once for both calendars. This should in particular be fixed (I know that I can fix it by duplicating the calendar code and set the date in the first part of the calendar only, but as written above I want to reduce the code redudancy and not increase it).

Best Answer

I think this addresses all the issues...

  1. For the School week thing, I used two conditionals: \ifWeekStarted, and \ifPrintSW. The first one is triggered every Monday to signal a week has started. Later on, if we're not in a holiday, and \ifWeekStarted, the \ifPrintSW switch is turned on. If this last switch is on, when PGF is drawing the node, it will step the schoolweek counter and write SW X. Now it will mark the earliest day of every week that it can. Now we only have to exclude possible holidays, so if the holiday falls on a Monday, it will hold \ifWeekStarted switch and wait for Tuesday to see if it can enable \ifPrintSW. We'll get to the holidays later.

  2. Similar to 1, but much easier, we only write a number every week. Here, however, we (may) have a problem. If you want both School Week and Calendar Week to start at 1, then the code is trivial. However, if you want the Calendar Week to start in January 1st, then I don't know if it's possible (with a reasonable amount of effort) without LuaTeX... I implemented a solution that with pdfTeX and XeTeX starts the Calendar Week numbering from 1 with the School Weeks, and with LuaTeX it calls os.date() to get the week number. Since you said it's OK, then OK :)

  3. I defined a command \addholiday which takes three arguments: an identifier for the holidays, a comma-separated list of holiday entries, and a style setting (which will be executed by \tikzset). Each holiday entry is of the form <date conditional>/<holiday name>. For example, the "Heilige Drei Könige" day is given as equals = 2019-01-06 / Heilige Drei Könige. All other valid PGF calendar day-selection keys are valid, for example: between = 2018-10-08 and 2018-10-09 / Seven-day weekend!!!. The code will then loop through the given holidays and print them when appropriate. Additionally, one can use the starred variant, \addholiday*, to define a "special" day, which is not a holiday per se, so it still counts as a school week, but such special days can have a header printed on them and have a special formatting.

Here's the first page when compiled with LuaTeX:

enter image description here

Note that the weeks are numbered from 37 because the output of date -d 2018-09-10 +%V (on a Linux machine) says that this is the 37th week of the year. When compiled with pdfTeX or XeTeX it will start from 1.

Notice that due to the Seven-day weekend!!! the label SW 5. appears only on Wednesday. Note also that, because for week number 42 I used \addholiday*, then the week still counts as School Week (SW 6.), but it gets a "Holiday Label" and gets formatted accordingly.


The first version I created (if someone is interested you can find it in the edit history -- it is too long to post two codes) used low-level definitions and argument handling to define the \addholiday macro, so it soon became far too awkward to maintain or modify anything (mostly because I was bodging-in features as I was writing the command. Don't recommend that :).

The current version uses expl3's property lists to store the holidays. This allows me to easily add properties to the holidays, query the existing properties, and use them in the drawing code.

If you call, say, this command:

\addholiday*{HDAY}{%
 , between = 2018-10-15 and 2018-10-19 / Special week!
 , equals = 2019-01-06 / Heilige Drei Könige
}
{every day/.style={fill=green!30}}

then the property tree created will be like this:

enter image description here

(I am deeply sorry for my attempt using forest. Code adapted from this answer).

When you do that, the \addholiday command creates a \l__julia_HDAY_holiday property list which will contain three properties: special, style, and dates. The special property will contain a boolean, true if the starred variant was used, false otherwise. The style property will hold the last argument to \addholiday which will do the formatting of this set of dates. The last property, dates, will contain the reference to another property list, \l__julia_HDAY_holiday_dates. This property list will contain as many properties as the number of entries you passed to \addholiday. Each property will be the name of the holiday, and the corresponding value will be the PGF Calendar test to find the dates.

Now the command \holidaycheck iterates through the defined holiday lists and calls \__julia_holiday_check:nnn. This last macro is responsible for checking if the current date is a holiday (with \ifdate), and executing the provided code if that's the case. With all this structure set up, implementing a verification to print only the first instance of a holiday's name is close to trivial :)

For each holiday (for example Heilige Drei Könige), we check if a command called \is_Heilige Drei Könige_printed is defined. If it is not, then we set the boolean \WriteName to true, write the holiday's name, and define the command \is_Heilige Drei Könige_printed (the definition is empty). If that command is defined (by the previous step) then we set \WriteName to false and don't write the name.

Code:

% DIN-A4 doublesided year calendar
% Author: Robert Krause
% License : Creative Commons attribution license
% Submitted to TeXample.net on 13 July 2018

% Modified by julia 2018

\documentclass[a4paper, ngerman, 10pt]{scrartcl}
\usepackage[utf8]{inputenc}
\usepackage[ngerman]{babel}
\usepackage[T1]{fontenc}
\usepackage{tikz,xparse}            % Use the calendar.sty style
\usepackage{iftex}
\makeatletter
\ifLuaTeX
  \expandafter\@firstofone
\else
  \expandafter\@gobble
\fi
  {\usepackage{luacode}}

\usepackage{translator} % German Month and Day names
\usepackage{fancyhdr}       % header and footer
\usepackage{fix-cm}     % Large year in header
\usepackage{xparse}

\usepackage[ headheight = 0.8cm, hmargin=.5cm,
  top = 1.7cm, nofoot,bottom=0cm]{geometry}

\usetikzlibrary{calc}
\usetikzlibrary{calendar}
\renewcommand*\familydefault{\sfdefault}

% Names of Holidays are inserted by employing this macro
\def\termin#1#2{%
  \node [anchor=north west, text width= 3.4cm] at
    ($(#1.north west)+(3em, 0em)$) {\tiny{#2}};
}

\ifLuaTeX
\begin{luacode*}
function get_week_number(y,m,d)
  return os.date("%V",os.time{year=y,month=m,day=d})
end
\end{luacode*}
\fi

\newcounter{calweek}
\newcounter{schoolweek}

\newcommand\woche[2]{%
  \node [anchor=south east, align=right] at
    ($(#1.south east)+(0em, 0em)$) {\tiny{#2}};}

\newcommand\schulwoche[2]{%
  \node [anchor=north east, align=right] at
    ($(#1.north east)+(0em, 0em)$) {\tiny{SW #2}.};}

\ExplSyntaxOn
\bool_new:N \WriteName
\prop_new:N \l__julia_holiday_list_prop
\bool_new:N \l__julia_special_bool
\cs_new:Npn \exp_args:NNnc { \::N \::n \::c \::: }
\NewDocumentCommand\addholiday
  { s m m m }
  {
    \IfBooleanTF { #1 }
      { \bool_set_true:N \l__julia_special_bool }
      { \bool_set_false:N \l__julia_special_bool }
    \__julia_add_holiday:nnn { #2 } { #3 } { #4 }
  }
\cs_new:Npn \__julia_add_holiday:nnn #1 #2 #3
  {
    \prop_clear_new:c { l__julia_#1_holiday }
    \prop_clear_new:c { l__julia_#1_holiday_dates }
    \__julia_add_holiday_aux:ccnnn
      { l__julia_#1_holiday } { l__julia_#1_holiday_dates }
      { #1 } { #2 } { #3 }
  }
\cs_new:Npn \__julia_add_holiday_aux:NNnnn #1 #2 #3 #4 #5
  {
    \prop_put:Nnn \l__julia_holiday_list_prop { #3 } { #1 }
    \exp_args:NNnV
    \prop_put:Nnn #1 { special } \l__julia_special_bool
    \prop_put:Nnn #1 { dates } { #2 }
    \prop_put:Nnn #1 { style } { #5 }
    \clist_map_inline:nn { #4 }
      { \__julia_prop_split_add:Nn #2 { ##1 } }
    % \prop_show:N #1
  }
\cs_generate_variant:Nn \__julia_add_holiday_aux:NNnnn { ccnnn }
\cs_new:Npn \__julia_prop_split_add:Nn #1 #2
  { \__julia_prop_split_add:Nw #1 #2 \q_stop }
\cs_new:Npn \__julia_prop_split_add:Nw #1 #2 / #3 \q_stop
  {
    \exp_args:NNff
    \prop_put:Nnn #1
      { \tl_trim_spaces:n { #3 } }
      { \tl_trim_spaces:n { #2 } }
  }
\cs_new:Npn \holidaycheck #1
  {
    \cs_gset_eq:NN \HolidayStyle \c_empty_tl
    \prop_map_inline:Nn \l__julia_holiday_list_prop
      {
        \prop_get:NnN ##2 { dates   } \HLprop
        \prop_get:NnN ##2 { style   } \HLstyle
        \prop_get:NnN ##2 { special } \HLtype
        \prop_map_inline:Nn \HLprop
          {
            \__julia_holiday_check:nnn
              { #1 } { ####1 } { ####2 }
          }
      }
  }
\cs_new:Npn \__julia_holiday_check:nnn #1 #2 #3
  {
    % #1 is the code passed to \holidaycheck
    % #2 is the holiday name
    % #3 is the holiday date
    \ifdate { #3 }
      {
        \cs_set:Npn \name { #2 }
        \cs_if_exist:cTF { is_ #2 _printed }
          { \bool_set_false:N \WriteName }
          { \bool_set_true:N \WriteName }
        #1
      }
      { }
  }
\cs_new:Npn \HolidaySetUsed #1
  { \cs_gset_eq:cN { is_ #1 _printed } \c_empty_tl }
\ExplSyntaxOff

%Header
\renewcommand{\headrulewidth}{0.0pt}
\setlength{\headheight}{0.8cm}
\chead{%
 \Huge 2018/2019
 \Large\textbf{Termine}\hfill
}
\cfoot{}

\newcommand{\kheight}{0.82}
\newcommand{\kwidth}{3.0}
\newcommand{\kshift}{3.4}
\newcommand{\calstartdate}{2018-09-10}

\newif\ifWeekStarted
\WeekStartedfalse
\newif\ifPrintSW
\PrintSWfalse
\newif\ifHasHoliday

\newcommand{\kal}[2]{%
\vspace*{-1cm}
\begin{tikzpicture}[every day/.style={anchor = north}]
\calendar[
  dates = #1,
  name = cal,
  day yshift = 3em,
  day code = 
    {%
      \holidaycheck{%
        \global\let\HolidayStyle\HLstyle
      }%
      \expandafter\tikzset\expandafter{\HolidayStyle}%
      \node [
        name = \pgfcalendarsuggestedname,
        every day,
        shape = rectangle,
        minimum height = \kheight cm,
        text width = \kwidth cm,
        draw = gray]
        {\tikzdaytext\enskip
         \pgfcalendarweekdayshortname{\pgfcalendarcurrentweekday}%
        };%
      \holidaycheck{%
        \IfBooleanF{\HLtype}{\global\PrintSWfalse} % A holiday
        \IfBooleanTF{\WriteName}
          {%
            \termin{\pgfcalendarsuggestedname}{\name}{}%
            \HolidaySetUsed{\name}%
          }%
          {}%
      }%
      \ifPrintSW
        \stepcounter{schoolweek}%
        \global\WeekStartedfalse
        \global\PrintSWfalse
        \schulwoche{\pgfcalendarsuggestedname}{\arabic{schoolweek}}%
      \fi
      \ifdate{Monday}{%
        \ifLuaTeX
          \setcounter{calweek}{%
            \directlua{%
              tex.print(
                get_week_number(
                  \pgfcalendarcurrentyear,
                  \pgfcalendarcurrentmonth,
                  \pgfcalendarcurrentday
                )
              )
            }%
          }%
        \else
          \stepcounter{calweek}%
        \fi
        \woche{\pgfcalendarsuggestedname}{\arabic{calweek}}%
      }{}%
    },
  execute before day scope=
    {%
      \ifdate{day of month=1}
      {%
        % Shift right
        \pgftransformxshift{\kshift cm}
        % Print month name 
        \draw (0,0) node [shape=rectangle, minimum height= \kheight cm,
          text width = \kwidth cm, fill = red, text= white, draw = red, text centered]
          {\textbf{\pgfcalendarmonthname{\pgfcalendarcurrentmonth}}};
      }{}%
      \ifdate{Monday}{%
        \global\WeekStartedtrue
      }{}
      \ifdate{workday}
      {%
        % normal days are white
        \tikzset{every day/.style={fill=white}}
        % Vacation (Germany, Baden-Wuerrtemberg) gray background
        \expandafter\ifdate\expandafter{#2}{%
          \tikzset{every day/.style={fill=gray!30}}
        }{%
          \ifWeekStarted
            \global\PrintSWtrue
          \fi
        }%
      }{}%
      % Saturdays and half holidays (Christma's and New year's eve)
      \ifdate{Saturday}{%
        \tikzset{every day/.style={fill=red!10}}%
      }{}%
      % % Sundays and full holidays
      \ifdate{Sunday}{%
        \tikzset{every day/.style={fill=red!20}}%
      }{}%
    },
  execute at begin day scope=
    {%
      % each day is shifted down according to the day of month
      \pgftransformyshift{-\kheight*\pgfcalendarcurrentday cm}
    }
  ];
\end{tikzpicture}
}

\addholiday{normal holidays}{%
 , between = 2018-10-08 and 2018-10-09 / Seven-day weekend!!!
 , equals = 2018-10-03 / Tag der dt. Einheit
 , equals = 2019-01-01 / Neujahr
 , equals = 2019-01-06 / Heilige Drei Könige
 , equals = 2019-04-19 / Karfreitag
 , equals = 2019-04-21 / Ostersonntag
 , equals = 2019-04-22 / Ostermontag
 , equals = 2019-05-01 / Tag der Arbeit
 , equals = 2019-05-30 / Christi Himmelfahrt
 , equals = 2019-06-09 / Pfingstsonntag
 , equals = 2019-06-20 / Pfingstmontag
}
{every day/.style={fill=gray!30}}

\addholiday*{very special week}{%
 , between = 2018-10-15 and 2018-10-19 / Special week!
}
{every day/.style={fill=green!30}}

\newcommand{\HolidayList}{%
  between=2018-10-03 and 2018-10-05,
  between=2018-10-29 and 2018-11-04,
  between=2019-02-28 and 2019-02-28,
  between=2019-03-01 and 2019-03-08,
  between=2019-04-15 and 2019-04-26,
  between=2019-06-10 and 2019-06-02,
  between=2019-07-29 and 2019-09-01,
}

\begin{document}
\pagestyle{fancy}
\begin{center}
\kal{2018-09-10 to 2019-02-28}{\HolidayList}
\kal{2019-03-01 to 2019-08-30}{\HolidayList}
\pagebreak
% \kal{2019-03-01 to 2019-08-30}{\holidays}
\end{center}

\end{document}