ElearningWorld.org

For the online learning world

Elearning WorldLinuxTechnical

Calendar

Introduction

In recent years, I’ve been creating a calendar using images that I’ve taken. Back in 2002, I created a small Java program that prints out the calendar for the next twelve months. In my themes and on the MoodleBites eLearningWorld theme courses I have code that arranges the Moodle blocks horizontally, this is partly facilitated through the employment of column CSS classes that are based upon the ideas implemented in the Bootstrap framework. Combine all of these thoughts, and add to my recent posts with Java then I thought ‘Why can’t I get Java to create a calendar just like the one I have printed?’. And that’s what this month is all about, where we will additionally see how pre-processing of HTML output can be designed and implemented from scratch.

Disclaimers

Ubuntu® is a registered trademark of Canonical Ltd – ubuntu.com/legal/intellectual-property-policy.

Firefox® is a registered trademark of the Mozilla Foundation.

Moodle™ is a registered trademark of ‘Martin Dougiamas’ – moodle.com/trademarks.

Other names / logos can be trademarks of their respective owners. Please review their website for details.

I am independent from the organisations mentioned and am in no way writing for or endorsed by them.

The information presented in this article is written according to my own understanding, there could be technical inaccuracies, so please do undertake your own research.

The images used are my copyright, please don’t use outside of the context of this post / project without my permission.

References

Prerequisites

To understand and run the code presented, I recommend that you read my previous posts beforehand: ‘A little bit of Java‘ and ‘A little bit more Java‘.

The calendar

Before we look at the code, which will take some time to read and understand, lets look at the web page output so that we have the image of the goal in mind. That’s the thing with software, when it gets complicated, having an understanding of its purpose keeps you going when it gets difficult:

2023 Calendar

The implementation

I have commented the code throughout to explain what each part does:

/*
 * CalGen.
 *
 * Generates the calendar for the year set, both as a HTML page from 'templated' and as text.
 *
 * Copyright (C) 2022 G J Barnard.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.
 */

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Calendar;
import java.util.Collections;
import java.util.GregorianCalendar;
import java.util.Iterator;
import java.util.LinkedList;

/**
 * CalGen class.
 *
 * @copyright 2022 G J Barnard.
 */
public class CalGen {

    // Calendar attributes.
    private final GregorianCalendar gc = new GregorianCalendar(); // The calenadar.
    private final LinkedList<Integer> months = new LinkedList<>(); // The months of the year.
    private final LinkedList<Integer> days = new LinkedList<>(); // The days of the week.
    private int currentMonth; // Keep track of the current month between methods.
    private int previousMonth; // Keep track of the previous month between methods.
    private final int theYear; // The year we are using.

    // Template attributes.
    private char[] calendarTemplate = null; // The template for the calendar.
    private char[] monthTemplate = null; // The template of a month within the calendar.

    private final char[] mpre = {'{', '{'}; // Template markup token start characters.
    private final char[] mpost = {'}', '}'}; // Template markup token end characters.
    private final FileOutputStream mout; // The stream for the markup html file.
    // Stores the markup html as its being generated before it is output to the file.
    private final StringBuffer markupOut = new StringBuffer();

    /**
     * Create the calendar and generate both the text and markup versions.
     *
     * @param args the command line arguments - not used.
     * @throws java.io.FileNotFoundException If a template file cannot be found.
     * @throws java.io.IOException If a problem occurs when reading a template file.
     */
    public static void main(String args[]) throws FileNotFoundException, IOException {
        CalGen us = new CalGen();

        us.calendar();
        us.calendarTemplate();
    }

