Showing posts with label datetime. Show all posts
Showing posts with label datetime. Show all posts

Wednesday, September 1, 2010

Number of working days in a month

I didn't realize it's been over a year since my last post. Most of the code I've written this past year has been too specialized to be considered useful to anyone outside of my present employer, but this week I was presented with a problem with our Intranet application where the annual salary of employees who are paid hourly was not being calculated correctly. I'm working with the application vendor to get this corrected in the next version, and learned some interesting things about how to calculate this correctly.

A common formula to calculate annual salary for hourly employees is hourly pay × 40 hours/week × 52 weeks/year. This is very close to accurate, but 365/366 days per year does not divide evenly by 7 days/week.

A more accurate result can be obtained by multiplying the hourly pay by the number of working hours in a year. One way to calculate this is based on the last day of the year. In a non-leap year, there are 2080 working hours if the last day of the year is a Saturday or Sunday, and 2088 working hours if the last day of the year is a weekday. In the case of a leap year, there are 2080 working hours if the last day of the year is a Sunday, 2088 working hours if the last day of the year is a Saturday or Monday, and 2096 working hours for any other weekday.

At our company, we work 8 hours each day, so we can calculate the number of working days rather than the number of working hours and multiply by 8 hours/day. Since the number of days per month is variable, I decided to calculate working days per month separately and sum them up for working days per year.


  1. Function WorkingDays(someYear, someMonth)
  2.     WorkingDays = 0
  3.     For i = 1 To MonthDays(someYear, someMonth)
  4.         If Weekday(DateSerial(someYear, someMonth, i)) <> 1 And Weekday(DateSerial(someYear, someMonth, i)) <> 7 Then
  5.             WorkingDays = WorkingDays + 1
  6.         End If
  7.     Next
  8. End Function

You can take the result and multiply by 8 hours/day to get the number of working hours in a month, and then multiply by the hourly pay to get the monthly salary. The annual working days can be obtained by calling the function in a for i = 1 to 12...next loop, multiplied by 8 hours/day to get the number of working hours in a year, and then multiplied by the hourly pay to get the annual salary.

We ignore holidays and vacation days since the employee is paid for those as well, though technically vacation pay is accured at 4% so you could subtract vacation days (probably either 10 or 15, depending on number of years of service) and multiply by 1.04 to get an even more accurate amount for annual salary.


View ASP implementation on Snipplr.

Saturday, November 29, 2008

Swatch Internet Time

Back in 1998, a company called Swatch decided to invent a new method of timekeeping. It didn't catch on, but some people embraced it. If you're one of those people, this is for you. We're going to write a function to convert a time value into a Swatch beat. Our PHP programmer friends already have access to this sort of thing through PHP's date() function.


The following code builds upon two other functions previously published here:

IMPORTANT: To return an accurate result, the time value passed in needs to be UTC/GMT (in other words, without offset). There are two ways to achieve this: set your server's time zone to GMT, or use my UTC time function.


ASP

  1. function swatch(someTime)
  2.     swatch = str_pad(floor(((Hour(someTime) * 3600 * 1000) + (Minute(someTime) * 60 * 1000) + (Second(someTime) * 1000)) / 86400), 3, "0", STR_PAD_LEFT)
  3. end function

View ASP implementation on Snipplr

Saturday, November 15, 2008

UTC and Atomic Time

In this special double issue, I'm going to show you how to obtain time values in the GMT timezone, as well as ultra-precise atomic clock time values.


Our PHP programmer friends have a slight advantage against us. Their language is aware of what time zone the server is located in and is capable of doing various things with that information, including returning time values without any timezone offset. ASP has no clue what time zone the server is set for, but we can use XMLHTTP to retrieve a time value from a NIST time server.


ASP

  1. function utcnow()
  2.     dim xmlhttp
  3.     dim response
  4.     ' Server to query datetime from
  5.     Const TimeServer = "http://time.nist.gov:13"
  6.     ' Use XML HTTP object to request web page content
  7.     Set xmlhttp = Server.CreateObject("Microsoft.XMLHTTP")
  8.     xmlhttp.Open "GET", TimeServer, false, "", ""
  9.     xmlhttp.Send
  10.     response = xmlhttp.ResponseText
  11.     set xmlhttp = nothing
  12.     ' Parse UTC date
  13.     utcnow = cDate(mid(response, 11, 2) & "/" & mid(response, 14, 2) & "/" & mid(response, 8, 2) & " " & mid(response, 16, 9))
  14. end function

