5
\$\begingroup\$

I wanted to write a utility method for learning purposes to determine if a given date (year, month and day) falls within EU summer time or not, without using any of the Java library methods. Is here something (logically) to simplify or is it an antipattern?

import java.util.Arrays;

/**
 * Die mitteleuropäische Sommerzeit beginnt am letzten Sonntag im März um 2:00 Uhr MEZ,
 * indem die Stundenzählung um eine Stunde von 2:00 Uhr auf 3:00 Uhr vorgestellt wird.
 * Sie endet jeweils am letzten Sonntag im Oktober um 3:00 Uhr MESZ, indem die Stundenzählung
 * um eine Stunde von 3:00 Uhr auf 2:00 Uhr zurückgestellt wird.
 * s. <a href="https://de.wikipedia.org/wiki/Sommerzeit">...</a>
 */
public class SummertimeEU {
    /**
     * Calculates the weekday (1-7) given a date in the format year-month-day.
     *
     * @param year  the year of the date
     * @param month the month of the date
     * @param day   the day of the date
     * @return the weekday (1-7) of the given date
     */
    public static int weekdayFromDate(int year, int month, int day) {
        if (month < 3) {
            year--;
            month += 12;
        }
        int wd = (day + 2 * month + (3 * month + 3) / 5 + year + year / 4 - year / 100 + year / 400 + 1) % 7;
        // return new int[] {7, 1, 2, 3, 4, 5, 6}[wd];
        return (wd + 6) % 7 + 1;
    }


    /**
     * Determines if a given year is a leap year.
     *
     * @param year the year to be checked
     * @return true if the year is a leap year, false otherwise
     */
    public static boolean isLeapYear(int year) {
        return year % 4 == 0 && year % 100 != 0 || year % 400 == 0;
    }

    /**
     * Calculates the day of the year based on the given year, month, and day.
     *
     * @param year  the year
     * @param month the month (1-12)
     * @param day   the day (1-31)
     * @return the day of the year (1-365)
     */
    public static int dayOfYearFromDate(int year, int month, int day) {
        int[] days = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
        int sum = 0;
        for (int i = 0; i < month - 1; i++) {
            sum += days[i];
        }
        if (isLeapYear(year) && month > 2) {
            sum++;
        }
        return sum + day;
    }

    /**
     * Generates an array containing the date (year, month, day) based on the given year and day of the year.
     *
     * @param year      the year for which the date is generated
     * @param dayOfYear the day of the year for which the date is generated
     * @return an array containing the date in the format [year, month, day]
     */
    public static int[] dateFromDayOfYear(int year, int dayOfYear) {
        int[] days = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
        if (isLeapYear(year)) {
            days[1] = 29;
        }
        int[] result = {year, 1, 0};
        for (int i = 0; i < 12; i++) {
            if (dayOfYear > days[i]) {
                dayOfYear -= days[i];
                result[1]++;
            }
        }
        result[2] = dayOfYear;
        return result;
    }

    public static int dateOfLastSundayInMarch(int year) {
        int march = dayOfYearFromDate(year, 3, 31);
        int wd = weekdayFromDate(year, 3, 31);
        return march - (wd % 7);
    }

    public static int dateOfLastSundayInOctober(int year) {
        int october = dayOfYearFromDate(year, 10, 31);
        int wd = weekdayFromDate(year, 10, 31);
        return october - (wd % 7);
    }

    public static boolean isSummertimeAt(int year, int month, int day) {
        int date = dayOfYearFromDate(year, month, day);
        int dateOfLastSundayInMarch = dateOfLastSundayInMarch(year);
        int dateOfLastSundayInOctober = dateOfLastSundayInOctober(year);
        return date == dateOfLastSundayInMarch || date > dateOfLastSundayInMarch && date < dateOfLastSundayInOctober;
    }

