Osy#betaRun it on Osyrin: osyrin.com

Reference / Query

Days, weeks and months in a time zone — at.Bucket(TimeBucket.Month, zone)

instant.Bucket(TimeBucket unit, Zone zone[, DayOfWeek weekStart]) → DateTime

The start of the hour, day, week, month, quarter or year an instant falls in — counted on a team's own clock, so a day starts at midnight in Stockholm, the month of March ends when Stockholm's March does (summer time included), and a week starts on the day you say. Grouped by in the database (`date_trunc` at the zone), or over a list in memory, with the same answer.

preview1 example compiled by CIquerydatesgroupingzones

Summary#

Group by the month (or day, or week) in the zone the team works in:

class PerMonth {
  public DateTime Month;
  public int Jobs;
}

entity Job {
  [Required, MaxLength(20)] string Team;
  DateTime At;
}

List<PerMonth> Monthly() {
  var stockholm = TimeZoneInfo.FindSystemTimeZoneById("Europe/Stockholm");
  return Job.GroupBy(j => j.At.Bucket(TimeBucket.Month, stockholm))
            .Select(g => new PerMonth { Month = g.Key, Jobs = g.Count() })
            .OrderBy(m => m.Month)
            .ToList();
}

List<PerMonth> WeeklyFromSunday() {
  var stockholm = TimeZoneInfo.FindSystemTimeZoneById("Europe/Stockholm");
  return Job.GroupBy(j => j.At.Bucket(TimeBucket.Week, stockholm, DayOfWeek.Sunday))
            .Select(g => new PerMonth { Month = g.Key, Jobs = g.Count() })
            .OrderBy(m => m.Month)
            .ToList();
}

A job at 22:30 UTC on 31 March is in April in Stockholm (00:30 on 1 April, summer time), and is counted there.

Signature#

DateTime Bucket(this DateTime at, TimeBucket unit, TimeZoneInfo zone)
DateTime Bucket(this DateTime at, TimeBucket unit, TimeZoneInfo zone, DayOfWeek weekStart)   // a week from this day
public enum TimeBucket { Hour, Day, Week, Month, Quarter, Year }

The answer is an instant (UTC): the moment the bucket starts in zone. Two instants share a bucket exactly when they fall in the same local hour, day, week, month, quarter or year there.

Description#

A calendar, not a clock#

A day in Stockholm is 23 hours on the last Sunday of March and 25 on the last Sunday of October; a bucket follows the calendar, so each day is one bar and none is split. Counting in UTC instead moves every evening's work after 22:00 (or 23:00 in winter) into the next day.

Weeks start on the day you say#

Without a weekStart a week starts on Monday (ISO). DayOfWeek.Sunday or DayOfWeek.Saturday starts it there.

Same answer in memory#

list.GroupBy(j => j.At.Bucket(TimeBucket.Month, zone)) over a List<T> gives the same buckets, so a test over a list and a report over the table agree.

See also#

Related

GroupBy (and HAVING)

Group rows by a key and reduce each group to one row — `g.Key`, `g.Count()`, `g.Count(o => p)`…

Median and percentiles — g.Median(…), g.Percentile(…, 0.9)

The middle value of a group, or any percentile of it — "half the jobs were done sooner than this", "nine in ten sooner…

A grouping of groups — GroupBy, then GroupBy again, in one statement

Group, then group the groups: each person's jobs per day, then per person how many days they worked and their busiest…

Time zones — TimeZoneInfo

Store and use civil time zones with C#'s `TimeZoneInfo`. A zone holds an IANA id (`"Europe/Stockholm"`), and C#'s…