OpenStreetMap logo OpenStreetMap

LastUpdated v1.9_2025-06-08

Posted by rphyrin on 8 June 2025 in English. Last updated on 10 June 2025.

Recently, I needed to open my OpenStreetMap profile—just to right-click and save my own profile picture for use on another platform.

Thanks to the newly redesigned OSM profile layout, I was greeted by a few new statistics—one of which showed how many comments my last diary post had received. While I was busy grabbing my avatar, I couldn’t help but notice that my recent diary post had garnered quite a bit of discussion.

To my surprise, at least two commenters pointed out the same thing : they suggested it would be more intuitive if the value reflected actual months, rather than “something that roughly represents the progress of the year in base-10.”

That got me thinking—how hard would it be to convert that base-10 year-progress value into something closer to a conventional month (base-12)?


Step 1: Extract the Year

To compute the year from an OSM timestamp (Unix time), we start by offsetting it from a known reference point—specifically, the Unix timestamp for the start of the year 2000.

(osm_timestamp - 946692127) / 31556952
  • 946692127 is the Unix timestamp for Sat Jan 01 2000 02:02:07 GMT+0000. This value was arbitrarily chosen by me (high accuracy isn’t necessary; I just needed a reference point roughly around the year 2000).
  • 31556952 is the average number of seconds in a year (365.2425 days).

This gives us a floating-point number: the integer part is the year offset from 2000.

To extract the integer part (representing the year), we can substring the first 2 character:

substring(divided_by(osm_timestamp - 946692127, 31556952), 0, 2)

Oh wait!

While writing this post, I stumbled upon a small but interesting bug from the initial release.

Originally, the code extracted the first three characters of the computed year value. This worked fine for double-digit years like 2010 and beyond—10.5 would yield “10.”, which was sufficient for identifying the year and using the decimal as a makeshift separator between year and month.

But what happens with single-digit years like 2009 or 2008?

Let’s take 9.13 as an example. Extracting the first three characters gives us “9.1”. Suddenly, the “1”—which is actually part of the month progress—is mistakenly included in the year field. This creates a conflict when we later try to append the month portion. The decimal part bleeds into the year.

To resolve this, I changed the logic to extract only the first two characters instead of three. Here’s what that accomplishes:

  • For 10.75, we now get “10”
  • For 9.13, we get “9.” (including the decimal point)

While this fixes the bleed issue for single-digit years, it introduces a new one: we no longer have a reliable separator between the year and month for two-digit years.

To make the formatting consistent, we could consider explicitly inserting a separator—perhaps using concat() to append a symbol (like a dash or underscore) between year and month.


Step 2: Convert the Decimal Part to a Month (Base-12)

Next, we want to convert the fractional part of the year (which represents the progress through the year) into a month.

We do this by isolating the remainder (i.e., how far we are into the current year):

mod(osm_timestamp - 946692127, 31556952)

Then, we normalize that remainder by dividing it again by 31556952, which gives us a decimal between 0 and just under 1:

divided_by(mod(osm_timestamp - 946692127, 31556952), 31556952)

This division is important because the previous remainder is still in Unix time seconds. We need to convert it to a year-based unit by dividing it by the average number of seconds in a year. A simple dimensional analysis helps clarify this: Unix time has the unit “seconds,” and 31,556,952 has the unit “seconds per year.” So the division becomes: seconds / (seconds/year) = years. After this division, we get a decimal value between 0 and just under 1, representing the progress through the current year.

To scale this from base-10 to base-12, we multiply it by 12, and then add 1—since months start at 1 (January), not 0:

((divided_by(mod(osm_timestamp - 946692127, 31556952), 31556952) * 12) + 1)

Finally, we extract the first 2 characters to get the integer representation of the month:

substring(..., 0, 2)

Yes—two characters. Again. So this code will work fine in the two-digit scenario, but it will look slightly odd in the single-digit case. Specifically, if the extracted month is a single-digit number, an extra trailing dot will appear.


So the full expression becomes:

substring(((mod(osm_timestamp - 946692127, 31556952) / 31556952) * 12) + 1, 0, 2)

And here’s the updated source code:

way {
  text: auto;
  text: eval(concat(concat("20",substring(divided_by(osm_timestamp()-946692127,31556952),0,2)),"-",substring((divided_by(mod(osm_timestamp()-946692127,31556952),31556952)*12)+1,0,2)));
  text-halo-color: white;
  font-size:20px;
  text-anchor-horizontal:center;
  text-anchor-vertical:center;
  text-position:center;
}

Pushing the Update

The next step was to publish the updated style to the JOSM wiki. This part was surprisingly straightforward: I edited the relevant “repository” page, and within minutes, the changes were reflected on the main JOSM Styles wiki page.

Naturally, I wanted to verify that the update was actually pulled into my local JOSM installation. But when I checked—nothing had changed. The style still appeared to be the old version.

I tried the usual workaround: uninstalling and reinstalling the style. Still no luck—it stubbornly continued loading the outdated version.

Puzzled, I returned to the JOSM wiki and found a helpful section on how to force an update.

  1. Open Preferences
  2. Make sure Expert mode is enabled
  3. Open Advanced Preferences
  4. Search for keys starting with: mirror.https://josm.openstreetmap.de/josmfile?page=Styles/ (or just mirror. for other external sources)
  5. Select the style you want to update
  6. Click Reset at the bottom of the window
  7. Click OK to save and close
  8. Restart JOSM

After following these steps, the new version was finally loaded.


Note :

  • For the sake of simplicity, this month’s calculation is still quite rough. Expect some inaccuracies when the timestamp falls near the boundary between two consecutive months, as this calculation doesn’t account for varying month lengths, leap years, or leap seconds.
  • Because of the previously mentioned “substring the first two characters” logic, things can look slightly off when dealing with single-digit years (2009 and earlier) or single-digit months (September and earlier). Specifically, you might see an extra trailing dot in the output. For example, a timestamp from September 2009 could end up looking like “209.-9.”—though I haven’t tested that exact case yet (still looking for an OSM object modified around that time).

Yes, there are still some imperfections in a few rare edge cases, but I hope it will display the correct result most of the time.


Update : Someone just asked why the date is not properly shown on the road.

So, I think I need to use separate styles: one for areas (text-position: center) and another for highways (text-position: line).

Here’s the updated source code…

area {
  text: auto;
  text: eval(concat(concat("20",substring(divided_by(osm_timestamp()-946692127,31556952),0,2)),"-",substring((divided_by(mod(osm_timestamp()-946692127,31556952),31556952)*12)+1,0,2)));
  text-halo-color: white;
  font-size:20px;
  text-anchor-horizontal:center;
  text-anchor-vertical:center;
  text-position:center;
}

way[highway]{
  text: auto;
  text: eval(concat(concat("20",substring(divided_by(osm_timestamp()-946692127,31556952),0,2)),"-",substring((divided_by(mod(osm_timestamp()-946692127,31556952),31556952)*12)+1,0,2)));
  text-halo-color: white;
  font-size:20px;
  text-position:line;
}

Email icon Bluesky Icon Facebook Icon LinkedIn Icon Mastodon Icon Telegram Icon X Icon

Discussion

Log in to leave a comment