Calculate the date from an ISO date

published: Fri, 24-Feb-2006   |   updated: Fri, 24-Feb-2006
Turnstone's trees

One of the most popular pages on my website is the one where I discuss how to calculate the ISO week number of a date. Today out of the blue I was asked, if given an ISO week, how would you do the reverse calculation and find the original date.

The thing is, my original code only calculated the week number and the year for a given date. Using those two pieces of information is insufficient to calculate the original date; indeed, you could only really return the date of the Monday of that week as a predictable answer.

So to properly answer the question I'd have to alter the original code so that the full ISO date was returned: the year, the week, and the day of the week. OK, no problem.

Then I started thinking. The original code I'd written was procedural (or, since it was written in C#, it used static methods): when presented with a date it would return the ISO week. Given that I'd been ranting recently about procedural code and that we should endeavor to design so as not to use it, I didn't want to just write yet another static method. No, it had to be a proper class. Much better. For fun I then decided to use Chrome instead of C# (Chrome is an Object Pascal derivative hosted in Visual Studio 2005).

So first of all, here's the original code, tidied up (some of my original identifiers were not that good), and converted to Chrome as a class. I also changed it so that the class deals with ISO dates (it returns the ISO date as an integer in the form YYYYWWD, where YYYY is the year, WW is the week number from 01 to 53, and D is the day of the week with 1 for Monday, 2 for Tuesday, all the way to 7 for Sunday). The ToString() method returns the formatted version of the ISO date with a constant character W in between the year and the week (2005123, the Wednesday in the 12th week of 2005, would be converted to '2005W123').

Anyway here's the code:

namespace IsoWeek;

interface

type
  IsoDate = public class
    private
      FValue : integer;
      method get_Day: integer;
      method get_Week: integer;
      method get_Year: integer;

      method getIsoDayNumber(date : DateTime) : integer;
      method getMondayOf1stWeek(year : integer) : DateTime;
      method convertDateToIsoDate(date : DateTime) : integer;
    protected
    public
      constructor(date : DateTime);
      constructor(year : Integer; month : Integer; day : integer);

      method ToString : string; override;

      property Value : integer read FValue;
      property Year : integer read get_Year;
      property Week : integer read get_Week;
      property Day : integer read get_Day;
  end;
  
implementation

{===IsoDate==========================================================}
constructor IsoDate(date : DateTime);
begin
  FValue := convertDateToIsoDate(date);
end;
{--------}
constructor IsoDate(year : Integer; month : Integer; day : integer);
begin
  FValue := convertDateToIsoDate(new DateTime(year, month, day));
end;
{--------}
method IsoDate.get_Year: integer;
begin
  Result := FValue div 1000;
end;
{--------}
method IsoDate.get_Week: integer;
begin
  Result := (FValue div 10) mod 100;
end;
{--------}
method IsoDate.get_Day: integer;
begin
  Result := FValue mod 10;
end;
{--------}
method IsoDate.getIsoDayNumber(date : DateTime) : integer;
begin
  // the ISO day number has 1==Monday, ..., 7==Sunday
  Result := integer(date.DayOfWeek); // 0==Sunday, 6==Saturday
  if (Result = 0) then 
    Result := Result + 7;
end;
{--------}
method IsoDate.getMondayOf1stWeek(year : integer) : DateTime;
var
  dt : DateTime;
begin
  // get the date for the 4-Jan for this year
  dt := new DateTime(Year, 1, 4);

  // return the date of the Monday that is less than or equal
  // to this date
  Result := dt.AddDays(1 - getIsoDayNumber(dt));
end;
{--------}
method IsoDate.convertDateToIsoDate(date : DateTime) : integer;
var
  mondayOf1stWeek : DateTime;
  isoYear : Integer;
begin
  isoYear := date.Year;
  if (date >= new DateTime(isoYear, 12, 29)) then begin
    mondayOf1stWeek := getMondayOf1stWeek(isoYear + 1);
    if (date < mondayOf1stWeek) then 
      mondayOf1stWeek := getMondayOf1stWeek(isoYear)
    else
      inc(IsoYear);
  end
  else begin
    mondayOf1stWeek := getMondayOf1stWeek(isoYear);
    if (date < mondayOf1stWeek) then begin
      dec(isoYear);
      mondayOf1stWeek := getMondayOf1stWeek(isoYear);
    end;
  end;
  
  Result := (isoYear * 1000) + 
            (((date - mondayOf1stWeek).Days / 7 + 1) * 10) + 
            getIsoDayNumber(date);
end;
{--------}
method IsoDate.ToString : string; 
begin
  Result := string.Format('{0:0000}W{1:000}', FValue div 1000, FValue mod 1000);
end;
{====================================================================}

end.

Now, to calculate the original date from an ISO date is simple: extract the year part, calculate the date of the Monday of the first week for that year, and then add the number of days in the week part of the ISO date and the day of the week.

namespace IsoWeek;

interface

type
  IsoDate = public class
    private
      ...   
      method get_Date: DateTime;
      ...
    protected
    public
      ...
      property Date : DateTime read get_Date;
  end;
  
implementation

{===IsoDate==========================================================}
...
method IsoDate.get_Date: DateTime;
begin
  Result := getMondayOf1stWeek(Year).AddDays((Week - 1) * 7 + (Day - 1)); 
end;
...
{====================================================================}

Not to hard, I think you'll agree. And it certainly looks better as a class.