A Calendar in XForms

The authorSteven Pemberton, CWI Amsterdam

Contents

  1. Contents
  2. A Month
  3. Day numbers
  4. Display
  5. Next steps
  6. Add data
  7. Move day 1 up
  8. Unwanted days
  9. Unwanted weeks
  10. Calculating the Month and its Values
  11. Initialising
  12. Month length
  13. Month length
  14. What Day of the Week does a Month Start On?
  15. Dominic
  16. The real start day of the month
  17. What's Next?
  18. Today
  19. Displaying today specially
  20. Events
  21. Display with events

A Month

A month consists of a number of weeks of seven days, displayed as a grid. In fact there can be up to six weeks displayed for a month, if the first day of the month falls on one of the two last days of the first week.

<instance>
  <cal xmlns="">
     <week><day/><day/><day/><day/><day/><day/><day/></week>
     <week><day/><day/><day/><day/><day/><day/><day/></week>
     <week><day/><day/><day/><day/><day/><day/><day/></week>
     <week><day/><day/><day/><day/><day/><day/><day/></week>
     <week><day/><day/><day/><day/><day/><day/><day/></week>
     <week><day/><day/><day/><day/><day/><day/><day/></week>
  </cal>
</instance>

Day numbers

To start off, we'll fill them starting from 1:

<bind ref="week/day" 
      calculate="1 + 7*count(../preceding-sibling::week)
                   + count(preceding-sibling::day)"/>

This sets the day number to the week number times seven, plus the day number in the week, where both start counting from zero, plus 1. So the very first day will be numbered 1 and so on.

Display

(Plus a little bit of CSS, not shown here):

<repeat ref="week">
   <repeat ref="day">
      <output ref="."/>
   </repeat>
</repeat>

which looks like this:

Source

Next steps

  1. Decide which month will be displayed;
  2. find out what day of the week that month starts on;
  3. move the numbers up so that day 1 is in the right place;
  4. hide the days that shouldn't be displayed for the month;
  5. make it pretty.

Add data

Add some data to the instance:

<instance>
  <cal xmlns="">
     <y/><m/>
     <monthlength/>
     <startday/>
     <week><day/>...
     ...
  </cal>
</instance>

We'll see how to calculate those shortly, but assume for now that that has been done.

Move day 1 up

So let's move the numbers up, so that day 1 falls on the right day of the week. To do that we change the bind that calculates the day number, so that it also subtracts the start day:

<bind ref="week/day" 
      calculate="1 + 7*count(../preceding-sibling::week) 
                   + count(preceding-sibling::day)
                   - /cal/startday"/>

Suppose the month starts on day 3 of the week, then this is what it looks like:

Source

Unwanted days

If the day number is less than 1, or more than the length of the month, then output nothing, and otherwise the number.

&#xa0; is a non-breaking space, and is needed because without it, on some browsers the CSS layout gets messed up.

<repeat ref="week" class="week">
   <repeat ref="day">
      <output value="if(. &lt; 1 or . &gt; /cal/monthlength, '&#xa0;', .)"
              class="day"/>
   </repeat>
</repeat>

It looks like this now:

Source

Unwanted weeks

You can see that there's now a blank week at the end; we can easily get rid of that with a bind:

<bind ref="week" relevant="day[1] &lt;= /cal/monthlength"/>

which says that a week is only relevant if its first day is less than or equal to the month length. Non-relevant values are never displayed:

Source

Calculating the Month and its Values

Let's start off initially with displaying the current month. To get that, we use the function local-date(), which returns the current date (plus how many hours different your timezone is from UTC):

Source

We want to extract the year and month from this string, and we want to set it on initialisation of the form.

Initialising

So we add the following action to respond to the xforms-ready event:

<action ev:event="xforms-ready">
   <setvalue ref="y" value="substring-before(local-date(), '-')"/>
   <setvalue ref="m"
             value="substring-before(substring-after(local-date(), '-'), '-')"/>
</action>

So the year is the substring before the first hyphen, and the month is the substring after the first hyphen, and before the second.

Month length

We add the length of each month to the instance:

<ml>31</ml><ml>28</ml><ml>31</ml><ml>30</ml><ml>31</ml><ml>30</ml>
<ml>31</ml><ml>31</ml><ml>30</ml><ml>31</ml><ml>30</ml><ml>31</ml>

But February changes in leap years. So we add leap to the instance, and calculate if the current year is a leap year (if the year is divisible by 4, but not by 100, or divisible by 400):

<bind ref="leap"
      calculate="if(((../y mod 4 = 0) and (../y mod 100 != 0)) or (../y mod 400 = 0), 1, 0)"/>

and calculate February accordingly:

<bind ref="ml[2]" calculate="28+../leap"/>