    /**
     * Constructor.
     *
     * @throws FileNotFoundException If a template file cannot be found.
     */
    public CalGen() throws FileNotFoundException {
        this.gc.setFirstDayOfWeek(Calendar.TUESDAY); // Change to another day if wished.
        this.theYear = 2023;

        // The name of the markup file in the current directory.
        this.mout = new FileOutputStream("./" + this.theYear + "_Calendar.html");

        // Add the months in the order we wish to output them as text.
        months.add(Calendar.JANUARY);
        months.add(Calendar.FEBRUARY);
        months.add(Calendar.MARCH);
        months.add(Calendar.APRIL);
        months.add(Calendar.MAY);
        months.add(Calendar.JUNE);
        months.add(Calendar.JULY);
        months.add(Calendar.AUGUST);
        months.add(Calendar.SEPTEMBER);
        months.add(Calendar.OCTOBER);
        months.add(Calendar.NOVEMBER);
        months.add(Calendar.DECEMBER);

        // Add the days in the order used by default in the GregorianCalendar.
        days.add(Calendar.SUNDAY);
        days.add(Calendar.MONDAY);
        days.add(Calendar.TUESDAY);
        days.add(Calendar.WEDNESDAY);
        days.add(Calendar.THURSDAY);
        days.add(Calendar.FRIDAY);
        days.add(Calendar.SATURDAY);

        // Rotate the days around if needed so that the start day is first in the list.
        Iterator<Integer> daysIt = days.iterator(); // The means of iterating over our list.
        boolean found = false; // Have we found the day we are looking for?
        int count = 0; // The number of positions the day we are looking for is away from Sunday.
        Integer current; // The reference to the current day.
        int firstDayOfWeek = this.gc.getFirstDayOfWeek(); // The day that the calendar has been set to be the first day of the week.

        // While we have another day to check and we've not found the day we are looking for.
        while (daysIt.hasNext() && found == false) {
            current = daysIt.next(); // Get the next day.
            if (current == firstDayOfWeek) { // Have we found the day we are looking for?
                found = true; // Yes.
            } else {
                count++; // Increment the position.
            }
        }

        if (count > 0) { // The day we are looking for is not Sunday, but is 'count' positions away from it.
            // Rotate the list to the left by the number of positions we have calculated.
            // The day we are looking for will then be the first.
            Collections.rotate(days, -count);
        }
    }

    /**
     * Generate the text version of the calendar.
     */
    public void calendar() {
        this.gc.set(theYear, 0, 1); // Set to the 1st January for the year we want.

        // Output the year.
        System.out.println(this.gc.get(Calendar.YEAR));

        // Output the months.
        Iterator<Integer> mit = this.months.iterator();
        while (mit.hasNext()) {
            this.month(mit.next());
            System.out.println();
        }
    }

    /**
     * Output the month.
     *
     * @param theMonth The month to output.
     */
    private void month(int theMonth) {
        this.gc.set(Calendar.MONTH, theMonth); // Tell the calendar the month we wish to use.
        // Set both months to be the same so that we can detect when the current changes.
        this.currentMonth = theMonth;
        this.previousMonth = theMonth;

        System.out.println(this.getMonthText(gc.get(Calendar.MONTH))); // Output the month text.

        // Output the day names.
        Iterator<Integer> daysIt = this.days.iterator();
        Integer current;
        while (daysIt.hasNext()) {
            current = daysIt.next();
            this.day(this.getDayText(current));
            if (daysIt.hasNext()) {
                System.out.print(" ");
            } else {
                System.out.println();
            }
        }

        // Output the 'blank days' before the day on which the 1st of the month is.
        // The current 'position' of the day in the week we are outputing, so '1' is the first day of the week.
        int currentPosition = 1;
        daysIt = this.days.iterator();
        boolean startDayReached = false; // Have we found the start day?
        int monthStartPostion = this.gc.get(Calendar.DAY_OF_WEEK); // Day of the week that the month starts on.

        while (daysIt.hasNext() && (startDayReached == false)) {
            current = daysIt.next();
            if (current == monthStartPostion) {
                startDayReached = true;
            } else {
                currentPosition++;
                this.day("");
                System.out.print("    ");
            }
        }

        // Loop until we have reached the next month.
        while (this.currentMonth == this.previousMonth) {

            // Loop through the day 'positions' as we have outputted them with the day names.
            while (currentPosition < 8) {

                // Have we reached the next month?
                if (this.currentMonth != this.previousMonth) {
                    // Are we on a week that has been started but not finished?
                    if (currentPosition != 1) {
                        // Loop through the remaining positions and output 'blank' days.
                        while (currentPosition < 8) {
                            this.day("");
                            System.out.print("    ");
                            currentPosition++;
                        }
                    }
                } else {
                    // Output the day.
                    if (this.gc.get(Calendar.DAY_OF_MONTH) > 9) { // Get the prefixing spacing correct.
                        System.out.print(" ");
                    } else {
                        System.out.print("  ");
                    }
                    this.day(this.gc.get(Calendar.DAY_OF_MONTH)); // The day.
                    if (currentPosition < 7) {
                        System.out.print(" "); // Postfix space.
                    }

                    // Get the next day.
                    this.gc.add(Calendar.DAY_OF_MONTH, 1);
                    this.currentMonth = this.gc.get(Calendar.MONTH);

                    currentPosition++; // The next position in the week.
                }
            }
            currentPosition = 1; // Reset to the next week.
            System.out.println();
        }
    }