If you were to compare the value returned by this function and the value returned by the built-in Now() function, you might notice more than just the hour value is different. This could mean you live in one of those funky half-hour-offset timezones, or it could mean that your server's clock is off by a few minutes. If accurate time values are important to you, you need a better Now() function. We can build one on top of the UTCnow() function we just wrote.


ASP

  1. function atomicnow()
  2.     dim utc
  3.     dim offset
  4.     utc = utcnow()
  5.     ' The order of the dates is important here!
  6.     offset = DateDiff("h", utc, now())
  7.     atomicnow = DateAdd("h", offset, utc)
  8. end function

There is expected to be some lag caused by this function, but the order of magnitude should only be milliseconds.


View implementation on Snipplr

Saturday, November 1, 2008

Nth Day

This week I want to share with you a function that I personally got a lot of use out of. Several years ago I had a situation where there was a calendar with several hundreds recurring events. It was desired to have these events fall on roughly the same day each year. The person responsible for this was spending two or three days at the end of each year planning all the occurrences for the next year on a giant wet-eraseable calendar.


Here's what all the fuss was about: Last year, November 1 was a Thursday. This year, November 1 is a Saturday. Saturday is not a working day for most people, so these events we are scheduling need to fall on week days. Because it fell on a Thursday last year, we'd prefer to have it on a Thursday again this year. This means either pushing it back to October 30 or forward to November 6.


To make the event occur on the same day of the week, we'll store this information in our database and use it to generate the date for a given year. This is called the Nth Day. In the above example, our Nth Day is the first Thursday in November.


ASP

  1. function nthDay(someYear, someMonth, someWeek, someWeekday)
  2.     dim firstDay
  3.     dim someDay
  4.     firstDay = weekday(dateSerial(someYear, someMonth, 1))
  5.     ' Check if the week day of the first day of the month is before or after the given week day.
  6.     if (someWeekday - firstDay) >= 0 then
  7.         someDay = 1 + (someWeekday - firstDay) + ((someWeek - 1) * 7)
  8.     else
  9.         someDay = 1 + (someWeekday - firstDay) + (someWeek * 7)
  10.     end if
  11.     nthDay = dateSerial(someYear, someMonth, someDay)
  12. end function

If developing a system around this concept like I did, it's important to know that some months have a fifth week, depending on how late in the week the first day of the month is. You can't really schedule anything recurring during the fifth week. We kept it open both to give the people conducting the events a little time off, as well as for a place to move an event from the following month in situations where the customer thought it would be too late in the year otherwise.


An idea I had to improve the system, but didn't get around to exploring, was to ignore the months entirely and use the weeks of the year. Nobody wants to schedule anything around Christmas and New Year's anyway, so you don't run into problems around the beginning/end of the year like you do at the beginning/end of a month. Under this system, first Thursday of November becomes Thursday of Week 44. Scheduling a recurring event this way is not only easier, but also more consistent from year to year.


View ASP implementation on Snipplr

Saturday, June 21, 2008

AM/PM

To those of you who have been bored by the recent string of math-related articles, I apologize. I still have a lot of math-related functions to share with you, but I do have other things to share with you as well, such as validation functions, string functions, and datetime functions. This week we'll take a look at one of those datetime functions.


In ASP, time values are normally in the format HH:MM:SS followed by AM or PM. Quite often, we don't want to include the seconds portion, so we extract the hours and minutes using the Hour() and Minute() functions respectively. Unfortunately, there is no quick function for extracting the AM/PM. Let's do something about that right now.


  1. function ampm(someTime)
  2.     if hour(someTime) < 12 then
  3.         ampm = "AM"
  4.     else
  5.         ampm = "PM"
  6.     end if
  7. end function

Now we can do stuff like this:
Hour(someTime) & ":" & Minute(someTime) & " " & ampm(someTime)