Month length

This now allows us to calculate the month length of the current month:

<bind ref="monthlength" calculate="../ml[position()=../m]"/>

Source

(Remember that we haven't calculated the true start day for the month yet, but you can play around with it, to see how the display changes.)

What Day of the Week does a Month Start On?

Answer: its day number in the year, less the 'Dominic' number for the year, plus a constant, all modulo 7 so that we get a value in the range 0 to 6.

The dominic number for any year is: 7 - ((year + floor(year ÷ 4) - century + floor(century ÷ 4) - leap) mod 7).

Well, we've already calculated leap. Let's do the rest.

Dominic

Add century and dominic to the instance:

<bind ref="century" calculate="floor(../year div 100)"/>
<bind ref="dominic" 
      calculate="7-((../y+floor(../y div 4)-../century+floor(../century div 4)-../leap) mod 7)"/>

Now calculate the day in the year of the first of the month:

<bind ref="dayinyear"
      calculate="sum(../ml[position() &lt; ../m]) + 1"/>

That is, add the month lengths for all months before the current month, and add one.

The real start day of the month

<bind ref="startday" calculate="(11 + ../dayinyear - ../dominic) mod 7"/>

See it in action here:

Source

This is the first moment we have made any decision about which weekday is the first day of the week. In this case we have used Monday; however, if your week starts with a different day, it is easy to add the necessary offset (you change that 11 to adjust).

What's Next?

Clearly, we need to add some decoration, like the names of the weekdays.

Controls to step through the calendar.

Year:

<trigger appearance="minimal"><label>→</label>
   <setvalue ref="y" value=". + 1" ev:event="DOMActivate"/>
</trigger>

Month:

<trigger appearance="minimal"><label>→</label>
   <action ev:event="DOMActivate">
      <setvalue ref="m" value="if(.=12, 1, . + 1)"/>
      <setvalue ref="y[../m=1]" value=". + 1"/>
   </action>
</trigger>

Today

I'll calculate it in a different way this time, just to show the options. Since local-date() has a fixed format, you can select fixed parts of the result:

<today><y/><m/><d/></today>
 ...
<bind ref="today">
   <bind ref="y" calculate="substring(local-date(), 1, 4)"/>
   <bind ref="m" calculate="substring(local-date(), 6, 2)"/>
   <bind ref="d" calculate="substring(local-date(), 9, 2)"/>
</bind>

There is a race condition in the calculation, since local-date gets called three times, and the date might just change between the calls. It will hardly ever happen, but it could. So better to add local-date to the instance as well, and call the function just once:

<today><date/><y/><m/><d/></today>
 ...
<bind ref="today">
   <bind ref="date" calculate="local-date()"/>
   <bind ref="y" calculate="substring(../date, 1, 4)"/>
   <bind ref="m" calculate="substring(../date, 6, 2)"/>
   <bind ref="d" calculate="substring(../date, 9, 2)"/>
</bind>

Displaying today specially

Now in the main output, we will add a special class on the output element:

<repeat ref="week" class="week">
   <repeat ref="day">
      <output value="if(. &lt; 1 or . &gt; /cal/monthlength, '&#xa0;', .)"
              class="{if(. = /cal/today/d and /cal/m = /cal/today/m and /cal/y = /cal/today/y, 
                         'today', 'day')}"/>
   </repeat>
</repeat>

which sets the class of the output to 'today' if it represents today's date, and otherwise to 'day', and we can use any CSS rules we want to display them differently:

Source

Events

We will have an instance to hold events, which we will load from an external source:

<instance id="events" src="events.xml"/>

This will hold our calendar events. For now a simple version:

<events>
   <event y="2018" m="3" d="20" t="10:00">Write program</event>
   <event y="2018" m="3" d="20" t="13:30">Meeting</event>
   <event y="2018" m="3" d="24">Clocks go forward</event>
   ...
</events>

Display with events

<repeat ref="week" class="week">
   <repeat ref="day">
      <group class="{if(. = /cal/today/d and /cal/m = /cal/today/m and /cal/y = /cal/today/y, 'today', 'day')}">
         <output value="if(. &lt; 1 or . &gt; /cal/monthlength, '&#xa0;', .)"/>
         <repeat ref="instance('events')/event[@y=/cal/y and @m = /cal/m and @d = context()]">
            <output class="event" value="concat(@t, ' ', .)"/>
         </repeat>
      </group>
   </repeat>
</repeat>

Source

Actually I also added

<output class="{if(@t='', 'dayevent', 'event')}" value="concat(@t, ' ', .)"/>

to allow different styling for events with no time given.

There. Depending on what you count, 75 lines of XForms. A simple, direct implementation of a calendar viewer.