    /**
     * Output the day.
     * @param day As an integer.
     */
    private void day(Integer day) {
        this.day(day.toString());
    }

    /**
     * Output the day.
     *
     * @param day As a string.
     */
    private void day(String day) {
        System.out.print(day);
    }

    /**
     * Generate the markup version of the calendar.
     *
     * @throws FileNotFoundException If a template file cannot be found.
     * @throws IOException If a problem occurs when reading a template file.
     */
    public void calendarTemplate() throws FileNotFoundException, IOException {
        this.gc.set(theYear, 0, 1); // Reset to the 1st January for the year we want.

        this.loadTemplates(); // Load the templates.

        // Process the calendar template.
        int currentIndex = 0; // Index of the current character.
        while (currentIndex < this.calendarTemplate.length) {
            if ((this.calendarTemplate[currentIndex] == this.mpre[0]) &&
                (this.calendarTemplate[currentIndex + 1]) == this.mpre[1]) {
                // Start token.
                currentIndex = currentIndex + 2; // Jump over the token start characters.
                currentIndex = this.processCalendarToken(currentIndex); // Process the token.
            } else {
                // Pass through.
                this.markupOut.append(this.calendarTemplate[currentIndex]); // Copy the character to the output.
                currentIndex++; // Get the next character.
            }
        }

        this.mout.write(this.markupOut.toString().getBytes()); // Write the markup to the output file.
        this.mout.close(); // Close the file.
    }

    /**
     * Load the templates.
     *
     * Ref: https://stackoverflow.com/questions/21980090/javas-randomaccessfile-eofexception
     *
     * @throws FileNotFoundException If a template file cannot be found.
     * @throws IOException If a problem occurs when reading a template file.
     */
    private void loadTemplates() throws FileNotFoundException, IOException {
        // Calendar template.
        java.io.File templateFile = new java.io.File("CalendarTemplate.txt"); // Using the File object so that we can get the length.

        char[] buffer = new char[(int) templateFile.length()]; // Buffer to store the read characters.

        java.io.FileInputStream fin = new java.io.FileInputStream(templateFile); // Stream to read the file.
        // Reader to read the file that is encoded with UTF-8 characters.
        java.io.InputStreamReader isr = new java.io.InputStreamReader(fin, "UTF-8");

        isr.read(buffer); // Read the file into the buffer.
        isr.close(); // Close the file.

        // To allow us to convert the bytes into characters.
        StringBuilder sb = new StringBuilder((int) templateFile.length());
        sb.append(buffer);

        this.calendarTemplate = sb.toString().toCharArray(); // Convert into a string and then an array of chars.

        // Month template.
        // Same processing as the Calendar Template.
        templateFile = new java.io.File("MonthTemplate.txt");
        buffer = new char[(int) templateFile.length()];

        fin = new java.io.FileInputStream(templateFile);
        isr = new java.io.InputStreamReader(fin, "UTF-8");

        isr.read(buffer);
        isr.close();

        sb = new StringBuilder((int) templateFile.length());
        sb.append(buffer);

        this.monthTemplate = sb.toString().toCharArray();
    }

