Delphi week number function based on system start of week - windows

The DateUtils.WeekOfTheYear function is great but it uses the ISO 8601 standard definition of a week. That is, a week is considered to start on a Monday and end on a Sunday. I need a similar function that determines the week number based on the system setting for the start of the week. Or at least for either a sunday or monday start of week like MySQL's week function. Anyone have such a function?

ISO-8601 includes a great deal more than just the first day of the week in its specifications for these calculations. There are also rules which determine the first week in the year, for example.
It is not clear whether what you are looking for is a function to replicate the ISO-8601 calculation with these rules otherwise intact and solely varying the first day of the week, or a direct equivalent of the WEEK() function of MySQL, or something else only similar (and not fully defined).
Worth noting is that the MySQL WEEK() function accepts a parameter which does not determine an arbitrary day marking the start of the week, rather it indicates whether either of Sunday or Monday is to be used along with changing a number of other rules that determine the calculated result.
By contrast, the system setting for first day of the week on Windows itself can be ANY day of the week that the user wishes - Mon, Tue, Wed, Thu, Fri, Sat or Sun.
The implementation I provide below is a simple calculation (some might call it naive) which simply returns a value 0..53 based on the number of week periods, or part periods, elapsed between a date specified and the start of the year in which that date occurs.
The week in which 1st of Jan occurs for the year containing the specified date is deemed to be week 0.
Therefore if the 1st of Jan occurs on a Sunday and the "start of week" is defined as Monday then:
Sun, 01-Jan = Week 0
Mon, 02-Jan = Week 1
..
Sun, 08-Jan = Week 1
Mon, 09-Jan = Week 2
..
etc
The Implementation
I have split the implementation into two distinct parts.
The first (WeeksSince01Jan) accepts a date and a parameter indicating the day of week to be considered the first day of the week in the calculation.
This parameter takes a TSYSDayOfWeek value - an enum arranged such that the ordinal values for each day correspond to the values used in system locale settings for the days of the week. (The value returned by the RTL DayOfWeek function uses different values, defined in this code as TRTLDayOfWeek).
The second part of the implementation provides a LocaleWeeksSince01Jan, to demonstrate obtaining the locale defined first day of week for the current user. This is then simply passed thru to a call to WeeksSince01Jan.
type
TSYSDayOfWeek = (sdMon, sdTue, sdWed, sdThu, sdFri, sdSat, sdSun);
TRTLDayOfWeek = 1..7; // Sun -> Sat
function WeeksSince01Jan(const aDate: TDateTime;
const aFirstDayOfWeek: TSYSDayOfWeek): Word;
const
LOCALE_DOW : array[TRTLDayOfWeek] of TSYSDayOfWeek = (sdSun, sdMon, sdTue, sdWed, sdThu, sdFri, sdSat);
var
y, m, d: Word;
dayOfYearStart: TSYSDayOfWeek;
dtYearStart: TDateTime;
dtStartOfFirstWeekInYear: TDateTime;
iAdjust: Integer;
begin
// Get the date for the first day of the year and determine which
// day of the week (Mon-Fri) that was
DecodeDate(aDate, y, m, d);
dtYearStart := EncodeDate(y, 1, 1);
dayOfYearStart := LOCALE_DOW[DayOfWeek(dtYearStart)];
// Week calculation is simply the number of 7 day periods
// elapsed since the start of the year to the specified date,
// adjusted to reflect any 'offset' to the specified first day of week.
iAdjust := Ord(dayOfYearStart) - Ord(aFirstDayOfWeek);
result := (((Trunc(aDate) + iAdjust) - Trunc(dtYearStart)) div 7);
end;
function LocaleWeeksSince01Jan(const aDate: TDateTime): Word;
var
localeValue: array[0..1] of Char;
firstDayOfWeek: TSYSDayOfWeek;
begin
// Get the system defined first day of the week
GetLocaleInfo(LOCALE_USER_DEFAULT, LOCALE_IFIRSTDAYOFWEEK, localeValue, SizeOf(localeValue));
firstDayOfWeek := TSYSDayOfWeek(Ord(localeValue[0]) - Ord('0'));
result := WeeksSince01Jan(aDate, firstDayOfWeek);
end;
If you have more complex rules to determine the 0th or 1st week of a year based on numbers of days in that week etc, then you will need to modify this implementation accordingly. There is no attempt to accommodate such needs in the current implementation.
For Testing
The code below may be used as the basis for testing the output (using the system defined first day of the week):
const
YEAR = 2012;
var
d: Integer;
dt: TDateTime;
wk: Word;
begin
List.Items.Clear;
dt := EncodeDate(YEAR, 1, 1) - 7;
for d := 1 to 380 do
begin
dt := dt + 1;
wk := LocaleWeeksSince01Jan(dt);
List.Items.Add(Format('%s, %s = week %d', [ShortDayNames[DayOfWeek(dt)],
DateToStr(dt),
wk]));
end;
Where List is a reference to a TListbox.
Change the value of YEAR to produce a range of results that cover all dates in the specified year +/- an additional 7/8 days, to illustrate the change in result at year end of the preceding and succeeding years.
NOTE: 2012 is a year which demonstrates the possibility of returning dates in that year covering the full range of potential results, 0..53.

