Skip to content
← Back to Examples

ISO 6709 Geographic Coordinate Parser

RustRubydata formatsintermediate

Parse ISO 6709 geographic point locations (latitude, longitude, and optional altitude). ISO 6709 is the international standard for representing geographic coordinates.

Key Concepts

  • sign convention for hemispheres
  • latitude/longitude ranges
  • altitude support
  • coordinate reference systems
  • sexagesimal (DMS) format

Why Parse This Format?

ISO 6709 is the international standard for representing geographic point locations. It's used whenever you need to store, transmit, or display precise location data in a standardized format.

Use Cases

  • GPS navigation applications - storing waypoints and routes
  • Photo geotagging - embedding location metadata in images
  • Location APIs - exchanging coordinate data between systems
  • GIS databases - storing points of interest
  • Emergency services - transmitting exact locations

Input Format

ISO 6709 represents coordinates as signed decimal or sexagesimal (DMS) values. Latitude comes first (-90 to +90), followed by longitude (-180 to +180), optional altitude, and optional coordinate reference system.

±DD.DDDD±DDD.DDDD[±AAA.A][CRScode/]

Examples

+40.6894-074.0447VALID

Statue of Liberty - basic latitude/longitude in decimal degrees

+48.8584+002.2945VALID

Eiffel Tower - note longitude uses leading zeros for alignment

-90+000VALID

South Pole - negative latitude for southern hemisphere

+40.6894-074.0447+93.0VALID

Statue of Liberty with altitude (93 meters above sea level)

+48.8584+002.2945+330CRSWGS_84/VALID

Eiffel Tower with altitude (330m) and coordinate reference system (WGS 84)

+27.9881+086.9250+8848.86CRSWGS_84/VALID

Mount Everest summit - altitude 8848.86 meters

+40 41 21.84-074 02 40.92VALID

Statue of Liberty in sexagesimal (DMS) format: 40°41'21.84"N 74°02'40.92"W

+95-074INVALID

Invalid - latitude cannot exceed 90 degrees

Output Format

The parser returns a GeoCoordinate struct containing the parsed values. Latitude and longitude are stored as f64 (double-precision floats). Altitude and CRS are optional.

Structure

latitude

Latitude in decimal degrees. Positive = North, Negative = South. Range: -90.0 to +90.0

longitude

Longitude in decimal degrees. Positive = East, Negative = West. Range: -180.0 to +180.0

altitude

Optional altitude in meters above sea level. Positive = above, Negative = below

crs

Optional coordinate reference system identifier (e.g., 'WGS_84', 'EPSG_4326')

Visual Overview

ISO 6709 Geographic Coordinate Parser diagram

How the Code Works

This section explains how the parsing rules map to the format semantics.

Sign Parsing (Hemisphere Indicator)

.rule("lat_sign", str("+").or(str("-")))
.rule("lon_sign", str("+").or(str("-")))

The first character of each coordinate component indicates the hemisphere: The sign convention follows ISO 6709: `+` = North/East, `-` = South/West. This is fundamental to the format - coordinates are always explicitly signed, never implied by cardinal direction letters (N/S/E/W).

Decimal Degrees Format

.rule("decimal_deg", seq(vec![
    dynamic(re("[0-9]{1,2}")), // degrees
    dynamic(seq(vec![
        dynamic(str(".")),
        dynamic(re("[0-9]+")), // fraction
    ]).optional()),
]))

The most common format uses decimal degrees with optional fractional part: Latitude uses 1-2 digits (range 0-90), while longitude uses 1-3 digits (range 0-180). The fractional part is optional but provides precision.

Coordinate Structure

.rule("coordinate", seq(vec![
    // Latitude: ±DD.DDDD
    dynamic(str("+").or(str("-"))),
    dynamic(re("[0-9]{1,2}(\\.[0-9]+)?")),
    // Longitude: ±DDD.DDDD
    dynamic(str("+").or(str("-"))),
    dynamic(re("[0-9]{1,3}(\\.[0-9]+)?")),
    // Optional altitude
    dynamic(altitude_parser.optional()),
    // Optional CRS
    dynamic(crs_parser.optional()),
]))

The complete coordinate combines all components in sequence: The order is fixed: latitude, longitude, altitude (optional), CRS (optional).

Output Transformation

pub struct GeoCoordinate {
    pub latitude: f64,
    pub longitude: f64,
    pub altitude: Option<f64>,
    pub crs: Option<String>,
}