    /**
     * Process a token in the Calendar template.
     *
     * @param currentIndex The current character in the calendar template.
     * @return The updated position in the template after processing the token so that we can continue.
     */
    private int processCalendarToken(int currentIndex) {
        int end = this.calendarTemplate.length;
        StringBuilder token = new StringBuilder();

        // Extract the whole token until the end token characters are reached/
        while (currentIndex < end) {
            if ((this.calendarTemplate[currentIndex] == this.mpost[0]) &&
                (this.calendarTemplate[currentIndex + 1]) == this.mpost[1]) {
                // End token.
                currentIndex = currentIndex + 2;
                end = currentIndex; // Exit the loop.
            } else {
                // Characters of the token.
                token.append(this.calendarTemplate[currentIndex]);
                currentIndex++;
            }
        }
        this.processCalendarToken(token.toString()); // Process the token.

        return currentIndex;
    }

    /**
     * Process the extracted token.
     *
     * @param token The token to process.
     */
    private void processCalendarToken(String token) {
        int dataIndex = token.indexOf('-'); // Do we have 'parameter' data in the token?
        String data = null;
        String dataExtra = null;
        if (dataIndex != -1) {
            // We have data, so extract it.
            data = token.substring(dataIndex + 1, token.length());
            token = token.substring(0, dataIndex);

            int ampIndex = data.indexOf('&'); // Do we have a second parameter data in the token?
            if (ampIndex != -1) {
                dataExtra = data.substring(ampIndex + 1, data.length());
                data = data.substring(0, ampIndex);
            }
        }

        // Identify and execute the action of the token with its data if any.
        // Rule switch, Java 12 - https://blogs.oracle.com/javamagazine/post/new-switch-expressions-in-java-12
        switch (token) {
            case "calendartitle" -> this.markupOut.append(this.gc.get(Calendar.YEAR)).append(" Calendar");
            case "jan" -> this.monthTemplate(Calendar.JANUARY, data, dataExtra);
            case "feb" -> this.monthTemplate(Calendar.FEBRUARY, data, dataExtra);
            case "mar" -> this.monthTemplate(Calendar.MARCH, data, dataExtra);
            case "apr" -> this.monthTemplate(Calendar.APRIL, data, dataExtra);
            case "may" -> this.monthTemplate(Calendar.MAY, data, dataExtra);
            case "jun" -> this.monthTemplate(Calendar.JUNE, data, dataExtra);
            case "jul" -> this.monthTemplate(Calendar.JULY, data, dataExtra);
            case "aug" -> this.monthTemplate(Calendar.AUGUST, data, dataExtra);
            case "sep" -> this.monthTemplate(Calendar.SEPTEMBER, data, dataExtra);
            case "oct" -> this.monthTemplate(Calendar.OCTOBER, data, dataExtra);
            case "nov" -> this.monthTemplate(Calendar.NOVEMBER, data, dataExtra);
            case "dec" -> this.monthTemplate(Calendar.DECEMBER, data, dataExtra);
            default -> this.markupOut.append("<p>Calendar error!  Unknown token.</p>");
        }
    }

    /**
     * Execute a month token by processing the month template with the supplied parameters.
     *
     * @param theMonth The month.
     * @param imageName The name of the image for the month.
     * @param imageDescription The description of the image for the month.
     */
    private void monthTemplate(int theMonth, String imageName, String imageDescription) {
        this.gc.set(Calendar.MONTH, theMonth); // Tell the calendar the month we want so that it tells us the correct days.
        this.previousMonth = theMonth;
        this.currentMonth = theMonth;

        int currentIndex = 0; // Current character in the month template.
        while (currentIndex < this.monthTemplate.length) {
            if ((this.monthTemplate[currentIndex] == this.mpre[0]) && (this.monthTemplate[currentIndex + 1]) == this.mpre[1]) {
                // Start token.
                currentIndex = currentIndex + 2;
                currentIndex = this.processMonthToken(currentIndex, imageName, imageDescription);
            } else {
                // Pass through.
                this.markupOut.append(this.monthTemplate[currentIndex]);
                currentIndex++;
            }
        }
    }