If you are only interested in week starting on Sunday instead on Monday you can simply substract 1 day from your DateTime value before feeding it to DateUtils.WeekOfTheYear function.
EDIT:
Response to David Heffernan comment:
Imagine what happens when you subtract 1 from January 1st
It depends on which day is on January 1st
From Embarcadero documentation: http://docwiki.embarcadero.com/Libraries/XE8/en/System.DateUtils.WeekOfTheYear
AYear returns the year in which the specified week occurs. Note that
this may not be the same as the year of AValue. This is because the
first week of a year is defined as the first week with four or more
days in that year. This means that, if the first calendar day of the
year is a Friday, Saturday, or Sunday, then for the first three, two,
or one days of the calendar year, WeekOfTheYear returns the last week
of the previous year. Similarly, if the last calendar day of the year
is a Monday, Tuesday, or Wednesday, then for the last one, two, or
three days of the calendar year, WeekOfTheYear returns 1 (the first
week of the next calendar year).
So if the week starts with Sunday instead of Monday then it means that week start and end days are simply shifted by one day backward.
So for such occasions it is best to use over-ridden version with additional variable parameter to which the year that this week belongs to is stored.

I've combined Deltics' great code with SilverWarior's simple idea to create a WeekOfYear function that handles the system week start day.
type
TSYSDayOfWeek = (sdMon, sdTue, sdWed, sdThu, sdFri, sdSat, sdSun);
function LocaleWeekOfTheYear(dte: TDateTime): word;
var
localeValue: array[0..1] of Char;
firstDayOfWeek: TSYSDayOfWeek;
yearOld,yearNew: word;
dteNew: TDateTime;
begin
// Get the system defined first day of the week
GetLocaleInfo(LOCALE_USER_DEFAULT, LOCALE_IFIRSTDAYOFWEEK, localeValue, SizeOf(localeValue));
firstDayOfWeek := TSYSDayOfWeek(Ord(localeValue[0]) - Ord('0'));
yearOld:= Year(dte);
if (firstDayOfWeek=sdSun) then
dteNew:= dte-1
else
dteNew:= dte+Ord(firstDayOfWeek);
yearNew:= Year(dteNew);
if (yearOld=yearNew) then
dte:= dteNew;
Result:= WeekOfTheYear(dte);
end;

To make the first day of the week is Saturday, I use to add 2 to the value of Now date.
As example WeekOf(Now + 2) : makes the first day is Saturday,
WeekOf(Now + 2) : makes the first day is Sunday,
WeekOf(Now + 0) : makes the first day is Monday,

Related

Get the first occurrence of a specific day after a given date

This thread provides an answer to find the first occurrence of a day (e.g. Thursday) after today's date.
I would like to get a more general function which gives the first occurrence of a day (e.g. Thursday) after any given date (e.g. November 1st, 2017).
The method would therefore take 2 arguments: the Date (either as a String, or as a Date object) and the day (as a String).
One way I found is to check each day after with the thursday? method (e.g. my_date_object.thursday?) but you would need a switch / if-else statement to check which day to check against, which makes the method rather bulky, as seen below.
def get_next_day_after_date(date, day)
days_in_week = 7
week = []
days_in_week.times { |day_after| week << (date + day_after) }
if day.casecmp('Monday').zero?
week.select(&:monday?)
elsif day.casecmp('Tuesday').zero?
week.select(&:tuesday?)
elsif day.casecmp('Wednesday').zero?
week.select(&:wednesday?)
# same code for Thursday to Sunday
end
end
Is there a cleaner way to achieve this in pure Ruby? (no use of active_support or rails-specific methods).
require "date"
def get_next_day_after_day(date, day)
date + (Date.strptime(day, "%A") - date) % 7
end

Finding the day in which a given year begins