Those of you who are PHP programmers are hopefully aware that the same result can be achieved in PHP using the built-in date() function and specifying the letter A or a in the format string for uppercase or lowercase respectively:
date("g:i A", $timestamp)


View this code on Snipplr

Saturday, March 22, 2008

Calculating Easter

It's Easter weekend, and as promised we're going to write a function to calculate when Easter occurs for a given year. Easter is a strange holiday in that it jumps around the calendar between March and April. This is because it is tied to the lunar cycle. Easter occurs on the first Sunday after the Paschal full moon.


ASP

  1. function dateEaster(someYear)
  2.     Dim goldenNumber
  3.     Dim solarCorrection
  4.     Dim lunarCorrection
  5.     Dim paschalFullMoon
  6.     Dim dominicalNumber
  7.     Dim difference
  8.     Dim dayEaster
  9.     goldenNumber = (someYear Mod 19) + 1
  10.     if someYear <= 1752 then
  11.         ' Julian calendar
  12.         dominicalNumber = (someYear + (someYear / 4) + 5) Mod 7
  13.         paschalFullMoon = (3 - (11 * goldenNumber) - 7) Mod 30
  14.     else
  15.         ' Gregorian calendar
  16.         dominicalNumber = (someYear + (someYear / 4) - (someYear / 100) + (someYear / 400)) Mod 7
  17.         solarCorrection = (someYear - 1600) / 100 - (someYear - 1600) / 400
  18.         lunarCorrection = (((someYear - 1400) / 100) * 8) / 25
  19.         paschalFullMoon = (3 - 11 * goldenNumber + solarCorrection - lunarCorrection) Mod 30
  20.     end if
  21.     do until dominicalNumber > 0
  22.         dominicalNumber = dominicalNumber + 7
  23.     loop
  24.     do until paschalFullMoon > 0
  25.         paschalFullMoon = paschalFullMoon + 30
  26.     loop
  27.     if paschalFullMoon = 29 or (paschalFullMoon = 28 and goldenNumber > 11) then
  28.         paschalFullMoon = paschalFullMoon - 1
  29.     end if
  30.     difference = (4 - paschalFullMoon - dominicalNumber) Mod 7
  31.     if difference < 0 then
  32.         difference = difference + 7
  33.     end if
  34.     dayEaster = paschalFullMoon + difference + 1
  35.     if dayEaster < 11 then
  36.         ' Easter occurs in March.
  37.         dateEaster = DateSerial(someYear, 3, dayEaster + 21)
  38.     else
  39.         ' Easter occurs in April.
  40.         dateEaster = DateSerial(someYear, 4, dayEaster - 10)
  41.     end if
  42. end function

PHP

  1. function dateEaster($someYear)
  2. {
  3.     $goldenNumber = fmod($someYear, 19) + 1;
  4.     if ($someYear <= 1752)
  5.     {
  6.         // Julian calendar
  7.         $dominicalNumber = fmod($someYear + ($someYear / 4) + 5, 7);
  8.         $paschalFullMoon = fmod(3 - (11 * $goldenNumber) - 7, 30);
  9.     }
  10.     else
  11.     {
  12.         // Gregorian calendar
  13.         $dominicalNumber = fmod($someYear + ($someYear / 4) - ($someYear / 100) + ($someYear / 400), 7);
  14.         $solarCorrection = ($someYear - 1600) / 100 - ($someYear - 1600) / 400;
  15.         $lunarCorrection = ((($someYear - 1400) / 100) * 8) / 25;
  16.         $paschalFullMoon = fmod(3 - 11 * $goldenNumber + $solarCorrection - $lunarCorrection, 30);
  17.     }
  18.     while ($dominicalNumber < 0)
  19.     {
  20.         $dominicalNumber += 7;
  21.     }
  22.     while ($paschalFullMoon < 0)
  23.     {
  24.         $paschalFullMoon += 30;
  25.     }
  26.     if ($paschalFullMoon == 29 || ($paschalFullMoon == 28 && $goldenNumber > 11))
  27.     {
  28.         $paschalFullMoon--;
  29.     }
  30.     $difference = fmod(4 - $paschalFullMoon - $dominicalNumber, 7);
  31.     if ($difference < 0)
  32.     {
  33.         $difference += 7;
  34.     }
  35.     $dayEaster = $paschalFullMoon + $difference + 1;
  36.     if ($dayEaster < 11)
  37.     {
  38.         // Easter occurs in March.
  39.         $dateEaster = mktime(0, 0, 0, 3, $dayEaster + 21, $someYear);
  40.     }
  41.     else
  42.     {
  43.         // Easter occurs in April.
  44.         $dateEaster = mktime(0, 0, 0, 4, $dayEaster - 10, $someYear);
  45.     }
  46.     return $dateEaster;
  47. }