    /**
     * Execute a month token.
     *
     * @param currentIndex The current character in the month template.
     * @param imageName The name of the image for the month.
     * @param imageDescription The description of the image for the month.
     */
    private int processMonthToken(int currentIndex, String imageName, String imageDescription) {
        int end = this.monthTemplate.length;
        StringBuilder token = new StringBuilder();
        while (currentIndex < end) {
            if ((this.monthTemplate[currentIndex] == this.mpost[0]) && (this.monthTemplate[currentIndex + 1]) == this.mpost[1]) {
                // End token.
                currentIndex = currentIndex + 2;
                end = currentIndex; // Exit the loop.
            } else {
                // Characters of the token.
                token.append(this.monthTemplate[currentIndex]);
                currentIndex++;
            }
        }
        this.processMonthToken(token.toString(), imageName, imageDescription);

        return currentIndex;
    }

    /**
     * Process a month token.
     *
     * @param token The month token.
     * @param imageName The name of the image for the month.
     * @param imageDescription The description of the image for the month.
     */
    private void processMonthToken(String token, String imageName, String imageDescription) {
        int dataIndex = token.indexOf('-');
        String data = null;
        if (dataIndex != -1) {
            // We have data.
            data = token.substring(dataIndex + 1, token.length());
            token = token.substring(0, dataIndex);
        }

        switch (token) {
            case "monthtitle" -> this.markupOut.append(this.getMonthText(gc.get(Calendar.MONTH)));
            case "monthdaynames" -> this.monthDayNames(data);
            case "monthweek" -> this.monthWeek(data);
            case "monthimage" -> this.monthImage(imageName);
            case "monthimagedescription" -> this.monthImage(imageDescription);
            default -> this.markupOut.append("<p>Month error!  Unknown token.</p>");
        }
    }

    /**
     * Put the month day in the markup output.
     *
     * @param data The token parameter, which is the wrapper html around the day text.
     */
    private void monthDayNames(String data) {
        int starIndex = data.indexOf('*'); // Where the day text should be placed in the wrapper markup.
        String pre = data.substring(0, starIndex); // Wrapper opening tag.
        String post = data.substring(starIndex + 1, data.length()); // Wrapper closing tag.

        // Output the days of the week in the order that we have set.
        Iterator<Integer> daysIt = this.days.iterator();
        Integer current;
        while (daysIt.hasNext()) {
            current = daysIt.next();
            this.monthDay(this.getDayText(current), pre, post);
        }
    }

    /**
     * Output a day as an integer wrapped within the pre and post wrapper markup, the opening / closing tags.
     *
     * @param day The day.
     * @param pre The opening wrapper tag.
     * @param post The closing wrapper tag.
     */
    private void monthDay(Integer day, String pre, String post) {
        this.monthDay(day.toString(), pre, post);
    }

    /**
     * Output a day as text wrapped within the pre and post wrapper markup, the opening / closing tags.
     *
     * @param day The day.
     * @param pre The opening wrapper tag.
     * @param post The closing wrapper tag.
     */
    private void monthDay(String day, String pre, String post) {
        this.markupOut.append(pre).append(day).append(post);
    }

    /**
     * Output the days in the month as weeks.
     *
     * @param data The week and day wrapper markup
     */
    private void monthWeek(String data) {
        int exlamationIndex = data.indexOf('!'); // The position of the week within its wrapper markup.
        int ampIndex = data.indexOf('&'); // The day parameter wrapper markup.
        int starIndex = data.indexOf('*'); // The position of the day within its wrapper markup.

        String weekPre = data.substring(0, exlamationIndex);
        String weekPost = data.substring(exlamationIndex + 1, ampIndex);

        String dayPre = data.substring(ampIndex + 1, starIndex);
        String dayPost = data.substring(starIndex + 1, data.length());

        int currentPosition = 1;
        boolean startDayReached = false;

        // Similar logic / structure to that of outputting the text, but this time we have to output the 'blank' days within the
        // loop so that it is withing the wrapper markup for the week.
        while (this.currentMonth == this.previousMonth) {
            this.markupOut.append(weekPre);

            if (startDayReached == false) {
                Iterator<Integer> daysIt = this.days.iterator();
                int monthStartPostion = this.gc.get(Calendar.DAY_OF_WEEK); // Day of the week that the month starts on.
                Integer current;

                while (daysIt.hasNext() && (startDayReached == false)) {
                    current = daysIt.next();
                    if (current == monthStartPostion) {
                        startDayReached = true;
                    } else {
                        currentPosition++;
                        this.monthDay("", dayPre, dayPost);
                    }
                }
            }

            while (currentPosition < 8) {
                if (this.currentMonth != this.previousMonth) {
                    if (currentPosition != 1) {
                        while (currentPosition < 8) {
                            this.monthDay("", dayPre, dayPost);
                            currentPosition++;
                        }
                    }
                } else {
                    this.monthDay(this.gc.get(Calendar.DAY_OF_MONTH), dayPre, dayPost);
                    this.gc.add(Calendar.DAY_OF_MONTH, 1);
                    this.currentMonth = this.gc.get(Calendar.MONTH);

                    currentPosition++;
                }
            }
            currentPosition = 1;

            this.markupOut.append(weekPost);
        }
    }