The parsed string is transformed into a typed Rust struct: Using `f64` provides ~15 decimal digits of precision, sufficient for sub-millimeter accuracy. `Option<T>` handles optional fields.

Output Transformation

pub struct GeoCoordinate {
    pub latitude: f64,
    pub longitude: f64,
    pub altitude: Option<f64>,
    pub crs: Option<String>,
}

The parsed string is transformed into a typed Rust struct: Using `f64` provides ~15 decimal digits of precision, sufficient for sub-millimeter accuracy. `Option<T>` handles optional fields.

Source Code

Rust
//! ISO 6709 Geographic Coordinate Parser Example
//!
//! This example demonstrates parsing ISO 6709 geographic point locations
//! (latitude/longitude with optional altitude).
//!
//! ISO 6709 is the standard for geographic point locations. Format:
//! - Latitude/Longitude: +40.6894-074.0447 (Statue of Liberty)
//! - With altitude: +40.6894-074.0447+93.0CRSWGS_84/
//! - Sexagesimal (DMS): +40 41 21.84-074 02 40.92
//!
//! Run with: cargo run --example iso-6709/basic --no-default-features

use parsanol::portable::{
    parser_dsl::{choice, dynamic, re, seq, str, GrammarBuilder, ParsletExt},
    AstArena, Grammar, PortableParser,
};
use serde::{Deserialize, Serialize};

/// Build ISO 6709 grammar
fn build_iso6709_grammar() -> Grammar {
    GrammarBuilder::new()
        // Sign: + for N/E, - for S/W
        .rule("lat_sign", str("+").or(str("-")))
        .rule("lon_sign", str("+").or(str("-")))
        // Decimal degrees
        .rule(
            "decimal_deg",
            seq(vec![
                dynamic(re("[0-9]{1,2}")), // degrees
                dynamic(
                    seq(vec![
                        dynamic(str(".")),
                        dynamic(re("[0-9]+")), // fraction
                    ])
                    .optional(),
                ),
            ]),
        )
        // Sexagesimal (degrees, minutes, seconds)
        .rule(
            "sexagesimal",
            seq(vec![
                dynamic(re("[0-9]{1,2}")), // degrees
                dynamic(
                    seq(vec![
                        dynamic(str(" ")),
                        dynamic(re("[0-9]{1,2}")), // minutes
                        dynamic(
                            seq(vec![
                                dynamic(str(" ")),
                                dynamic(re("[0-9]{1,2}")), // seconds
                                dynamic(
                                    seq(vec![
                                        dynamic(str(".")),
                                        dynamic(re("[0-9]+")), // sec fraction
                                    ])
                                    .optional(),
                                ),
                            ])
                            .optional(),
                        ),
                    ])
                    .optional(),
                ),
            ]),
        )
        // Latitude: -90 to +90
        .rule(
            "latitude",
            seq(vec![
                dynamic(str("+").or(str("-"))),
                dynamic(choice(vec![
                    // Decimal degrees: +DD.DDDD or +DD.DD
                    dynamic(re("[0-9]{1,2}(\\.[0-9]+)?")),
                    // Sexagesimal: +DD MM SS.ss
                    dynamic(re("[0-9]{1,2}( [0-9]{1,2}){0,2}(\\.[0-9]+)?")),
                ])),
            ]),
        )
        // Longitude: -180 to +180
        .rule(
            "longitude",
            seq(vec![
                dynamic(str("+").or(str("-"))),
                dynamic(choice(vec![
                    // Decimal degrees: +DDD.DDDD
                    dynamic(re("[0-9]{1,3}(\\.[0-9]+)?")),
                    // Sexagesimal: +DDD MM SS.ss
                    dynamic(re("[0-9]{1,3}( [0-9]{1,2}){0,2}(\\.[0-9]+)?")),
                ])),
            ]),
        )
        // Altitude (optional)
        .rule(
            "altitude",
            seq(vec![
                dynamic(str("+").or(str("-"))),
                dynamic(re("[0-9]+")),
                dynamic(seq(vec![dynamic(str(".")), dynamic(re("[0-9]+"))]).optional()),
            ]),
        )
        // Coordinate Reference System (optional)
        .rule(
            "crs",
            seq(vec![dynamic(str("CRS")), dynamic(re("[A-Z0-9_]+"))]),
        )
        // Complete coordinate
        .rule(
            "coordinate",
            seq(vec![
                // Latitude
                dynamic(str("+").or(str("-"))),
                dynamic(re("[0-9]{1,2}(\\.[0-9]+)?")),
                // Longitude
                dynamic(str("+").or(str("-"))),
                dynamic(re("[0-9]{1,3}(\\.[0-9]+)?")),
                // Optional altitude
                dynamic(
                    seq(vec![
                        dynamic(str("+").or(str("-"))),
                        dynamic(re("[0-9]+(\\.[0-9]+)?")),
                    ])
                    .optional(),
                ),
                // Optional CRS
                dynamic(
                    seq(vec![
                        dynamic(str("CRS")),
                        dynamic(re("[A-Z0-9_]+")),
                        dynamic(str("/")),
                    ])
                    .optional(),
                ),
            ]),
        )
        .build()
}