This question arose when I was trying to understand Sakamoto's algorithm for finding the day of a given date.
I found the working of the algorithm to be difficult to comprehend even after reading the following Stackoverflow answer
So, I decided to first solve a specific problem of finding the day in which a given year begins( Jan-1).
From the Sakamoto's algorithm, I just took the part of adding the additional days contributed by the leap and non-leap years.
My code is as follows:
public String getDay(String date)
{
String[] days = { "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" };
int day = Integer.parseInt(date.split("/")[0]);
int month = Integer.parseInt(date.split("/")[1]);
int year = Integer.parseInt(date.split("/")[2]);
year--; // to calculate the additional days till the previous year
int dayOfTheWeek = (year + year/4 - year/100 + year/400) % 7;
return days[dayOfTheWeek];
}
Thus, for the date "1/1/0001", it returns Sunday.
To verify its correctness, I implemented Sakamoto's algorithm and compared the results and my program's result always seems to be one day before the day returned by the Sakamoto's algorithm.
For the date "1/1/0001" my program returns Sunday, while Sakamoto's returns Monday.
So,
1) Does it mean that the Gregorian calendar started on Monday instead of Sunday??
2) If yes, does it mean I should add 1 to the result to get the right day or is my program logically incorrect?
Finally, I used TimeAndDate site's day calculator tool and "1/1/0001" starts on Saturday.
My final question is
3) On what day does the Gregorian calendar start?
Any light on the these questions is much appreciated.
Thanks,
What exactly is the point of reinventing the wheel?
Joda-Time is a de facto standard for date-time operations in Java, and it provides dayOfWeek method for its DateTime objects. See e.g. http://joda-time.sourceforge.net/userguide.html#Querying_DateTimes
If you are then still interested in details how to get the computation right, see https://github.com/JodaOrg/joda-time/blob/master/src/main/java/org/joda/time/chrono/BasicChronology.java#L538

Error Type mismatch

Why am I getting this Type Mismatch error?
Code:
Dim data as Date
data = CDate(Format(31, "00") & "/" & Format("1/9/2013", "mm/yyyy"))
When I try this with strings of course it works perfect.
Obs: It works if I use a different day than 31 like 28 for exemple... But Why this error occurs only with the day 31.
It looks like you are trying to get the last day of the month. If that is the case, try this...
Dim data As Date
Dim OriginalDate As Date
OriginalDate = DateSerial(2013, 12, 20)
data = DateSerial(Year(OriginalDate), Month(OriginalDate) + 1, 0)
This code basically gets the first day of the next month and then subtracts one day. The nice thing about using the DateSerial function is that you can give it "invalid" values, For example, if you use Year = 2013 and Month = 13, you will get January 2014.
I found the answer, and it's stupid -.-" Month 9 = Agost = 30 days and not 31... I feel so stupid now ;x
You have to watch out for dates because depending on what region your computer is in (Control Panel / Region and Language) sometimes your date of Format("1/9/2013", "mm/yyyy")) can be interpreted as September 1, 2013 or as January 9, 2013
If you can, use DateSerial to specifically hook in the month/day numbers without relying on the format output.

number of days in a period that fall within another period

I have 2 independent but contiguous date ranges. The first range is the start and end date for a project. Lets say start = 3/21/10 and end = 5/16/10. The second range is a month boundary (say 3/1/10 to 3/31/10, 4/1/10 to 4/30/10, etc.) I need to figure out how many days in each month fall into the first range.
The answer to my example above is March = 10, April = 30, May = 16.
I am trying to figure out an excel formula or VBA function that will give me this value.
Any thoughts on an algorithm for this? I feel it should be rather easy but I can't seem to figure it out.
I have a formula which will return TRUE/FALSE if ANY part of the month range is within the project start/end but not the number of days. That function is below.
return month_start <= project_end And month_end >= project_start
Think it figured it out.
=MAX( MIN(project_end, month_end) - MAX(project_start,month_start) + 1 , 0 )

Calculating future occurrences of Friday the 13th