    /**
     * Output the image name / description in the place of its token, no wrapper here.
     *
     * @param text The text to use, if 'null' then don't output otherwise "null" will appear in the markup!
     */
    private void monthImage(String text) {
        if (text != null) {
            this.markupOut.append(text);
        }
    }

    /**
     * Given the month as a number return its string representation.
     *
     * @param theMonth The month.
     * @return The name of the month.
     */
    private String getMonthText(int theMonth) {
        return switch (theMonth) {
            case Calendar.JANUARY -> "January";
            case Calendar.FEBRUARY -> "February";
            case Calendar.MARCH -> "March";
            case Calendar.APRIL -> "April";
            case Calendar.MAY -> "May";
            case Calendar.JUNE -> "June";
            case Calendar.JULY -> "July";
            case Calendar.AUGUST -> "August";
            case Calendar.SEPTEMBER -> "September";
            case Calendar.OCTOBER -> "October";
            case Calendar.NOVEMBER -> "November";
            case Calendar.DECEMBER -> "December";
            default -> "Unknown";
        };
    }

    /**
     * Given the day as a number return its string representation.
     *
     * @param theDay The day.
     * @return The name of the day.
     */
    private String getDayText (int theDay) {
        return switch (theDay) {
            case Calendar.SUNDAY -> "Sun";
            case Calendar.MONDAY -> "Mon";
            case Calendar.TUESDAY -> "Tue";
            case Calendar.WEDNESDAY -> "Wed";
            case Calendar.THURSDAY -> "Thu";
            case Calendar.FRIDAY -> "Fri";
            case Calendar.SATURDAY -> "Sat";
            default -> "Unknown";
        };
    }
}

What exactly is going on? Well, firstly there is the textual output of the calendar. Its purpose is to show that the logic is working, a means of developing the more complex HTML markup output as it evolved from the original code, and can be copied and used in other documents:

Original calendar output.

Secondly is the creation of the HTML markup file that is rendered by a web browser. This employs two custom ‘template’ files that represent the calendar and the months within. The syntax of the ‘tokens’ that the code replaces with data is based upon ‘Mustache’ but is bespoke for this project. Like Mustache and indeed PHP, but not so complex, we are pre-processing HTML which contains our own syntax into a form that is completely HTML that the web browser understands.

If we look at the first template, ‘CalendarTemplate.txt’:

