ISO 6709 Geographic Coordinate Parser
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.0447VALIDStatue of Liberty - basic latitude/longitude in decimal degrees
+48.8584+002.2945VALIDEiffel Tower - note longitude uses leading zeros for alignment
-90+000VALIDSouth Pole - negative latitude for southern hemisphere
+40.6894-074.0447+93.0VALIDStatue of Liberty with altitude (93 meters above sea level)
+48.8584+002.2945+330CRSWGS_84/VALIDEiffel Tower with altitude (330m) and coordinate reference system (WGS 84)
+27.9881+086.9250+8848.86CRSWGS_84/VALIDMount Everest summit - altitude 8848.86 meters
+40 41 21.84-074 02 40.92VALIDStatue of Liberty in sexagesimal (DMS) format: 40°41'21.84"N 74°02'40.92"W
+95-074INVALIDInvalid - 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
latitudeLatitude in decimal degrees. Positive = North, Negative = South. Range: -90.0 to +90.0
longitudeLongitude in decimal degrees. Positive = East, Negative = West. Range: -180.0 to +180.0
altitudeOptional altitude in meters above sea level. Positive = above, Negative = below
crsOptional coordinate reference system identifier (e.g., 'WGS_84', 'EPSG_4326')
Visual Overview
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
//! 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");
}
# 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