We can calculate the other ecclesiastical holidays by offsetting the date of Easter with the number of days between Easter and the holiday we're looking for.


ASP

  1. function dateGoodFriday(someYear)
  2.     dateGoodFriday = DateAdd("d", -2, dateEaster(someYear))
  3. end function
  4. function datePalmSunday(someYear)
  5.     datePalmSunday = DateAdd("d", -7, dateEaster(someYear))
  6. end function
  7. function dateAshWednesday(someYear)
  8.     dateAshWednesday = DateAdd("d", -46, dateEaster(someYear))
  9. end function
  10. function dateAscensionDay(someYear)
  11.     dateAscensionDay = DateAdd("d", 39, dateEaster(someYear))
  12. end function
  13. function datePentecost(someYear)
  14.     datePentecost = DateAdd("d", 49, dateEaster(someYear))
  15. end function

PHP

  1. function dateGoodFriday($someYear)
  2. {
  3.     $easter = getDate(dateEaster($someYear));
  4.     return mktime(0, 0, 0, $easter[mon], $easter[mday] - 2, $easter[year]);
  5. }
  6. function datePalmSunday($someYear)
  7. {
  8.     $easter = getDate(dateEaster($someYear));
  9.     return mktime(0, 0, 0, $easter[mon], $easter[mday] - 7, $easter[year]);
  10. }
  11. function dateAshWednesday($someYear)
  12. {
  13.     $easter = getDate(dateEaster($someYear));
  14.     return mktime(0, 0, 0, $easter[mon], $easter[mday] - 46, $easter[year]);
  15. }
  16. function dateAscensionDay($someYear)
  17. {
  18.     $easter = getDate(dateEaster($someYear));
  19.     return mktime(0, 0, 0, $easter[mon], $easter[mday] + 39, $easter[year]);
  20. }
  21. function datePentecost($someYear)
  22. {
  23.     $easter = getDate(dateEaster($someYear));
  24.     return mktime(0, 0, 0, $easter[mon], $easter[mday] + 49, $easter[year]);
  25. }

Sunday, March 2, 2008

Leap Years

This year (2008) is a leap year, and this past week contained February 29. It seems only fitting that we talk about calculating leap years and what we can do with that information.


A leap year is always divisible by four, but not by one hundred unless it is also divisible by four hundred.


ASP

  1. function isLeapYear(someYear)
  2.     if someYear Mod 4 = 0 and (someYear Mod 100 <> 0 or (someYear Mod 100 = 0 and someYear Mod 400 = 0)) then
  3.         isLeapYear = True
  4.     else
  5.         isLeapYear = False
  6.     end if
  7. end function

PHP

  1. function isLeapYear($someYear)
  2. {
  3.     return date("L", strtotime($someYear . "-01-01"));
  4. }

Now let's write a function to build on this which returns the number of days in a given month.


ASP

  1. function MonthDays(someMonth, someYear)
  2.     select case someMonth
  3.     case 1, 3, 5, 7, 8, 10, 12
  4.         MonthDays = 31
  5.     case 4, 6, 9, 11
  6.         MonthDays = 30
  7.     case 2
  8.         if isLeapYear(someYear) then
  9.             MonthDays = 29
  10.         else
  11.             MonthDays = 28
  12.         end if
  13.     end select
  14. end function

PHP

  1. function MonthDays($someMonth, $someYear)
  2. {
  3.     return date("t", strtotime($someYear . "-" . $someMonth . "-01"));
  4. }

UPDATE: Thanks to Jim Mayes for showing me more elegant PHP solutions.