/// Parsed ISO 6709 coordinate
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GeoCoordinate {
    pub latitude: f64,
    pub longitude: f64,
    pub altitude: Option<f64>,
    pub crs: Option<String>,
}

/// Parse ISO 6709 coordinate string
pub fn parse_iso6709(input: &str) -> Result<String, String> {
    let grammar = build_iso6709_grammar();
    let mut arena = AstArena::for_input(input.len());
    let mut parser = PortableParser::new(&grammar, input, &mut arena);
    let _ast = parser
        .parse()
        .map_err(|e| format!("Parse error: {:?}", e))?;

    // Parse manually for structured output
    let input = input.trim();

    // Remove trailing slash if present
    let input = input.strip_suffix('/').unwrap_or(input);

    let mut chars = input.chars().peekable();
    let mut coord = GeoCoordinate {
        latitude: 0.0,
        longitude: 0.0,
        altitude: None,
        crs: None,
    };

    // Parse latitude
    let lat_sign = match chars.next() {
        Some('+') => 1.0,
        Some('-') => -1.0,
        _ => return Err("Expected +/- at start".into()),
    };

    let mut lat_str = String::new();
    while let Some(c) = chars.peek() {
        if *c == '+' || *c == '-' {
            break;
        }
        lat_str.push(chars.next().unwrap());
    }
    coord.latitude = lat_str
        .trim()
        .parse::<f64>()
        .map(|v| v * lat_sign)
        .map_err(|_| format!("Invalid latitude: {}", lat_str))?;

    // Parse longitude
    let lon_sign = match chars.next() {
        Some('+') => 1.0,
        Some('-') => -1.0,
        _ => return Err("Expected +/- before longitude".into()),
    };

    let mut lon_str = String::new();
    while let Some(c) = chars.peek() {
        if *c == '+' || *c == '-' || *c == 'C' {
            break;
        }
        lon_str.push(chars.next().unwrap());
    }
    coord.longitude = lon_str
        .trim()
        .parse::<f64>()
        .map(|v| v * lon_sign)
        .map_err(|_| format!("Invalid longitude: {}", lon_str))?;

    // Parse optional altitude
    if let Some(&c) = chars.peek() {
        if c == '+' || c == '-' {
            let alt_sign = match chars.next() {
                Some('+') => 1.0,
                Some('-') => -1.0,
                _ => unreachable!(),
            };

            let mut alt_str = String::new();
            while let Some(c) = chars.peek() {
                if *c == 'C' {
                    break;
                }
                alt_str.push(chars.next().unwrap());
            }
            coord.altitude = Some(
                alt_str
                    .trim()
                    .parse::<f64>()
                    .map(|v| v * alt_sign)
                    .map_err(|_| format!("Invalid altitude: {}", alt_str))?,
            );
        }
    }

    // Parse optional CRS
    let remaining: String = chars.collect();
    if let Some(stripped) = remaining.strip_prefix("CRS") {
        coord.crs = Some(stripped.to_string());
    }

    serde_json::to_string_pretty(&coord).map_err(|e| e.to_string())
}

/// Convert sexagesimal (DMS) to decimal degrees
pub fn dms_to_decimal(degrees: f64, minutes: f64, seconds: f64, sign: f64) -> f64 {
    sign * (degrees + minutes / 60.0 + seconds / 3600.0)
}

