Wrong day of week

Getting the days of the week the hard way, because there is no better way I try this:

var Jan1 = Gregorian.moment({:year => 2023, :month => 1, :day => 1});
for (var i = 0; i < self.DoW.size(); i++) {
	var DDD = Gregorian.info(Jan1.add(new Time.Duration(86400*i)), Time.FORMAT_MEDIUM);
	self.DoW[i] = DDD.day_of_week.substring(0,2).toUpper();
}

But DoW[0] = "SA"? My calendar says something different, so I try this:

var Jan1 = Gregorian.moment({:year => 2023, :month => 1, :day => 1, :hour => 12});
for (var i = 0; i < self.DoW.size(); i++) {
    var DDD = Gregorian.info(Jan1.add(new Time.Duration(86400*i)), Time.FORMAT_MEDIUM);
    self.DoW[i] = DDD.day_of_week.substring(0,2).toUpper();
}

Just by adding hour = 12 now I get the correct answer, DoW[0] = 'SU'. I'm pretty sure the day of the week is the same for all hours on Jan 1. Is this some time zone bug?

  • I suspect it has to do with UTC.  What time zone are you in?   You'll find that "day" isn't what you expect either.

    Look at the example in the API Doc, and note the reference to CST:.  

    Example:

    Creating a Moment representing May 16, 2003

    using Toybox.System;
    using Toybox.Time;
    using Toybox.Time.Gregorian;
    var options = {
        :year   => 2003,
        :month  => 5,
        :day    => 16,
        :hour   => 6    // UTC offset, in this case for CST
    };
    var birthday = Gregorian.moment(options);

  • I ran a test and confirmed that input to Time.Gregorian is UTC and info returns local time. It doesn't return the offset used, and you can't know it unless you use localMoment and pass it a location. Or you can use System.getClockTime to get the time zone offset but if the date changes because of the overlap in time zones it doesn't return that. It seems like something designed to be a watch would handle time better.

  • You can get your TZ offset by way of System.getClockTime().  You also want to pay attention to the dst offset.

    Between devices and the sim, there's a difference in how these are handled.  In one case dst is always 0 and the TZ offset changes, and in the other, the TZ offset is constant and dst changes,

  • To make matters worse, DST changes at 0200 (in the US) so the DST offset at 12AM is different than 12PM on two days of the year. DST in the southern hemisphere is 6 months apart from us, so Jan 1st may be DST in some countries. It seems to require even more steps to get something simple like days of the week starting from Sunday in the configured language. I double down on my previous comment: It seems like something designed to be a watch would handle time better.

  • In this case, simply change Gregorian.info() to Gregorian.utcInfo() and you won't have to worry about any time zone-related issues. (The input to Gregorian.moment() is UTC, as you mentioned, so using Gregorian.utcInfo() gives you consistent information without any unwanted adjustments due to the local time zone being different from UTC.)

    (This kind of issue and approach has come up before, and the main objection is "but I don't want to work with *UTC* time". But sometimes when you don't care about the actual time zone, as in this case, it's a viable strategy to exclusively deal with UTC time. Applies to systems other than CIQ/Monkey C as well.)

    (On a side note, the documentation for Gregorian.moment() used to incorrectly imply that the input was local time, and not UTC, which just goes to show that time and date handling ain't easy.)

  • That seems like it will solve my particular problem. I always use UTC when I can. My app involves the sun, moon, and tides, which use Julian 2000.0 terrestrial time. This started from 0 at 2000-01-01 11:58.55.816 UTC. Not to be confused with delta time or sidereal time which I also need.

    Still, it would be nice if a function was making adjustments it would be nice if there was a way to get both the adjusted value along with the adjustment made when the adjustments can be variable and non-linear.

  • Spoke too soon. utcInfo doesn't appear in the fossil record until API 2.1.0, so no go on my FR 235. localMoment doesn't come along until 3.3.0, so no help there either.

  • In that case, I see a couple of options:

    1) Do what Jim suggested and use System.getClockTime().timeZoneOffset to correct the time/date that you pass in to Gregorian.moment(). i.e. the input to Gregorian.moment() is UTC, but Gregorian.info() gives you info in terms of local time, so you use timeZoneOffset to convert input in terms of local time to the equivalent UTC time.

    e.g. If timeZoneOffset is -18000 seconds (-5 hours), and you're looking for the day of week on Jan 1, 2024 (midnight), then call Gregorian.moment() with Jan 1, 2024 5:00 AM (midnight - (-5 hours)), then use Gregorian.info() on the resulting moment.

    2) https://en.wikipedia.org/wiki/Julian_day#Julian_day_number_calculation

    - Convert the calendar day to the Julian day using the given formula:

    Converting Gregorian calendar date to Julian Day Number

    The algorithm is valid for all (possibly proleptic) Gregorian calendar dates after November 23, −4713. Divisions are integer divisions towards zero; fractional parts are ignored.[66]

    JDN = (1461 × (Y + 4800 + (M − 14)/12))/4 +(367 × (M − 2 − 12 × ((M − 14)/12)))/12 − (3 × ((Y + 4900 + (M - 14)/12)/100))/4 + D − 32075
    - Convert the julian day to the day of week

    Finding day of week given Julian day number

    The US day of the week W1 (for an afternoon or evening UT) can be determined from the Julian Day Number J with the expression:

    W1 = mod(J + 1, 7)[68]
    W1 0 1 2 3 4 5 6
    Day of the week Sun Mon Tue Wed Thu Fri Sat
  • Lets get back the the problem statement: I want a list of the days of the week, starting with Sunday as zero, in the user's configured language.

    I have the Julian days covered, that's not an issue. BTW getting the Julian date is way easier if you start with the unix time.

    There are multiple problems here:

    1. System.getClockTime().timeZoneOffset gives you the offset at the time and place that you call it, there is not an option to give it a time or place like there is with localMoment. There isn't any way to know if the offset now is the same offset as it was on January 1st, 2023, a week I know began on a Sunday. Some countries set their DST dates only a few months in advance each year.

    2. I can give a date and time (in UTC) to Time.Moment, but there's no guarantee that the first day of week generated will be Saturday or Sunday or Monday in the language (see original post). So I need some hunting algorithm to check the returned calendar date to see if it matches the one passed in and if not adjust and try again?

    There doesn't appear to be an exact solution prior to the introduction of utcInfo in API 2.1.0.

  • Lets get back the the problem statement: I want a list of the days of the week, starting with Sunday as zero, in the user's configured language.

    Sorry, that (somewhat implicit) part of your OP flew over my head. My bad. I thought you just wanted to get DOW (as a number) for a given date, but that's clearly not the case.

    Both of your points are correct.

    Guess you'll just have to search for a Sunday by trial and error, as you said.

    EDIT:

    Or, you could start with any fixed day (*) and go 6 days back/forward, which is just another variation of the same idea. But since Time.FORMAT_SHORT gives you day_of_week as an integer from 1 to 7, it's actually not that bad.

    (*) Probably best to avoid days near DST transitions, to avoid any edge cases where it's the same day 24 hours later, which is why I edited my example to use Jan 1 as you did, instead of today.

    e.g. something like:

    var daysOfWeek = new[7];
    
    var jan1 = Gregorian.moment({:year => 2023, :month => 1, :day => 1});
    for (var i = 0; i < 7; i++) {
        var date = jan1.add(new Time.Duration(86400*i));
        var infoShort = Gregorian.info(date, Time.FORMAT_SHORT);
        var infoMedium = Gregorian.info(date, Time.FORMAT_MEDIUM);
        daysOfWeek[infoShort.day_of_week - 1] = infoMedium.day_of_week.substring(0, 2).toUpper();
    }
    System.println("daysOfWeek = " + daysOfWeek);
    // Output:
    // daysOfWeek = [SU, MO, TU, WE, TH, FR, SA]

    Notes:

    - this code doesn't depend on the fact that Jan 1, 2023 was a Sunday. Any date would work, except for dates near the end of DST (when there's a 25 hour day in local time), since in that case it's possible that "now" and "now + 24 hours" are the same date in local time. If you want to make the code super-bulletproof you could detect duplicate days (or simply unconditionally iterate over 8 days instead of 7), but it's probably a pretty safe bet that there will never be a DST transition from Dec 31 to Jan 8 in any jurisdiction, so January 1 of *any year* would be a good choice for this algorithm. Or, just change "i < 7" to "i < 8" to remove any doubt.

    - obviously this code can be optimized to only call Gregorian.info with Time.FORMAT_SHORT once, instead of 7 times.)

    EDIT2: super-bulletproof optimized version

    var daysOfWeek = new[7];
    
    // any date at all would work
    var jan1 = Gregorian.moment({:year => 2000, :month => 1, :day => 1});
    var jan1_dow = Gregorian.info(jan1, Time.FORMAT_SHORT).day_of_week - 1;
    for (var i = 0; i < 8; i++) { // 8 is not a typo
        var date = jan1.add(new Time.Duration(86400*i));
        var infoMedium = Gregorian.info(date, Time.FORMAT_MEDIUM);
        daysOfWeek[(jan1_dow + i) % 7] = infoMedium.day_of_week.substring(0, 2).toUpper();
    }
    System.println("daysOfWeek = " + daysOfWeek);
    // Output:
    // daysOfWeek = [SU, MO, TU, WE, TH, FR, SA]