Fast and Accurate Leap Year calculation

Hi,
The forum has helped me a lot over the past several years, and I thought I would give something back. Hope you find this useful.

RouterOS 6.x version:

{
  # Fast and accurate Leap Year calculation (for RouterOS 6.x only)

  :local months [:toarray "jan,feb,mar,apr,may,jun,jul,aug,sep,oct,nov,dec"];
  :local CurrentDate [system clock get date];   # current date (mar/01/1981)
  :local yr [:tonum [:pick $CurrentDate 7 11]]; # current year
  :local mo [:pick $CurrentDate 0 3];           # current month (jan...dec)
  :local mm ([find key=$mo in=$months]+1);      # month (1...12)

  :local leap; # current leap year (true|false)
  :local days; # No_of days in the current month
  {
    :if (($yr&3!=0)||(($yr&15!=0)&&((($yr/25)*25)=$yr))) do={
      :set $leap false} else={:set $leap true}; :set $days 30
    :if ($mm!=2) do={:set $days ($days+(($mm+($mm>>3))&1))
      } else={:if (!$leap) do={:set $days ($days-2)
        } else={:set $days ($days-1)}}
  }
# testing
:put ( " Year: $yr  Leap: $leap " )
:put ( " Month: $mo  Days: $days " )
}

RouterOS v7.x version

{
  # Fast and accurate Leap Year calculation (for RouterOS 7.x only)

  :local months [:toarray "jan,feb,mar,apr,may,jun,jul,aug,sep,oct,nov,dec"];
  :local CurrentDate [system clock get date];   # current date (2025-11-28)
  :local yr [:tonum [:pick $CurrentDate 0 4]];  # current year
  :local mm [:tonum [:pick $CurrentDate 5 7]];  # month (1...12)
  :local mo ($months->[($mm-1)]);               # current month (jan...dec)

  :local leap; # current leap year (true|false)
  :local days; # No_of days in the current month
  {
    :if (($yr&3!=0)||(($yr&15!=0)&&((($yr/25)*25)=$yr))) do={
      :set $leap false} else={:set $leap true}; :set $days 30
    :if ($mm!=2) do={:set $days ($days+(($mm+($mm>>3))&1))
      } else={:if (!$leap) do={:set $days ($days-2)
        } else={:set $days ($days-1)}}
  }
# testing
:put ( " Year: $yr  Leap: $leap " )
:put ( " Month: $mo  Days: $days " )
}

RouterOS 6.x and 7.x

{
  # Fast and accurate Leap Year calculation (compatible with RouterOS .x / 7.x )

  :local fullVersion [/system package get routeros version];
  :local majorVersion [:pick $fullVersion 0 [:find $fullVersion "."]];
  :local CurrentDate; :local yr; :local mm; :local mo;
  :local months [:toarray "jan,feb,mar,apr,may,jun,jul,aug,sep,oct,nov,dec"];

  :if ($majorVersion <= 6) do={
      :set $CurrentDate [system clock get date];   # current date (mar/01/1981)
      :set $yr [:tonum [:pick $CurrentDate 7 11]]; # current year
      :set $mo [:pick $CurrentDate 0 3];           # current month (jan...dec)
      :set $mm ([find key=$mo in=$months]+1);      # month (1...12)
  }

  :if ($majorVersion >= 7) do={
      :set $CurrentDate [system clock get date];   # current date (2025-11-28)
      :set $yr [:tonum [:pick $CurrentDate 0 4]];  # current year
      :set $mm [:tonum [:pick $CurrentDate 5 7]];  # month (1...12)
      :set $mo ($months->($mm-1));                 # current month (jan...dec)
  }

  :local leap; # current leap year (true|false)
  :local days; # No_of days in the current month
  {
    :if (($yr&3!=0)||(($yr&15!=0)&&((($yr/25)*25)=$yr))) do={
      :set $leap false} else={:set $leap true}; :set $days 30
    :if ($mm!=2) do={:set $days ($days+(($mm+($mm>>3))&1))
      } else={:if (!$leap) do={:set $days ($days-2)
        } else={:set $days ($days-1)}}
  }
# testing
:put ( " Running RouterOS . $majorVersion ")
:put ( " Year: $yr  Leap: $leap " )
:put ( " Month: $mo  Days: $days " )
}

