Calendar

Monday, 16 January 2023

First posted on eLearningWorld.

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:

  1/*
  2 * CalGen.
  3 *
  4 * Generates the calendar for the year set, both as a HTML page from 'templated' and as text.
  5 *
  6 * Copyright (C) 2022 G J Barnard.
  7 *
  8 * This program is free software: you can redistribute it and/or modify
  9 * it under the terms of the GNU General Public License as published by
 10 * the Free Software Foundation, either version 3 of the License, or
 11 * (at your option) any later version.
 12 *
 13 * This program is distributed in the hope that it will be useful,
 14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 16 * GNU General Public License for more details.
 17 *
 18 * You should have received a copy of the GNU General Public License
 19 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 20 *
 21 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.
 22 */
 23
 24import java.io.FileNotFoundException;
 25import java.io.FileOutputStream;
 26import java.io.IOException;
 27import java.util.Calendar;
 28import java.util.Collections;
 29import java.util.GregorianCalendar;
 30import java.util.Iterator;
 31import java.util.LinkedList;
 32
 33/**
 34 * CalGen class.
 35 *
 36 * @copyright 2022 G J Barnard.
 37 */
 38public class CalGen {
 39
 40    // Calendar attributes.
 41    private final GregorianCalendar gc = new GregorianCalendar(); // The calenadar.
 42    private final LinkedList<Integer> months = new LinkedList<>(); // The days of the week.
 43    private final LinkedList<Integer> days = new LinkedList<>(); // The days of the week.
 44    private int currentMonth; // Keep track of the current month between methods.
 45    private int previousMonth; // Keep track of the previous month between methods.
 46    private final int theYear; // The year we are using.
 47
 48    // Template attributes.
 49    private char[] calendarTemplate = null; // The template for the calendar.
 50    private char[] monthTemplate = null; // The template of a month within the calendar.
 51
 52    private final char[] mpre = {'{', '{'}; // Template markup token start characters.
 53    private final char[] mpost = {'}', '}'}; // Template markup token end characters.
 54    private final FileOutputStream mout; // The stream for the markup html file.
 55    // Stores the markup html as its being generated before it is output to the file.
 56    private final StringBuffer markupOut = new StringBuffer();
 57
 58    /**
 59     * Create the calendar and generate both the text and markup versions.
 60     *
 61     * @param args the command line arguments - not used.
 62     * @throws java.io.FileNotFoundException If a template file cannot be found.
 63     * @throws java.io.IOException If a problem occurs when reading a template file.
 64     */
 65    public static void main(String args[]) throws FileNotFoundException, IOException {
 66        CalGen us = new CalGen();
 67
 68        us.calendar();
 69        us.calendarTemplate();
 70    }
 71
 72    /**
 73     * Constructor.
 74     *
 75     * @throws FileNotFoundException If a template file cannot be found.
 76     */
 77    public CalGen() throws FileNotFoundException {
 78        this.gc.setFirstDayOfWeek(Calendar.TUESDAY); // Change to another day if wished.
 79        this.theYear = 2023;
 80
 81        // The name of the markup file in the current directory.
 82        this.mout = new FileOutputStream("./" + this.theYear + "_Calendar.html");
 83
 84        // Add the months in the order we wish to output them as text.
 85        months.add(Calendar.JANUARY);
 86        months.add(Calendar.FEBRUARY);
 87        months.add(Calendar.MARCH);
 88        months.add(Calendar.APRIL);
 89        months.add(Calendar.MAY);
 90        months.add(Calendar.JUNE);
 91        months.add(Calendar.JULY);
 92        months.add(Calendar.AUGUST);
 93        months.add(Calendar.SEPTEMBER);
 94        months.add(Calendar.OCTOBER);
 95        months.add(Calendar.NOVEMBER);
 96        months.add(Calendar.DECEMBER);
 97
 98        // Add the days in the order used by default in the GregorianCalendar.
 99        days.add(Calendar.SUNDAY);
100        days.add(Calendar.MONDAY);
101        days.add(Calendar.TUESDAY);
102        days.add(Calendar.WEDNESDAY);
103        days.add(Calendar.THURSDAY);
104        days.add(Calendar.FRIDAY);
105        days.add(Calendar.SATURDAY);
106
107        // Rotate the days around if needed so that the start day is first in the list.
108        Iterator<Integer> daysIt = days.iterator(); // The means of iterating over our list.
109        boolean found = false; // Have we found the day we are looking for?
110        int count = 0; // The number of positions the day we are looking for is away from Sunday.
111        Integer current; // The reference to the current day.
112        int firstDayOfWeek = this.gc.getFirstDayOfWeek(); // The day that the calendar has been set to be the first day of the week.
113
114        // While we have another day to check and we've not found the day we are looking for.
115        while (daysIt.hasNext() && found == false) {
116            current = daysIt.next(); // Get the next day.
117            if (current == firstDayOfWeek) { // Have we found the day we are looking for?
118                found = true; // Yes.
119            } else {
120                count++; // Increment the position.
121            }
122        }
123
124        if (count > 0) { // The day we are looking for is not Sunday, but is 'count' positions away from it.
125            // Rotate the list to the left by the number of positions we have calculated.
126            // The day we are looking for will then be the first.
127            Collections.rotate(days, -count);
128        }
129    }
130
131    /**
132     * Generate the text version of the calendar.
133     */
134    public void calendar() {
135        this.gc.set(theYear, 0, 1); // Set to the 1st January for the year we want.
136
137        // Output the year.
138        System.out.println(this.gc.get(Calendar.YEAR));
139
140        // Output the months.
141        Iterator<Integer> mit = this.months.iterator();
142        while (mit.hasNext()) {
143            this.month(mit.next());
144            System.out.println();
145        }
146    }
147
148    /**
149     * Output the month.
150     *
151     * @param theMonth The month to output.
152     */
153    private void month(int theMonth) {
154        this.gc.set(Calendar.MONTH, theMonth); // Tell the calendar the month we wish to use.
155        // Set both months to be the same so that we can detect when the current changes.
156        this.currentMonth = theMonth;
157        this.previousMonth = theMonth;
158
159        System.out.println(this.getMonthText(gc.get(Calendar.MONTH))); // Output the month text.
160
161        // Output the day names.
162        Iterator<Integer> daysIt = this.days.iterator();
163        Integer current;
164        while (daysIt.hasNext()) {
165            current = daysIt.next();
166            this.day(this.getDayText(current));
167            if (daysIt.hasNext()) {
168                System.out.print(" ");
169            } else {
170                System.out.println();
171            }
172        }
173
174        // Output the 'blank days' before the day on which the 1st of the month is.
175        // The current 'position' of the day in the week we are outputing, so '1' is the first day of the week.
176        int currentPosition = 1;
177        daysIt = this.days.iterator();
178        boolean startDayReached = false; // Have we found the start day?
179        int monthStartPostion = this.gc.get(Calendar.DAY_OF_WEEK); // Day of the week that the month starts on.
180
181        while (daysIt.hasNext() && (startDayReached == false)) {
182            current = daysIt.next();
183            if (current == monthStartPostion) {
184                startDayReached = true;
185            } else {
186                currentPosition++;
187                this.day("");
188                System.out.print("    ");
189            }
190        }
191
192        // Loop until we have reached the next month.
193        while (this.currentMonth == this.previousMonth) {
194
195            // Loop through the day 'positions' as we have outputted them with the day names.
196            while (currentPosition < 8) {
197
198                // Have we reached the next month?
199                if (this.currentMonth != this.previousMonth) {
200                    // Are we on a week that has been started but not finished?
201                    if (currentPosition != 1) {
202                        // Loop through the remaining positions and output 'blank' days.
203                        while (currentPosition < 8) {
204                            this.day("");
205                            System.out.print("    ");
206                            currentPosition++;
207                        }
208                    }
209                } else {
210                    // Output the day.
211                    if (this.gc.get(Calendar.DAY_OF_MONTH) > 9) { // Get the prefixing spacing correct.
212                        System.out.print(" ");
213                    } else {
214                        System.out.print("  ");
215                    }
216                    this.day(this.gc.get(Calendar.DAY_OF_MONTH)); // The day.
217                    if (currentPosition < 7) {
218                        System.out.print(" "); // Postfix space.
219                    }
220
221                    // Get the next day.
222                    this.gc.add(Calendar.DAY_OF_MONTH, 1);
223                    this.currentMonth = this.gc.get(Calendar.MONTH);
224
225                    currentPosition++; // The next position in the week.
226                }
227            }
228            currentPosition = 1; // Reset to the next week.
229            System.out.println();
230        }
231    }
232
233    /**
234     * Output the day.
235     * @param day As an integer.
236     */
237    private void day(Integer day) {
238        this.day(day.toString());
239    }
240
241    /**
242     * Output the day.
243     *
244     * @param day As a string.
245     */
246    private void day(String day) {
247        System.out.print(day);
248    }
249
250    /**
251     * Generate the markup version of the calendar.
252     *
253     * @throws FileNotFoundException If a template file cannot be found.
254     * @throws IOException If a problem occurs when reading a template file.
255     */
256    public void calendarTemplate() throws FileNotFoundException, IOException {
257        this.gc.set(theYear, 0, 1); // Reset to the 1st January for the year we want.
258
259        this.loadTemplates(); // Load the templates.
260
261        // Process the calendar template.
262        int currentIndex = 0; // Index of the current character.
263        while (currentIndex < this.calendarTemplate.length) {
264            if ((this.calendarTemplate[currentIndex] == this.mpre[0]) &&
265                (this.calendarTemplate[currentIndex + 1]) == this.mpre[1]) {
266                // Start token.
267                currentIndex = currentIndex + 2; // Jump over the token start characters.
268                currentIndex = this.processCalendarToken(currentIndex); // Process the token.
269            } else {
270                // Pass through.
271                this.markupOut.append(this.calendarTemplate[currentIndex]); // Copy the character to the output.
272                currentIndex++; // Get the next character.
273            }
274        }
275
276        this.mout.write(this.markupOut.toString().getBytes()); // Write the markup to the output file.
277        this.mout.close(); // Close the file.
278    }
279
280    /**
281     * Load the templates.
282     *
283     * Ref: https://stackoverflow.com/questions/21980090/javas-randomaccessfile-eofexception
284     *
285     * @throws FileNotFoundException If a template file cannot be found.
286     * @throws IOException If a problem occurs when reading a template file.
287     */
288    private void loadTemplates() throws FileNotFoundException, IOException {
289        // Calendar template.
290        java.io.File templateFile = new java.io.File("CalendarTemplate.txt"); // Using the File object so that we can get the length.
291
292        char[] buffer = new char[(int) templateFile.length()]; // Buffer to store the read characters.
293
294        java.io.FileInputStream fin = new java.io.FileInputStream(templateFile); // Stream to read the file.
295        // Reader to read the file that is encoded with UTF-8 characters.
296        java.io.InputStreamReader isr = new java.io.InputStreamReader(fin, "UTF-8");
297
298        isr.read(buffer); // Read the file into the buffer.
299        isr.close(); // Close the file.
300
301        // To allow us to convert the bytes into characters.
302        StringBuilder sb = new StringBuilder((int) templateFile.length());
303        sb.append(buffer);
304
305        this.calendarTemplate = sb.toString().toCharArray(); // Convert into a string and then an array of chars.
306
307        // Month template.
308        // Same processing as the Calendar Template.
309        templateFile = new java.io.File("MonthTemplate.txt");
310        buffer = new char[(int) templateFile.length()];
311
312        fin = new java.io.FileInputStream(templateFile);
313        isr = new java.io.InputStreamReader(fin, "UTF-8");
314
315        isr.read(buffer);
316        isr.close();
317
318        sb = new StringBuilder((int) templateFile.length());
319        sb.append(buffer);
320
321        this.monthTemplate = sb.toString().toCharArray();
322    }
323
324    /**
325     * Process a token in the Calendar template.
326     *
327     * @param currentIndex The current character in the calendar template.
328     * @return The updated position in the template after processing the token so that we can continue.
329     */
330    private int processCalendarToken(int currentIndex) {
331        int end = this.calendarTemplate.length;
332        StringBuilder token = new StringBuilder();
333
334        // Extract the whole token until the end token characters are reached/
335        while (currentIndex < end) {
336            if ((this.calendarTemplate[currentIndex] == this.mpost[0]) &&
337                (this.calendarTemplate[currentIndex + 1]) == this.mpost[1]) {
338                // End token.
339                currentIndex = currentIndex + 2;
340                end = currentIndex; // Exit the loop.
341            } else {
342                // Characters of the token.
343                token.append(this.calendarTemplate[currentIndex]);
344                currentIndex++;
345            }
346        }
347        this.processCalendarToken(token.toString()); // Process the token.
348
349        return currentIndex;
350    }
351
352    /**
353     * Process the extracted token.
354     *
355     * @param token The token to process.
356     */
357    private void processCalendarToken(String token) {
358        int dataIndex = token.indexOf('-'); // Do we have 'parameter' data in the token?
359        String data = null;
360        String dataExtra = null;
361        if (dataIndex != -1) {
362            // We have data, so extract it.
363            data = token.substring(dataIndex + 1, token.length());
364            token = token.substring(0, dataIndex);
365
366            int ampIndex = data.indexOf('&'); // Do we have a second parameter data in the token?
367            if (ampIndex != -1) {
368                dataExtra = data.substring(ampIndex + 1, data.length());
369                data = data.substring(0, ampIndex);
370            }
371        }
372
373        // Identify and execute the action of the token with its data if any.
374        // Rule switch, Java 12 - https://blogs.oracle.com/javamagazine/post/new-switch-expressions-in-java-12
375        switch (token) {
376            case "calendartitle" -> this.markupOut.append(this.gc.get(Calendar.YEAR)).append(" Calendar");
377            case "jan" -> this.monthTemplate(Calendar.JANUARY, data, dataExtra);
378            case "feb" -> this.monthTemplate(Calendar.FEBRUARY, data, dataExtra);
379            case "mar" -> this.monthTemplate(Calendar.MARCH, data, dataExtra);
380            case "apr" -> this.monthTemplate(Calendar.APRIL, data, dataExtra);
381            case "may" -> this.monthTemplate(Calendar.MAY, data, dataExtra);
382            case "jun" -> this.monthTemplate(Calendar.JUNE, data, dataExtra);
383            case "jul" -> this.monthTemplate(Calendar.JULY, data, dataExtra);
384            case "aug" -> this.monthTemplate(Calendar.AUGUST, data, dataExtra);
385            case "sep" -> this.monthTemplate(Calendar.SEPTEMBER, data, dataExtra);
386            case "oct" -> this.monthTemplate(Calendar.OCTOBER, data, dataExtra);
387            case "nov" -> this.monthTemplate(Calendar.NOVEMBER, data, dataExtra);
388            case "dec" -> this.monthTemplate(Calendar.DECEMBER, data, dataExtra);
389            default -> this.markupOut.append("<p>Calendar error!  Unknown token.</p>");
390        }
391    }
392
393    /**
394     * Execute a month token by processing the month template with the supplied parameters.
395     *
396     * @param theMonth The month.
397     * @param imageName The name of the image for the month.
398     * @param imageDescription The description of the image for the month.
399     */
400    private void monthTemplate(int theMonth, String imageName, String imageDescription) {
401        this.gc.set(Calendar.MONTH, theMonth); // Tell the calendar the month we want so that it tells us the correct days.
402        this.previousMonth = theMonth;
403        this.currentMonth = theMonth;
404
405        int currentIndex = 0; // Current character in the month template.
406        while (currentIndex < this.monthTemplate.length) {
407            if ((this.monthTemplate[currentIndex] == this.mpre[0]) && (this.monthTemplate[currentIndex + 1]) == this.mpre[1]) {
408                // Start token.
409                currentIndex = currentIndex + 2;
410                currentIndex = this.processMonthToken(currentIndex, imageName, imageDescription);
411            } else {
412                // Pass through.
413                this.markupOut.append(this.monthTemplate[currentIndex]);
414                currentIndex++;
415            }
416        }
417    }
418
419    /**
420     * Execute a month token.
421     *
422     * @param currentIndex The current character in the month template.
423     * @param imageName The name of the image for the month.
424     * @param imageDescription The description of the image for the month.
425     */
426    private int processMonthToken(int currentIndex, String imageName, String imageDescription) {
427        int end = this.monthTemplate.length;
428        StringBuilder token = new StringBuilder();
429        while (currentIndex < end) {
430            if ((this.monthTemplate[currentIndex] == this.mpost[0]) && (this.monthTemplate[currentIndex + 1]) == this.mpost[1]) {
431                // End token.
432                currentIndex = currentIndex + 2;
433                end = currentIndex; // Exit the loop.
434            } else {
435                // Characters of the token.
436                token.append(this.monthTemplate[currentIndex]);
437                currentIndex++;
438            }
439        }
440        this.processMonthToken(token.toString(), imageName, imageDescription);
441
442        return currentIndex;
443    }
444
445    /**
446     * Process a month token.
447     *
448     * @param token The month token.
449     * @param imageName The name of the image for the month.
450     * @param imageDescription The description of the image for the month.
451     */
452    private void processMonthToken(String token, String imageName, String imageDescription) {
453        int dataIndex = token.indexOf('-');
454        String data = null;
455        if (dataIndex != -1) {
456            // We have data.
457            data = token.substring(dataIndex + 1, token.length());
458            token = token.substring(0, dataIndex);
459        }
460
461        switch (token) {
462            case "monthtitle" -> this.markupOut.append(this.getMonthText(gc.get(Calendar.MONTH)));
463            case "monthdaynames" -> this.monthDayNames(data);
464            case "monthweek" -> this.monthWeek(data);
465            case "monthimage" -> this.monthImage(imageName);
466            case "monthimagedescription" -> this.monthImage(imageDescription);
467            default -> this.markupOut.append("<p>Month error!  Unknown token.</p>");
468        }
469    }
470
471    /**
472     * Put the month day in the markup output.
473     *
474     * @param data The token parameter, which is the wrapper html around the day text.
475     */
476    private void monthDayNames(String data) {
477        int starIndex = data.indexOf('*'); // Where the day text should be placed in the wrapper markup.
478        String pre = data.substring(0, starIndex); // Wrapper opening tag.
479        String post = data.substring(starIndex + 1, data.length()); // Wrapper closing tag.
480
481        // Output the days of the week in the order that we have set.
482        Iterator<Integer> daysIt = this.days.iterator();
483        Integer current;
484        while (daysIt.hasNext()) {
485            current = daysIt.next();
486            this.monthDay(this.getDayText(current), pre, post);
487        }
488    }
489
490    /**
491     * Output a day as an integer wrapped within the pre and post wrapper markup, the opening / closing tags.
492     *
493     * @param day The day.
494     * @param pre The opening wrapper tag.
495     * @param post The closing wrapper tag.
496     */
497    private void monthDay(Integer day, String pre, String post) {
498        this.monthDay(day.toString(), pre, post);
499    }
500
501    /**
502     * Output a day as text wrapped within the pre and post wrapper markup, the opening / closing tags.
503     *
504     * @param day The day.
505     * @param pre The opening wrapper tag.
506     * @param post The closing wrapper tag.
507     */
508    private void monthDay(String day, String pre, String post) {
509        this.markupOut.append(pre).append(day).append(post);
510    }
511
512    /**
513     * Output the days in the month as weeks.
514     *
515     * @param data The week and day wrapper markup
516     */
517    private void monthWeek(String data) {
518        int exlamationIndex = data.indexOf('!'); // The position of the week within its wrapper markup.
519        int ampIndex = data.indexOf('&'); // The day parameter wrapper markup.
520        int starIndex = data.indexOf('*'); // The position of the day within its wrapper markup.
521
522        String weekPre = data.substring(0, exlamationIndex);
523        String weekPost = data.substring(exlamationIndex + 1, ampIndex);
524
525        String dayPre = data.substring(ampIndex + 1, starIndex);
526        String dayPost = data.substring(starIndex + 1, data.length());
527
528        int currentPosition = 1;
529        boolean startDayReached = false;
530
531        // Similar logic / structure to that of outputting the text, but this time we have to output the 'blank' days within the
532        // loop so that it is withing the wrapper markup for the week.
533        while (this.currentMonth == this.previousMonth) {
534            this.markupOut.append(weekPre);
535
536            if (startDayReached == false) {
537                Iterator<Integer> daysIt = this.days.iterator();
538                int monthStartPostion = this.gc.get(Calendar.DAY_OF_WEEK); // Day of the week that the month starts on.
539                Integer current;
540
541                while (daysIt.hasNext() && (startDayReached == false)) {
542                    current = daysIt.next();
543                    if (current == monthStartPostion) {
544                        startDayReached = true;
545                    } else {
546                        currentPosition++;
547                        this.monthDay("", dayPre, dayPost);
548                    }
549                }
550            }
551
552            while (currentPosition < 8) {
553                if (this.currentMonth != this.previousMonth) {
554                    if (currentPosition != 1) {
555                        while (currentPosition < 8) {
556                            this.monthDay("", dayPre, dayPost);
557                            currentPosition++;
558                        }
559                    }
560                } else {
561                    this.monthDay(this.gc.get(Calendar.DAY_OF_MONTH), dayPre, dayPost);
562                    this.gc.add(Calendar.DAY_OF_MONTH, 1);
563                    this.currentMonth = this.gc.get(Calendar.MONTH);
564
565                    currentPosition++;
566                }
567            }
568            currentPosition = 1;
569
570            this.markupOut.append(weekPost);
571        }
572    }
573
574    /**
575     * Output the image name / description in the place of its token, no wrapper here.
576     *
577     * @param text The text to use, if 'null' then don't output otherwise "null" will appear in the markup!
578     */
579    private void monthImage(String text) {
580        if (text != null) {
581            this.markupOut.append(text);
582        }
583    }
584
585    /**
586     * Given the month as a number return its string representation.
587     *
588     * @param theMonth The month.
589     * @return The name of the month.
590     */
591    private String getMonthText(int theMonth) {
592        return switch (theMonth) {
593            case Calendar.JANUARY -> "January";
594            case Calendar.FEBRUARY -> "February";
595            case Calendar.MARCH -> "March";
596            case Calendar.APRIL -> "April";
597            case Calendar.MAY -> "May";
598            case Calendar.JUNE -> "June";
599            case Calendar.JULY -> "July";
600            case Calendar.AUGUST -> "August";
601            case Calendar.SEPTEMBER -> "September";
602            case Calendar.OCTOBER -> "October";
603            case Calendar.NOVEMBER -> "November";
604            case Calendar.DECEMBER -> "December";
605            default -> "Unknown";
606        };
607    }
608
609    /**
610     * Given the day as a number return its string representation.
611     *
612     * @param theDay The day.
613     * @return The name of the day.
614     */
615    private String getDayText (int theDay) {
616        return switch (theDay) {
617            case Calendar.SUNDAY -> "Sun";
618            case Calendar.MONDAY -> "Mon";
619            case Calendar.TUESDAY -> "Tue";
620            case Calendar.WEDNESDAY -> "Wed";
621            case Calendar.THURSDAY -> "Thu";
622            case Calendar.FRIDAY -> "Fri";
623            case Calendar.SATURDAY -> "Sat";
624            default -> "Unknown";
625        };
626    }
627}

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’:

 1<!doctype html>
 2<html>
 3    <head>
 4        <style type="text/css">
 5            .cal-row {
 6                display: flex; 
 7                flex-wrap: wrap;
 8                justify-content: 
 9                space-evenly; margin: 10px;
10            }
11
12            .cal-row.nowrap {
13                flex-wrap: nowrap;
14            }
15
16            .cal-1 {
17                flex-basis: 100%;
18            }
19
20            .cal-4 {
21                flex-basis: 25%;
22            }
23    
24            .cal-7 {
25                flex-basis: 14.28%;
26                margin-left: 2px;
27                margin-right: 2px;
28            }
29
30            .cal-7:first-child {
31                margin-left: 0;
32            }
33
34            .cal-7:first-child {
35                margin-right: 0;
36            }
37
38            .cal-img {
39                height: auto;
40                max-width: 100%; 
41            }
42
43            .monthdayname,
44            .monthday {
45                text-align: end;
46            }
47
48            h1,
49           .monthtitle {
50                text-align: center;
51            }
52
53            * {
54                font-family: sans-serif;
55            }
56        </style>
57        <title>{{calendartitle}}</title>
58    </head>
59    <body>
60        <h1>{{calendartitle}}</h1>
61        <div class="cal-row">
62            {{jan-Jan_760D_3389_sRGB.webp&Female mallard duck on ice}}
63            {{feb-Feb_760D_0509_sRGB.webp&Pigeon looking at the camera standing on a fence}}
64            {{mar-Mar_760D_5005_sRGB.webp&Cygnet in the sun}}
65            {{apr-Apr_760D_8255_sRGB.webp&Mallard duckling}}
66            {{may-May_760D_4222_sRGB.webp&Midland Pullman Train, Class 43 version}}
67            {{jun-Jun_760D_4905_sRGB.webp&Beer beach, Devon}}
68            {{jul-Jul_760D_4809_sRGB.webp&Moorhen}}
69            {{aug-Aug_760D_6863_sRGB.webp&Male mallard duck}}
70            {{sep-Sep_760D_4164_sRGB.webp&Great Western Railway Class 43, 43198, Driver Stan Martin 25th June 1950 - 6th November 2004}}
71            {{oct-Oct_760D_0803_sRGB.webp&White swan looking at its reflection in the water}}
72            {{nov-Nov_760D_3724_sRGB.webp&Chaffinch on a fence}}
73            {{dec-Dec_760D_3366_sRGB.webp&Robin looking skywards}}
74        </div>
75    </body>
76</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 ‘<title>{{calendartitle}}</title>’, 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’:

 1<div class="cal-4 month">
 2    <div class="cal-row nowrap monthheader">
 3        <div class="cal-1 monthtitle">{{monthtitle}}</div>
 4    </div>
 5    <div class="cal-row monthimagewrapper">
 6        <img class="cal-1 cal-img monthimage" src="{{monthimage}}" alt="{{monthimagedescription}}">
 7    </div>
 8    <div class="cal-row nowrap monthdaynames">
 9        {{monthdaynames-<div class="cal-7 monthdayname">*</div>}}
10    </div>
11    {{monthweek-<div class="cal-row nowrap monthweek">!</div>&<div class="cal-7 monthday">*</div>}}
12</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 ’ 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:

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

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.