Parse and encode Two-Line Element sets.
TLE is the legacy fixed-width format for satellite orbital elements, designed for 80-column punch cards in the 1960s. Despite its age, it remains the most widely used format for distributing orbital data.
The format grammar lives in the Rust core (sidereon_core::astro::tle): fixed-width
field extraction and validation, the modulo-10 checksum, the "assumed decimal"
drag-term codec, per-field number formatting, and the two-digit-year pivot.
This module keeps the Sidereon API shape: it marshals the epoch between its native
DateTime and the (epoch_year, epoch_day_of_year) pair the core exposes,
applies input defaults, logs advisory checksum warnings, and maps errors.
Parsing
The parser is liberal in what it accepts:
- Trailing whitespace and extra characters are trimmed
- Leading dots in floats (
.123→0.123) - Spaces in numeric fields
Checksum validation is performed and reported but does not prevent parsing.
Examples
{:ok, elements} = Sidereon.Format.TLE.parse(line1, line2)
{:ok, {line1, line2}} = Sidereon.Format.TLE.encode(elements)
Summary
Types
Functions
@spec encode(Sidereon.Elements.t()) :: {:ok, {String.t(), String.t()}} | {:error, encode_error()}
Encode an %Sidereon.Elements{} struct as TLE-format strings.
Returns {:ok, {line1, line2}}: two 69-character strings with valid
checksums, or {:error, reason} for malformed elements. Round-trips are
character-exact for standard TLEs.
Examples
iex> l1 = "1 25544U 98067A 18184.80969102 .00001614 00000-0 31745-4 0 9993"
iex> l2 = "2 25544 51.6414 295.8524 0003435 262.6267 204.2868 15.54005638121106"
iex> {:ok, el} = Sidereon.Format.TLE.parse(l1, l2)
iex> {:ok, {gen_l1, gen_l2}} = Sidereon.Format.TLE.encode(el)
iex> gen_l1 == l1
true
iex> gen_l2 == l2
true
@spec encode!(Sidereon.Elements.t()) :: {String.t(), String.t()}
Like encode/1 but raises on malformed elements.
@spec parse(String.t(), String.t()) :: {:ok, Sidereon.Elements.t()} | {:error, String.t()}
Parse a two-line element set into an %Sidereon.Elements{} struct.
Returns {:ok, elements} or {:error, reason}.
Logs a warning if checksums are invalid but still parses.
Examples
iex> {:ok, el} = Sidereon.Format.TLE.parse(
...> "1 25544U 98067A 18184.80969102 .00001614 00000-0 31745-4 0 9993",
...> "2 25544 51.6414 295.8524 0003435 262.6267 204.2868 15.54005638121106"
...> )
iex> el.catalog_number
"25544"
iex> el.inclination_deg
51.6414
@spec parse_file(String.t()) :: {:ok, %{ satellites: [%{name: String.t(), tle: Sidereon.Elements.t()}], skipped: non_neg_integer() }}
Parse a multi-record TLE file (CelesTrak / Space-Track style).
Accepts the common variants in a single pass: bare two-line element sets,
three-line sets (a name line followed by lines 1 and 2), and CelesTrak
0 NAME name lines. Blank lines, CRLF endings, and surrounding whitespace are
tolerated.
Returns {:ok, %{satellites: [%{name: name, tle: %Sidereon.Elements{}}], skipped: n}}.
Each tle is a fully populated %Sidereon.Elements{} (with object_name set
to the record's name, or nil for a bare two-line set) ready for
Sidereon.propagate/2, Sidereon.look_angle/3, and friends. name is the
empty string for a bare two-line record. skipped counts complete records
that were found but failed SGP4 initialization, so an empty file
(satellites: [], skipped: 0) is distinguishable from a fully corrupt one.
Examples
iex> text = """
...> ISS (ZARYA)
...> 1 25544U 98067A 18184.80969102 .00001614 00000-0 31745-4 0 9993
...> 2 25544 51.6414 295.8524 0003435 262.6267 204.2868 15.54005638121106
...> """
iex> {:ok, %{satellites: [sat], skipped: 0}} = Sidereon.Format.TLE.parse_file(text)
iex> sat.name
"ISS (ZARYA)"
iex> sat.tle.catalog_number
"25544"