fn main() {
    println!("ISO 6709 Geographic Coordinate Parser");
    println!("======================================\n");

    let examples = [
        // Basic coordinates
        ("+40.6894-074.0447", "Statue of Liberty (decimal)"),
        ("+48.8584+002.2945", "Eiffel Tower"),
        ("-90+000", "South Pole"),
        ("+00+000", "Null Island (0,0)"),
        // With altitude
        ("+40.6894-074.0447+93.0", "Statue of Liberty with altitude"),
        // With CRS
        ("+48.8584+002.2945+330CRSWGS_84/", "Eiffel Tower with CRS"),
        // High altitude
        ("+27.9881+086.9250+8848.86CRSWGS_84/", "Mount Everest"),
        // Sexagesimal (DMS) format
        ("+40 41 21.84-074 02 40.92", "Statue of Liberty (DMS)"),
    ];

    for (input, description) in examples {
        println!("Input: {} ({})", input, description);
        match parse_iso6709(input) {
            Ok(json) => println!("Output:\n{}\n", json),
            Err(e) => println!("Error: {}\n", e),
        }
    }

    println!("ISO 6709 Format Reference:");
    println!("--------------------------");
    println!("  ±DD.DDDD±DDD.DDDD         Basic (lat, lon)");
    println!("  ±DD.DDDD±DDD.DDDD±AAA.A   With altitude");
    println!("  ±DD MM SS.ss±DDD MM SS.ss Sexagesimal (DMS)");
    println!("  ...CRSEPSG_CODE/          With coordinate reference system");
}
Ruby
# frozen_string_literal: true

# ISO 6709 Geographic Coordinate Parser - Ruby Implementation
#
# Parse ISO 6709 geographic point locations (latitude, longitude, altitude).
#
# Run with: ruby example/iso-6709/basic.rb

$LOAD_PATH.unshift "#{File.dirname(__FILE__)}/../lib"

require "parsanol/parslet"

# ISO 6709 coordinate parser
class Iso6709Parser < Parsanol::Parser
  root :coordinate

  # Sign: + for N/E, - for S/W
  rule(:lat_sign) { (str("+") | str("-")).as(:lat_sign) }
  rule(:lon_sign) { (str("+") | str("-")).as(:lon_sign) }

  # Decimal degrees: DD.DDDD or DDD.DDDD
  rule(:decimal_deg) do
    match("[0-9]").repeat(1, 2).as(:degrees) >>
      (str(".") >> match("[0-9]").repeat(1)).maybe.as(:fraction)
  end

  rule(:decimal_deg_3) do
    match("[0-9]").repeat(1, 3).as(:degrees) >>
      (str(".") >> match("[0-9]").repeat(1)).maybe.as(:fraction)
  end

  # Sexagesimal (DMS): DD MM SS.ss or DD MM
  rule(:sexagesimal) do
    match("[0-9]").repeat(1, 2).as(:degrees) >>
      (
        space >>
        match("[0-9]").repeat(1, 2).as(:minutes) >>
        (
          space >>
          match("[0-9]").repeat(1, 2).as(:seconds) >>
          (str(".") >> match("[0-9]").repeat(1)).maybe.as(:sec_fraction)
        ).maybe
      ).maybe
  end

  rule(:sexagesimal_3) do
    match("[0-9]").repeat(1, 3).as(:degrees) >>
      (
        space >>
        match("[0-9]").repeat(1, 2).as(:minutes) >>
        (
          space >>
          match("[0-9]").repeat(1, 2).as(:seconds) >>
          (str(".") >> match("[0-9]").repeat(1)).maybe.as(:sec_fraction)
        ).maybe
      ).maybe
  end

  # Latitude: -90 to +90
  rule(:latitude) do
    lat_sign >> (decimal_deg | sexagesimal).as(:latitude)
  end

  # Longitude: -180 to +180
  rule(:longitude) do
    lon_sign >> (decimal_deg_3 | sexagesimal_3).as(:longitude)
  end

  # Altitude (optional): +AAA.A or -AAA.A
  rule(:altitude) do
    (str("+") | str("-")).as(:alt_sign) >>
      match("[0-9]").repeat(1).as(:alt_value) >>
      (str(".") >> match("[0-9]").repeat(1)).maybe.as(:alt_fraction)
  end

  # Coordinate Reference System (optional): CRScode/
  rule(:crs) do
    str("CRS") >>
      match("[A-Z0-9_]").repeat(1).as(:crs) >>
      str("/")
  end

  # Complete coordinate
  rule(:coordinate) do
    latitude >>
      (space | str("")).maybe >>
      longitude >>
      altitude.maybe.as(:altitude) >>
      (str("/") >> crs).maybe.as(:crs_info)
  end

  rule(:space) { match('\s') }