    public static void main(String[] args) {
        // Examples:
        System.out.println(weekdayFromDate(2023, 1, 1)); // Sunday = 7
        System.out.println(weekdayFromDate(2023, 1, 2)); // Monday = 1
        System.out.println(weekdayFromDate(2023, 1, 3)); // Tuesday = 2
        System.out.println(weekdayFromDate(2023, 1, 4)); // Wednesday = 3
        System.out.println(weekdayFromDate(2023, 1, 5)); // Thursday = 4
        System.out.println(weekdayFromDate(2023, 1, 6)); // Friday = 5
        System.out.println(weekdayFromDate(2023, 1, 7)); // Saturday = 6
        System.out.println(weekdayFromDate(2023, 1, 8)); // Sunday = 7
        System.out.println(weekdayFromDate(2023, 1, 9)); // Monday = 1

        System.out.println(isSummertimeAt(2023, 1, 1)); // false
        System.out.println(isSummertimeAt(2023, 12, 31)); // false
        System.out.println(isSummertimeAt(2023, 3, 25)); // false
        System.out.println();
        System.out.println(isSummertimeAt(2023, 3, 26)); // true
        System.out.println(isSummertimeAt(2023, 6, 1)); // true
        System.out.println(isSummertimeAt(2023, 10, 28)); // true
        System.out.println();
        System.out.println(isSummertimeAt(2023, 10, 29)); // false

        System.out.println(Arrays.toString(dateFromDayOfYear(2024, 60))); // {2024, 2, 29}
        System.out.println(Arrays.toString(dateFromDayOfYear(2024, 61))); // {2024, 3,  1}
    }
}
\$\endgroup\$
7
  • 4
    \$\begingroup\$ A println() of Jan. 1st is entertaining and all. But automated tests always "know the right answer", so they can display Red / Green bar results. Recommend you code these up as JUnit tests. \$\endgroup\$
    – J_H
    Commented Oct 22, 2023 at 19:04
  • \$\begingroup\$ @J_H I assume that the functionality is correct. \$\endgroup\$ Commented Oct 22, 2023 at 19:07
  • 3
    \$\begingroup\$ Surely you have watched Tom Scott's YouTube video about time zones? The answer to this question is in the end. :) I can't recommend it enough, just the entertainment value alone is worth it. \$\endgroup\$ Commented Oct 23, 2023 at 6:49
  • 8
    \$\begingroup\$ Obviously not: it’s currently Western Indonesian Time in Java. \$\endgroup\$ Commented Oct 23, 2023 at 10:05
  • 2
    \$\begingroup\$ Link to the Tom Scott video: The Problem with Time & Timezones - Computerphile.  (Most Tom Scott videos are well worth seeing, but this one is especially good.) \$\endgroup\$
    – gidds
    Commented Oct 23, 2023 at 16:40

1 Answer 1

12
\$\begingroup\$

determine if a given date (year, month and day) falls within EU summer time or not