I'd like to be able to start with a year, and calculate occurrences of Friday the 13th. A brute force solution is easy and obvious. I have something slightly better, but I have no doubt that someone else can come up with an elegant algorithm for this.
Perhaps a little trickier, I'd be interested in giving the program a month, and have it find the next year in which that month has a Friday the 13th.
Feel free to use pseudo code, but I expect people will vote more for working code samples in you favorite language.
Any month that starts with a Sunday has a Friday on the thirteenth. There are only 14 combinations possible knowing what day the first of the year is on (with or without leap year, and sun-sat). You should just calculate it once and get it over with. You'd only check 14*12 possible months to start out with, well with in reason.
resultant table element (from 2009, 2010):
[Thursday,false] => Feb, March, Nov
[Friday,false] => Aug
to fill the table you have a generic month Jan(31),Feb(28).. and then iterate with a seed of each day of the week, noting months that start with sunday, and also with a leap year and without. Pretty straight forward, and once done, you can share it with us :)
Since your brute force algorithm is apparently the intuitive day-by-day iteration option, perhaps you haven't considered the Doomsday Algorithm. It would allow you to simply check if that 13th is a Friday. As far as I know, it is the most efficient solution to the problem.
Here's some example PHP code that goes through a pretty straight forward loop of the dates in a range. I would modify this to check the 13th of each month for Fridayness, rather than checking every Friday for 13thness, as they do in the example.
One thing I noticed is that the first of the month falls on a Sunday during months with a Friday the 13th. You can probably leverage this to make it easier to calculate.
initialize startDate to 13th of the month given in the current year
while (true) {
if (startDate.dayOfWeek == Date.FRIDAY)
break;
else
startDate.year ++;
}
return startDate.year;
This is how I would do it:
Assume year is known and is an integer.
Loop from 1 to 12
Create date with loop index, year and 13 for the day
Determine day of week as per established algorithms
If day of week calculated above is Friday, do your work
If you want to start with a month and year (you have to assume some sort of year), your algorithm becomes
Assume year is known and an integer
Assume month is known and is an integer
Loop
Create date with index of loop as year, known month variable, and 13 for the day
Determine day of week as per established algorithms
If day of week calculate above is Friday, return date, else
Else increment year by 1
I use PowerShell 7.1 x64 on Windows 10, I am also interested in this, though my programming skill is not advanced enough to develop an independent and complex algorithm (without built-in) to solve this, currently I can do this:
$start=Read-Host "Input start year"
$end=Read-Host "Input end year"
$years=$start..$end
$blackfriday=#()
foreach ($year in $years) {
$blackfriday+=1..12 | foreach { [datetime]"$year-$_-13" } | where { $_.DayOfWeek -eq 'Friday'} | foreach {$_.tostring("yyyy, MMMM dd, dddd")}
}
$blackfriday
Update:
Now I have this, by brute forcing year-month-13 to convert date to days, and get the results where days mod 7 equal to 5, I have used all the advanced algorithms found on Wikipedia: https://en.wikipedia.org/wiki/Determination_of_the_day_of_the_week
And I found out they don't work well with PowerShell, don't know if it's just me or Wikipedia got it wrong, anyway here is my script, I have fc compared the results of the script with builtin and this script, and it shows"FC: no differences encountered":
$months=#(
[PSCustomObject]#{Month='January';Days=31}
[PSCustomObject]#{Month='February';Days=28}
[PSCustomObject]#{Month='March';Days=31}
[PSCustomObject]#{Month='April';Days=30}
[PSCustomObject]#{Month='May';Days=31}
[PSCustomObject]#{Month='June';Days=30}
[PSCustomObject]#{Month='July';Days=31}
[PSCustomObject]#{Month='August';Days=31}
[PSCustomObject]#{Month='September';Days=30}
[PSCustomObject]#{Month='October';Days=31}
[PSCustomObject]#{Month='November';Days=30}
[PSCustomObject]#{Month='December';Days=31}
)
function BlackFriday {
param(
[Parameter(ValueFromPipeline=$true, Mandatory=$true, Position=0)] [int64] $start,
[Parameter(ValueFromPipeline=$true, Mandatory=$true, Position=1)] [int64] $end
)
$years=$start..$end
$blackfriday=#()
foreach ($year in $years) {
$array=1..12
foreach ($arra in $array) {
$month=0
for ($i=0;$i -lt $arra-1;$i++) {
$month+=$months[$i].Days
}
[int]$ye=$year
if ($arra -le 2) { $ye-=1}
if ($ye % 4 -eq 0) {$leap=$ye/4}
else {while ($ye % 4 -ne 0) {$ye-=1}
$leap=$ye/4}
if ($ye % 100 -eq 0) {$century=$ye/100}
else {while ($ye % 100 -ne 0) {$ye-=4}
$century=$ye/100}
if ($ye % 400 -eq 0) {$cycle=$ye/400}
else {while ($ye % 400 -ne 0) {$ye-=100}
$cycle=$ye/400}
$leap=$leap-$century+$cycle
$date=[int64](($year-1)*365+$month+13+$leap)
if ($date % 7 -eq 5) {
$name=$months[$arra-1].Month
$blackfriday+=[string]"$year, $name 13, Friday"
}
}
}
$blackfriday
}
$start=Read-Host "Input start year"
$end=Read-Host "Input end year"
BlackFriday $start $end

Resources