Here’s an unpopular opinion: the word jdate should be retired from data pipelines. It causes more silent bugs than it solves problems, because two completely different things share the name – and no two libraries agree on which one you meant.
If you’ve ever downloaded a Landsat scene, opened a legacy stock-trading feed, or tried to reconcile timestamps from an astronomy dataset, you’ve hit this. One tool says jdate = 2460496. Another says jdate = 187. Both are correct. Neither is wrong. That’s the whole problem.
The two things called jdate (and why your ML pipeline cares)
There are two date encodings that share the nickname “Julian date,” and mixing them up is the single most common date bug I’ve seen in geospatial ML code.
- Ordinal day-of-year: An integer from 1 to 366. February 15 = 046. This is what
date +%jreturns on Linux, what Landsat filenames embed, and what most people mean when they say “jdate” in a business context. - Astronomical Julian Day Number (JDN): A running day count since Julian Jan 1, 4713 BCE. Today’s value is somewhere north of 2,460,000. This is what astropy, astronomy, and the Linux
jdayutility use.
A Linux Mint forum thread that digs into this references Wikipedia’s recommendation directly: ordinal date is the preferred name for what was formerly called “Julian date” or JDATE – still seen in old programming languages and spreadsheet software – and the older names are deprecated precisely because they’re so easily confused with the Julian Day Number. If you’re writing new code, use “ordinal day-of-year” for the small number and “JDN” for the big one. Your future self will thank you.
Why this matters for AI data pipelines
Satellite imagery is the classic case. Modern ML models for land cover, crop monitoring, and change detection eat Landsat and Sentinel-2 tiles by the terabyte, and both encode dates using the ordinal (small-number) version.
Here’s what an HLS (Harmonized Landsat-Sentinel) filename looks like, straight from NASA Earthdata’s spec:
HLS.S30.T60HTE.2022103T222539.v2.0.B01.tif
In this HLS tile, 2022103 encodes the Julian Date of Acquisition as YYYYDDD – the 103rd day of 2022. Not JDN. Not YYYYMMDD. Ordinal, 7-digit, packed.
Now compare it to a Collection-1 Landsat scene, where USGS uses an explicit YYYYMMDD acquisition date in the scene ID. Same satellite program. Different date format. If your data-loader regex was written for one and you feed it the other, it will either crash or – worse – silently parse the wrong bytes as a year.
Parsing jdate in Python – the version that actually holds up
Most tutorials show you datetime.strptime(s, '%Y%j') and call it a day. That works for clean input. Real pipelines have dirty input.
from datetime import datetime, date
def parse_ordinal_jdate(s: str) -> date:
"""Parse a YYYYDDD or YYDDD ordinal 'Julian' date."""
s = s.strip()
if len(s) == 7: # YYYYDDD (HLS, modern Landsat scene IDs)
return datetime.strptime(s, '%Y%j').date()
if len(s) == 5: # YYDDD (legacy NLAPS filenames)
yy, ddd = int(s[:2]), int(s[2:])
year = 2000 + yy if yy < 70 else 1900 + yy
return date(year, 1, 1).replace(day=1)
.fromordinal(date(year, 1, 1).toordinal() + ddd - 1)
raise ValueError(f"Not a recognizable ordinal jdate: {s!r}")
>>> parse_ordinal_jdate('2022103')
datetime.date(2022, 4, 13)
>>> parse_ordinal_jdate('99001')
datetime.date(1999, 1, 1)
The 5-digit case matters more than you’d think. According to NASA’s own USGS README documentation, older NLAPS-processed Landsat files use the format LLNppprrrOOYYDDDMM_AA.TIF, where YY is the last two digits of the year and DDD is the Julian date of acquisition. The same archive can hand you both 5-digit and 7-digit ordinals depending on when the file was processed. A single regex won’t catch both.
The astronomical JDN version – when you actually need it
If you’re doing anything with orbital mechanics, telescope schedules, or timestamps from astronomical instruments, you want the big number. The juliandate package handles it cleanly (as of 2025):
import juliandate as jd
from datetime import datetime
# Gregorian → JDN
jd.from_gregorian(2024, 7, 5, 12, 0, 0)
# 2460497.0
# JDN → Gregorian
datetime(*jd.to_gregorian(2460497.0))
# datetime.datetime(2024, 7, 5, 12, 0, 0)
The conversion formulas used by juliandate – drawn from Seidelmann’s Explanatory Supplement to the Astronomical Almanac – are valid only for JDN ≥ 0, meaning calculations cannot be considered correct for dates before Julian January 1, 4713 BCE. For anything post-Bronze-Age that’s fine, but if you’re training a model on ancient astronomical records, read the docs first.
For high-precision work, astropy’s Time object is the scientific standard. As of astropy 6.x, it maintains time as a pair of double-precision numbers whose sum equals the Julian Date, preserving precision across the full range. Overkill for most ML work – necessary for anything scientific.
Quick diagnostic: if your training data has a column literally named
jdate, don’t guess. Inspect one row. Under 400 → ordinal day-of-year. Five digits starting with a decade code → YYDDD. Over 2,000,000 → JDN. Three seconds of inspection beats three hours debugging a misaligned time series.
It’s a bit strange that this ambiguity has persisted for decades – the two formats differ by a factor of roughly 13,000, yet the same four-letter abbreviation covers both. The real reason is historical inertia: different communities (astronomers vs. manufacturing/government systems vs. satellite ops) developed their own “Julian date” conventions independently and never had a reason to reconcile the terminology. By the time anyone noticed the collision, the name was baked into too many file specs and codebases to change.
Pitfalls competitors don’t mention
The year 2100 problem. The Gregorian calendar excludes years that are multiples of 100 but not multiples of 400 – so 1700, 1800, 1900, and 2100 are not leap years, but 2000 is. Naive ordinal date libraries that assume “year % 4 == 0 → leap year” will drift after Feb 29, 2100. That bug is already sitting in older codebases. If your model has to project past 2100 (climate forecasting, long-range planning), audit your date math now.
The AIX gotcha is nastier because it fails silently. The bash idiom date -d "2024-07-05" +%j works fine on Linux – but as of 2025, the -d flag is not supported in AIX. Move a containerized ML pipeline from a Linux build environment to an AIX production server and the date conversion step produces nothing, or errors out, with no obvious clue why. Either bundle GNU coreutils in your container image or do the conversion in Python where behavior is consistent.
Bash on Linux – for reference:
$ date +%j
187
$ date -d "2024-07-05" +%j
187
$ date -d "2024-12-31" +%j
366
How jdate compares to alternatives
You have three practical choices when storing dates in a data pipeline. Each has a real cost:
| Format | Example | Pro | Con |
|---|---|---|---|
| ISO 8601 (YYYY-MM-DD) | 2024-07-05 | Human-readable, universal parser support | 10 characters, no time zone by default |
| Ordinal jdate (YYYYDDD) | 2024187 | 7 characters, sorts chronologically, matches satellite filenames | Ambiguous with JDN, requires custom parser |
| Unix epoch | 1720137600 (example) | Precise, arithmetic-friendly, no ambiguity | Not human-readable at all |
| Astronomical JDN | 2460497 | Scientific-standard, sub-day precision via decimals | Overkill for most ML, easy to confuse with ordinal jdate |
For AI pipelines, my rule: store ISO 8601 or Unix epoch internally. Only convert to jdate when writing filenames or interfacing with a legacy system that demands it. Don’t propagate the ambiguity.
FAQ
Is jdate the same as the Julian calendar?
No. The Julian calendar is a completely separate topic – a 46 BCE calendar reform still used by some Orthodox churches. “jdate” in software refers to ordinal day-of-year or the astronomical Julian Day Number, full stop.
How do I convert a Landsat filename’s jdate to a real date in pandas?
Start with pd.to_datetime(df['jdate'], format='%Y%j') – that handles 7-digit YYYYDDD values cleanly. The harder problem is the 5-digit YYDDD case. Pandas won’t guess the century, and the wrong guess (1922 instead of 2022) will silently corrupt your time series. Prepend the century before parsing: if the first two digits are less than 70, prepend “20”; otherwise prepend “19”. That’s the convention most legacy Landsat tooling assumes, though you should verify it against a known acquisition date in your specific archive before trusting it at scale.
Which Python library should I use for astronomical Julian dates?
People sometimes assume juliandate and astropy.time are just different wrappers for the same thing. They’re not. The juliandate package does one thing – converts between Gregorian dates and JDN using the Fliegel & Van Flandern (1968) formula, the same one the U.S. Naval Observatory uses. It’s small, dependency-free, and correct. What it doesn’t do is handle time scales. The moment you need to distinguish UTC from TAI from Terrestrial Time, or you need sub-millisecond precision over centuries, juliandate isn’t enough. That’s when you reach for astropy.time, which stores time as two summed doubles precisely to avoid floating-point drift. For a typical ML feature-engineering pipeline pulling satellite timestamps? juliandate is all you need.
Next step
Open a Python REPL right now and run this: from datetime import date; date.today().timetuple().tm_yday. That’s your ordinal jdate for today. Now open one file in your current dataset and check whether the date column matches that shape. If it doesn’t, you’ve just found a parsing bug worth fixing before your next training run.