end

# Coordinate result class
Coordinate = Struct.new(:lat_sign, :latitude, :lon_sign, :longitude, :altitude,
                        :crs) do
  def to_h
    {
      latitude: lat_value,
      longitude: lon_value,
      altitude: alt_value,
      crs: crs,
    }.compact
  end

  def lat_value
    return nil unless latitude

    val = degrees_to_decimal(latitude)
    lat_sign == "-" ? -val : val
  end

  def lon_value
    return nil unless longitude

    val = degrees_to_decimal(longitude)
    lon_sign == "-" ? -val : val
  end

  def alt_value
    return nil unless altitude

    val = altitude[:alt_value].to_f
    val += altitude[:alt_fraction].to_s.to_f if altitude[:alt_fraction]
    altitude[:alt_sign] == "-" ? -val : val
  end

  private

  def degrees_to_decimal(d)
    return 0.0 unless d

    deg = d[:degrees].to_i
    min = d[:minutes].to_s.to_i
    sec = d[:seconds].to_s.to_f
    sec += d[:sec_fraction].to_s.to_f if d[:sec_fraction]
    frac = d[:fraction].to_s.to_f

    deg + (min / 60.0) + (sec / 3600.0) + frac
  end
end

# Transform parse tree to Coordinate
class Iso6709Transform < Parsanol::Transform
  rule(
    lat_sign: simple(:ls),
    latitude: simple(:lat),
    lon_sign: simple(:lons),
    longitude: simple(:lon),
  ) do
    Coordinate.new(ls.to_s, lat, lons.to_s, lon, nil, nil)
  end

  rule(
    lat_sign: simple(:ls),
    latitude: simple(:lat),
    lon_sign: simple(:lons),
    longitude: simple(:lon),
    altitude: simple(:alt),
  ) do
    Coordinate.new(ls.to_s, lat, lons.to_s, lon, alt, nil)
  end

  rule(
    lat_sign: simple(:ls),
    latitude: simple(:lat),
    lon_sign: simple(:lons),
    longitude: simple(:lon),
    crs_info: simple(:crs),
  ) do
    Coordinate.new(ls.to_s, lat, lons.to_s, lon, nil, crs.to_s)
  end

  rule(
    lat_sign: simple(:ls),
    latitude: simple(:lat),
    lon_sign: simple(:lons),
    longitude: simple(:lon),
    altitude: simple(:alt),
    crs_info: simple(:crs),
  ) do
    Coordinate.new(ls.to_s, lat, lons.to_s, lon, alt, crs.to_s)
  end
end

# Parse and return Coordinate
def parse_coordinate(str)
  parser = Iso6709Parser.new
  transform = Iso6709Transform.new

  tree = parser.parse(str)
  transform.apply(tree)
rescue Parsanol::ParseError => e
  puts "Parse error: #{e.message}"
  nil
end

# Main demo
if __FILE__ == $PROGRAM_NAME
  puts "ISO 6709 Geographic Coordinate Parser"
  puts "=" * 50
  puts

  coordinates = [
    "+40.6894-074.0447",                    # Statue of Liberty
    "+48.8584+002.2945",                    # Eiffel Tower
    "-90+000",                              # South Pole
    "+27.9881+086.9250",                    # Mount Everest
    "+40 41 21.84-074 02 40.92", # Sexagesimal format
    "+48.8584+002.2945+330CRSWGS_84/", # With altitude and CRS
  ]

  coordinates.each do |coord_str|
    puts "Input: #{coord_str}"
    result = parse_coordinate(coord_str)
    if result
      puts "  Latitude:  #{result.lat_value}"
      puts "  Longitude: #{result.lon_value}"
      puts "  Altitude:  #{result.alt_value}" if result.alt_value
      puts "  CRS:       #{result.crs}" if result.crs
    end
    puts
  end

  # Validation examples
  puts "-" * 50
  puts "Validation examples:"
  puts

  ["+95-074", "+40.6894"].each do |invalid|
    puts "Invalid: #{invalid}"
    result = parse_coordinate(invalid)
    puts "  Result: #{result.inspect}"
    puts
  end
end