First thing, thank you for sharing your scripts. :slightly_smiling_face:

Then two questions:

  1. what is the actual use of these scripts? I mean, they seem (to my inexperienced eyes) self-contained, i.e. not a function or anyway something that can be called from another script?
  2. I see that you are using the "other" algorithm using bitwise operations to find out if a year is a leap year, not the most common one derived from:

Every year that is exactly divisible by four is a leap year , except for years that are exactly divisible by 100 , but these centurial years are leap years, if they are exactly divisible by 400 .

Was this choice made because of some reasons or only because you fancied it?

The topic is discussed at length here (only for the record):
https://stackoverflow.com/questions/3220163/how-to-find-leap-year-programmatically-in-c

  • With ($yr&3!=0) the condition is true if the value in $yr is not exactly divisible by 4.
  • The part ($yr&15!=0)&&((($yr/25)*25)=$yr) tests for when the value is divisible by 100 (by 25 and by 4 because if not by 4 then the previous ($yr&3!=0) is already taken) but not by 16 (which means value is also not divisible by 400).

So, this whole (($yr&3!=0)||(($yr&15!=0)&&((($yr/25)*25)=$yr))) is true if the number is not divisible by 4, or divisible by 100 but not by 400. Which is the inverse of

Normally when we are dealing with native code, then BITWISE AND is of course faster than an integer (modulo) division. However, we are talking from an interpreted script language here, and the tiny amount of time saved by the bit operation is probably overshadowed by order of magnitudes (hundreds of times) by the time spent to parse the extra characters and interpreting the resulting internal representation.

Which means it's only for "fancy" reasons.

Yep, I thought so, even because it is not like you are running this set of operations in the thousands per second.
The given thread on stackoverflow has results of a nice test (but on compiled C) about the speed of the various possible approaches, but obviously chosing the one or the other doesn't change anything in practice.

This said by someone that - to the bewildered look of many people - usually multiplies by 355 and divides by 113 to get Pi when calculating a circumference or the area of a circle :wink:.

Yup. Imagine all the string parsing so outweighs any "optimized" RouterOS script.

Now, all scripts get "trans-piled" into an intermediate language (IL) [or perhaps p-code or s-expressions be equally valid analogies). This is what you see in code datatype (like /system/script/environment or /environment/print) – this is done when you create a function variables, or save a script. And, this is why using :parse or :execute is significantly slower than functions/stored scripts in my observations – since it has to convert the string-based, user-facing script into the "IL" code everytime a script that runs using script-as-strings (vs when saving/storing a script).

Given that, and linear string parsing, using less characters is #1 way to making things "faster" - if one cared even though likely useless in nearly all practical applications for RSC. But to this point, one monolithic block that deals with both V6 and V7 is likely slower than having 3 functions: one that figures out V6 vs V7, then one function for V6 and another V7.

You should use a colon in :find to ensure you're not getting some command's find matching operator if somehow path was not root. And one could quibble about unnecessary semi-colons, too

Another example:

If you use :toarray, that's slower than just defining an array list type directly since the string is immutable.

:global strmonths do={:return [:toarray "jan,feb,mar,apr,may,jun,jul,aug,sep,oct,nov,dec"]}      
# vs
:global arrmonths do={:return ("jan","feb","mar","apr","may","jun","jul","aug","sep","oct","nov","dec")}

Calling using :time to track execution time, use toarray is twice as slow as directly defining an array:

:put [:time command={:put [$strmonths]}] 
jan;feb;mar;apr;may;jun;jul;aug;sep;oct;nov;dec
00:00:00.000414

:put [:time command={:put [$arrmonths]}] 
jan;feb;mar;apr;may;jun;jul;aug;sep;oct;nov;dec
00:00:00.000215

Obviously immaterial with one operation, but noting. More there is a lot of roam before getting to where bitwise optimizations matter.