<!doctype html>
<html>
    <head>
        <style type="text/css">
            .cal-row {
                display: flex; 
                flex-wrap: wrap;
                justify-content: 
                space-evenly; margin: 10px;
            }

            .cal-row.nowrap {
                flex-wrap: nowrap;
            }

            .cal-1 {
                flex-basis: 100%;
            }

            .cal-4 {
                flex-basis: 25%;
            }
    
            .cal-7 {
                flex-basis: 14.28%;
                margin-left: 2px;
                margin-right: 2px;
            }

            .cal-7:first-child {
                margin-left: 0;
            }

            .cal-7:first-child {
                margin-right: 0;
            }

            .cal-img {
                height: auto;
                max-width: 100%; 
            }

            .monthdayname,
            .monthday {
                text-align: end;
            }

            h1,
           .monthtitle {
                text-align: center;
            }

            * {
                font-family: sans-serif;
            }
        </style>
        <title>{{calendartitle}}</title>
    </head>
    <body>
        <h1>{{calendartitle}}</h1>
        <div class="cal-row">
            {{jan-Jan_760D_3389_sRGB.webp&Female mallard duck on ice}}
            {{feb-Feb_760D_0509_sRGB.webp&Pigeon looking at the camera standing on a fence}}
            {{mar-Mar_760D_5005_sRGB.webp&Cygnet in the sun}}
            {{apr-Apr_760D_8255_sRGB.webp&Mallard duckling}}
            {{may-May_760D_4222_sRGB.webp&Midland Pullman Train, Class 43 version}}
            {{jun-Jun_760D_4905_sRGB.webp&Beer beach, Devon}}
            {{jul-Jul_760D_4809_sRGB.webp&Moorhen}}
            {{aug-Aug_760D_6863_sRGB.webp&Male mallard duck}}
            {{sep-Sep_760D_4164_sRGB.webp&Great Western Railway Class 43, 43198, Driver Stan Martin 25th June 1950 - 6th November 2004}}
            {{oct-Oct_760D_0803_sRGB.webp&White swan looking at its reflection in the water}}
            {{nov-Nov_760D_3724_sRGB.webp&Chaffinch on a fence}}
            {{dec-Dec_760D_3366_sRGB.webp&Robin looking skywards}}
        </div>
    </body>
</html>

Then most of it is HTML that we are familiar with, until we come to a token. Like Mustache, I have employed the curly brackets ‘{{‘ and ‘}}’ as opening and closing indicators. Between the indicators is the token itself, which depending on which one it is can have one or two parameters.

If we look at ‘{{calendartitle}}’, then the markup will be sent to the output but the ‘{{calendartitle}}’ token will be replaced by the actual title of the calendar by the method ‘processCalendarToken’. Looking at ‘{{dec-Dec_760D_3366_sRGB.webp&Robin looking skywards}}’, then that is a month token that says to output December with the image file ‘Dec_760D_3366_sRGB.webp’ which has a description of ‘Robin looking skywards’ that will go in the ‘img’ tag ‘alt’ attribute for accessibility. This we can see in the ‘MonthTemplate.txt’:

<div class="cal-4 month">
    <div class="cal-row nowrap monthheader">
        <div class="cal-1 monthtitle">{{monthtitle}}</div>
    </div>
    <div class="cal-row monthimagewrapper">
        <img class="cal-1 cal-img monthimage" src="{{monthimage}}" alt="{{monthimagedescription}}">
    </div>
    <div class="cal-row nowrap monthdaynames">
        {{monthdaynames-<div class="cal-7 monthdayname">*</div>}}
    </div>
    {{monthweek-<div class="cal-row nowrap monthweek">!</div>&<div class="cal-7 monthday">*</div>}}
</div>

We need to use this template as the month, just like the day, is repeated. But unlike the day, the output is a combination of lots of data that has its own wrapper markup. The processing of this template works in the same way as the calendar template.

Running

Get the code by retrieving the entire contents of the folder ‘CalGen’ (github.com/gjb2048/code/tree/master/Java/CalGen) or if you’re familiar with ‘Git’, then use ‘git clone https://github.com/gjb2048/code.git’ to clone my ‘code’ repository then go to the CalGen folder and run ‘javac CalGen.java’ to create the ‘class’ file, followed by ‘java CalGen’ to create the calendar, which all being well should produce the text version to the console:

CalGen compiled and running

And we’ll have a new file in the folder called ‘2023_Calendar.html’:

New 2023_Calendar.html file.

If your machine has a web browser installed or is running a web server, then open / copy the it (and the images) to where they can be served. I’m using a headless Raspberry Pi in this example, so I need to copy ‘2023_Calendar.html’ and the webp images to the Ubuntu virtual machine I’m using:

scp file transfer to the Raspberry Pi

Then we can open it in a web browser:

2023 Calendar