This is already problematic for a few reasons:

  • Year, month and day... at what time? Daylight savings changes at a well-defined time (that usually isn't midnight), and so choosing an entire day is not well-defined.
  • Rather than "EU summer time", it seems you actually meant CET, but for most intents and purposes that's deprecated and you should define the timezone as e.g. Europe/Berlin.

I wanted to write a utility method for learning purposes [...] without using any of the Java library methods

is counter to learning. You should only try out learning code like this if you simultaneously endeavour to learn the actual API that should be used for this purpose, and compare the results of the two. There are actually multiple Java APIs that can do this, old java.util and newer java.time being options. This bears clarifying: there are good lessons and bad lessons to learn from this exercise. Good lessons to learn are:

  • Time is hard.
  • Date and time manipulation routines should basically never be written by hand.
  • Tests are good.

The wrong lessons to learn, and ones that I fear too many students (not necessarily you) may fall into, are

  • "I have written a thing, and I am now confident that it is as production-viable a solution as an established solution from the open-source community."
  • "Time manipulation math is worth doing manually."

There's a tonne of code out there betraying a programmer who has learned the wrong lessons specifically on this topic of datetime manipulation.

As @J_H suggests, add tests instead of print demos. The following code shows a cheap test method with assert that requires passing -ea on the Java command line (this should not be done in real life, and JUnit should be used instead).

This assumes that the daylight status of each day is assessed at noon. It tests both the original and Java-integrated methods. It reveals that, yes, time is even more complicated than you had assumed in the original implementation - I will leave it as an exercise to the reader to figure out what occurs on October 28, 1995.

package com.stackexchange;

import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.zone.ZoneRules;
import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;

public class Main {
    private interface SummertimeUtil
    {
        boolean isSummertimeAt(int year, int month, int day);
    }

    public static class SummertimeOP implements SummertimeUtil
    {
        /**
         * Calculates the weekday (1-7) given a date in the format year-month-day.
         *
         * @param year  the year of the date
         * @param month the month of the date
         * @param day   the day of the date
         * @return the weekday (1-7) of the given date
         */
        public static int weekdayFromDate(int year, int month, int day) {
            if (month < 3) {
                year--;
                month += 12;
            }
            int wd = (day + 2 * month + (3 * month + 3) / 5 + year + year / 4 - year / 100 + year / 400 + 1) % 7;
            // return new int[] {7, 1, 2, 3, 4, 5, 6}[wd];
            return (wd + 6) % 7 + 1;
        }

        /**
         * Determines if a given year is a leap year.
         *
         * @param year the year to be checked
         * @return true if the year is a leap year, false otherwise
         */
        public static boolean isLeapYear(int year) {
            return year % 4 == 0 && year % 100 != 0 || year % 400 == 0;
        }

        /**
         * Calculates the day of the year based on the given year, month, and day.
         *
         * @param year  the year
         * @param month the month (1-12)
         * @param day   the day (1-31)
         * @return the day of the year (1-365)
         */
        public static int dayOfYearFromDate(int year, int month, int day) {
            int[] days = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
            int sum = 0;
            for (int i = 0; i < month - 1; i++) {
                sum += days[i];
            }
            if (isLeapYear(year) && month > 2) {
                sum++;
            }
            return sum + day;
        }

        public static int dateOfLastSundayInMarch(int year) {
            int march = dayOfYearFromDate(year, 3, 31);
            int wd = weekdayFromDate(year, 3, 31);
            return march - (wd % 7);
        }

        public static int dateOfLastSundayInOctober(int year) {
            int october = dayOfYearFromDate(year, 10, 31);
            int wd = weekdayFromDate(year, 10, 31);
            return october - (wd % 7);
        }

        public boolean isSummertimeAt(int year, int month, int day) {
            int date = dayOfYearFromDate(year, month, day);
            int dateOfLastSundayInMarch = dateOfLastSundayInMarch(year);
            int dateOfLastSundayInOctober = dateOfLastSundayInOctober(year);
            return date == dateOfLastSundayInMarch || date > dateOfLastSundayInMarch && date < dateOfLastSundayInOctober;
        }
    }

    public static class SummertimeOldAPI implements SummertimeUtil
    {
        public final TimeZone zone;

        public SummertimeOldAPI(String zoneName)
        {
            zone = TimeZone.getTimeZone(zoneName);
        }

        public boolean isSummertimeAt(int year, int month, int day)
        {
            Calendar cal = Calendar.getInstance(zone);
            cal.set(year, month-1, day, 12, 0, 0);
            return isSummertimeAt(cal.getTime());
        }

        public boolean isSummertimeAt(Date date)
        {
            return zone.inDaylightTime(date);
        }
    }

    public static class SummertimeNewAPI implements SummertimeUtil
    {
        public final ZoneRules zoneRules;

        public SummertimeNewAPI(String zoneName) {
            zoneRules = ZoneId.of(zoneName).getRules();
        }

        public boolean isSummertimeAt(int year, int month, int day)
        {
            LocalDateTime time = LocalDateTime.of(year, month, day, 12, 0, 0);
            Instant instant = time.toInstant(zoneRules.getOffset(time));
            return isSummertimeAt(instant);
        }

        public boolean isSummertimeAt(Instant instant) {
            return zoneRules.isDaylightSavings(instant);
        }
    }

    private final SummertimeUtil[] utils = {
        new SummertimeOP(),
        new SummertimeOldAPI("Europe/Berlin"),
        new SummertimeNewAPI("Europe/Berlin")
    };

    private void testCorrectness()
    {
        for (SummertimeUtil util: utils)
        {
            assert !util.isSummertimeAt(2023, 1, 1);

            assert !util.isSummertimeAt(2023, 3, 25);
            assert util.isSummertimeAt(2023, 3, 26);

            assert util.isSummertimeAt(2023, 6, 1);

            assert util.isSummertimeAt(2023, 10, 28);
            assert !util.isSummertimeAt(2023, 10, 29);

            assert !util.isSummertimeAt(2023, 12, 31);
        }
    }

    private void testEquivalence()
    {
        LocalDate
            start = LocalDate.of(2023, 10, 23),
            limit = LocalDate.of(1995, 10, 29);

        for (long day_delta = 0;; ++day_delta)
        {
            LocalDate date = start.minusDays(day_delta);
            if (date.isBefore(limit)) break;

            int year = date.getYear(),
                month = date.getMonthValue(),
                day = date.getDayOfMonth();

            boolean origResult = utils[0].isSummertimeAt(year, month, day);
            for (int i = 1; i < utils.length; ++i)
            {
                boolean newResult = utils[i].isSummertimeAt(year, month, day);
                assert origResult == newResult;
            }
        }
    }

    public void test()
    {
        testCorrectness();
        testEquivalence();
    }

    public static void main(String[] args) {
        new Main().test();
    }
}
\$\endgroup\$
7
  • 9
    \$\begingroup\$ I strongly disagree with the "... is counter to learning" comment. I don't think any beginner will, in their first year, implement anything that doesn't exist as a library somewhere (ok ok, I didn't search for a fizzbuzz library ...). On the contrary, too many programmers just use some library, having no idea about inner workings and complexity, then wonder why their program that worked well with 5 data objects becomes horribly slow when there's 1 million of them. Implementing libraries yourself will a) teach you to use them better and b) make you appreciate their existence. \$\endgroup\$ Commented Oct 23, 2023 at 8:50
  • 3
    \$\begingroup\$ @GuntramBlohm: Indeed, this question illustrates that point perfectly. Through trying to write their own time-zone method, OP is learning (along with many other readers!) about all the subtleties of time-zones, that have to go into a serious time-zone library. \$\endgroup\$ Commented Oct 23, 2023 at 10:08
  • 1
    \$\begingroup\$ “EU summer time is not a thing.” Yes, it is, and it is independent of the actual time zone en.wikipedia.org/wiki/Summer_time_in_Europe \$\endgroup\$
    – Carsten S
    Commented Oct 23, 2023 at 14:46
  • 1
    \$\begingroup\$ @CarstenS It being independent of the time zone is on its own enough to make it clearly irrelevant for the question at hand; and anyway, OP's reference to MESZ shows that they do care about a specific time zone; so it's just a wording issue. I've adjusted my language. \$\endgroup\$
    – Reinderien
    Commented Oct 23, 2023 at 15:10
  • 2
    \$\begingroup\$ The main question that the code is supposed to answer is “is summer time in effect on a given date?”. This is regulated by a EU directive, even though there are multiple time zones in the EU. (The problem that you mention that this does not only depend on the day but also on the time of day and that that would have to be given in a specific tz remains, of course.) \$\endgroup\$
    – Carsten S
    Commented Oct 23, 2023 at 15:31

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.