The code has not been written in a way that it is robust to user errors and as such if you modify the template files to use your own images and / or change the structure of the page then you need to be sure that you’ve done this correctly. Such as having the first day of the week to be Friday ‘this.gc.setFirstDayOfWeek(Calendar.FRIDAY);’ and the months to be ordered in columns and not rows:

        <div class="cal-row">
            {{jan-Jan_760D_3389_sRGB.webp&Female mallard duck on ice}}
            {{apr-Apr_760D_8255_sRGB.webp&Mallard duckling}}
            {{jul-Jul_760D_4809_sRGB.webp&Moorhen}}
            {{oct-Oct_760D_0803_sRGB.webp&White swan looking at its reflection in the water}}
            {{feb-Feb_760D_0509_sRGB.webp&Pigeon looking at the camera standing on a fence}}
            {{may-May_760D_4222_sRGB.webp&Midland Pullman Train, Class 43 version}}
            {{aug-Aug_760D_6863_sRGB.webp&Male mallard duck}}
            {{nov-Nov_760D_3724_sRGB.webp&Chaffinch on a fence}}
            {{mar-Mar_760D_5005_sRGB.webp&Cygnet in the sun}}
            {{jun-Jun_760D_4905_sRGB.webp&Beer beach, Devon}}
            {{sep-Sep_760D_4164_sRGB.webp&Great Western Railway Class 43, 43198, Driver Stan Martin 25th June 1950 - 6th November 2004}}
            {{dec-Dec_760D_3366_sRGB.webp&Robin looking skywards}}
        </div>

giving:

Changed 2023 Calendar

But the output of the text will be the same as it’s month order has not been changed. How do you think the code could be modified to achieve this?

A few extra thoughts.

I’ve written the code in such a way that there is only one place to change for each concept, i.e. only one place to state what day of the week the month starts on.

Development can take time and hit the odd brick wall, such as making the code work with any start day. There is a flow to the development, where I’ve had an intermediate step of generating the markup within the code with no tokens and adding the data in place. If you want to see all of the stages, then do look at the history of the code: github.com/gjb2048/code/commits/master/Java/CalGen/CalGen.java and github.com/gjb2048/code/commits/456dbcd937349fc094357e66a64b569dd9ec119c/Java/CalGen/src/CalGen.java?browsing_rename_history=true&new_path=Java/CalGen/CalGen.java&original_branch=master.

Don’t give up when hitting a brick wall during development but ask yourself ‘Why does this not work?’ and walk away from the screen for a while. With the start day issue, I discovered that the array index calculations weren’t flexible enough to cope with multiple day differences. Perhaps its because I’m not that mathematical. After a pause, I thought about the problem again and realised that I needed to get the days of the week in the order I wanted first, with their associated index. Then the code at the start of each month, iterate from the first day of the week to the start day, because now it was working with a list it could traverse regardless of the index number (was being used for the previous array data type). And when the end of the month occurred, then just fill in the blanks for the remaining days.

Conclusion

In this post, we’ve moved on from my previous Java posts to introduce the use of additional files, both input and output, whilst building upon the concept of streams.

I hope you can now envisage in a different way how the concept of taking a static template, parsing it and adding data from a different source can be achieved from the ground up. This concept is essentially how Moodle works using PHP and JavaScript to generate the output (web page) from the static files and database data it has.

Do have a go and please do say what you think in the comments.

Gareth Barnard
Latest posts by Gareth Barnard (see all)
blank

Gareth Barnard

Gareth is a developer of numerous Moodle Themes including Essential (the most popular Moodle Theme ever), Foundation, and other plugins such as course formats, including Collapsed Topics.

2 thoughts on “Calendar

  • Wow – that is a very cool project – I like it !
    I wonder if it could be a Block in WordPress or even Moodle one day ?!

    Reply
    • Thanks Stuart,

      It possibly could be, the sticking point really is its use of the GregorianCalendar class which really helps to overcome issues of leap years etc. If such a class existed or could be written for PHP then it would be possible… humm, there is already the Calendar block… so perhaps just a matter of allowing the upload of images on a per month basis, just like the Grid format does with sections. Quite a bit of work! For now I’m very happy with it, and with what it demonstrates in terms of the fundamental beginnings of more complex systems like WordPress and Moodle, that follow the same dynamic ‘template’ / ‘output’ substitution pattern :).

Add a reply or comment...