Skip to content

CkGeo

Job: read latitudes and longitudes from raw data, and do the arithmetic of the globe — distance, direction, midpoint, radius checks — with no map service involved.

A coordinate is two numbers: latitude (how far north or south, -90 to 90) and longitude (how far east or west, -180 to 180). CkGeo reads them from a number, from decimal text like "40.7128", from degrees-minutes-seconds text like "40°42'46"N", or from a whole point written the way the spatial standards write one — "POINT(-74.006 40.7128)" or {"type":"Point","coordinates":[-74.006,40.7128]} — and then works with them: how far apart two points are, which way to head, what lies halfway. When a read fails, the answer is the North Pole (latitude 90, longitude 0) — a real place where nobody lives, so bad data stands out on a map instead of hiding at (0, 0), which is a real spot in the ocean off Africa. When a calculation is handed a bad coordinate, the answer is NaN ("not a number"), which no real distance or bearing is.

Quick look

using SafeCoder.CommonKit;
using SafeCoder.CommonKit.Enums;

CkGeo.Latitude("40.7128");      // -> 40.7128
CkGeo.Longitude("-74.006");     // -> -74.006
CkGeo.Latitude("junk");         // -> 90  (the North Pole — rejected)
CkGeo.Distance(
    latitude1: 40.7128,
    longitude1: -74.0060,
    latitude2: 51.5074,
    longitude2: -0.1278
);                              // -> 5570.229873656523  (New York to London, in km)
CkGeo.IsCoordinate("40.7128, -74.0060");   // -> true
CkGeo.IsPlaceholder(0, 0);                 // -> true  (Null Island: a failed geocode)
CkGeo.ToWkt(40.7128, -74.0060);            // -> "POINT(-74.006 40.7128)"  (longitude first)
CkGeo.BoundsAround(40.7128, -74.0060, 50); // -> (South ≈ 40.26, West ≈ -74.6, North ≈ 41.16, East ≈ -73.41)
CkGeo.Destination(0, 0, 90, 111.2);        // -> (0, ≈ 1)  (a degree east along the equator)
CkGeo.Explain("91");   // -> "outside -90 to 90"

Methods

Latitude and Longitude share their rules. Each reads one value: a number of any type, decimal text, degrees-minutes-seconds text with a hemisphere letter (N, S, E or W), or a whole point in WKT or GeoJSON, out of which each takes its own half. A letter from the wrong axis is refused — an "N" in a longitude field means someone swapped the columns, and CkGeo will not paper over it.

When the value cannot be read, the fallback is tried — and checked the same way, so an impossible default is no fallback. When both fail, Latitude answers 90 and Longitude answers 0: the two halves of the North Pole.

Latitude

How far north or south.

double Latitude(
    object? input = null,
    double? fallback = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input object? null The raw value: a number of any type, decimal text, or degrees-minutes-seconds text.
fallback double? null Latitude to use when the value cannot be read. It is checked the same way, so an impossible default is no fallback.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkGeoException that says why, instead of answering the North Pole, when neither the value nor the fallback can be used.

Returns

When Default With throwIfInvalid: true
The value (or the fallback) is a readable latitude The latitude, held to -90…90: 40.7128 The latitude, held to -90…90: 40.7128
Rejected: missing, unreadable, past a pole, or a wrong-axis letter — and the fallback fails too 90 — the North Pole Throws CkGeoException — carries Input, Expected and Reason

A DMS value with an E or W letter is a longitude in the wrong field — refused, not read.

Latitude holds the result to -90…90 — nothing is past the poles.

CkGeo.Latitude("40.7128");        // -> 40.7128
CkGeo.Latitude("40°42'46\"N");    // -> 40.71277777777778
CkGeo.Latitude("91");             // -> 90  (no latitude is past the pole — rejected)
CkGeo.Latitude(
    input: "abc",
    fallback: 0
);                                // -> 0

Longitude

How far east or west.

double Longitude(
    object? input = null,
    double? fallback = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input object? null The raw value: a number of any type, decimal text, or degrees-minutes-seconds text.
fallback double? null Longitude to use when the value cannot be read. It is checked the same way, so an impossible default is no fallback.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkGeoException that says why, instead of answering 0 — the North Pole's longitude — when neither the value nor the fallback can be used.

Returns

When Default With throwIfInvalid: true
The value (or the fallback) is a readable longitude The longitude, held to -180…180: -74.006 The longitude, held to -180…180: -74.006
Rejected: missing, unreadable, past 180, or a wrong-axis letter — and the fallback fails too 0 — the North Pole's longitude Throws CkGeoException — carries Input, Expected and Reason

A DMS value with an N or S letter is a latitude in the wrong field — refused, not read.

Works like Latitude, but Longitude holds the result to -180…180, and a rejected value answers 0 — longitude's half of the pole.

CkGeo.Longitude("-74.006");       // -> -74.006
CkGeo.Longitude("40°42'46\"N");   // -> 0  (an N letter is a latitude — wrong field)

The two OrNull twins are the same read, but the answer for a missing value (null or DBNull) or an unreadable one is null instead of the pole — made for nullable database columns, which should receive an honest NULL, never a stand-in and never NaN. Unlike the rest of the OrNull family (see CkNumber) there is no zero switch here: the equator and the Greenwich meridian are real places, so 0 always comes back as 0.

LatitudeOrNull

Null when there is nothing usable.

double? LatitudeOrNull(object? input = null)
Parameter Type Default What it does
input object? null The raw value: a number of any type, decimal text, or degrees-minutes-seconds text.

Returns

When You get
The value is a readable latitude The latitude: 40.7128
Missing (null or DBNull), unreadable, past a pole, or a wrong-axis letter null — this method never throws

There is no zero switch here: 0 is the equator, a real place, and always comes back as 0.

The nullable read for a latitude: anything Latitude would reject to the pole comes back as null instead.

CkGeo.LatitudeOrNull("40.7128");   // -> 40.7128
CkGeo.LatitudeOrNull(null);        // -> null
CkGeo.LatitudeOrNull("junk");      // -> null
CkGeo.LatitudeOrNull("0");         // -> 0  (the equator is a real place)

LongitudeOrNull

Null for a longitude.

double? LongitudeOrNull(object? input = null)
Parameter Type Default What it does
input object? null The raw value: a number of any type, decimal text, or degrees-minutes-seconds text.

Returns

When You get
The value is a readable longitude The longitude: -74.006
Missing (null or DBNull), unreadable, past 180, or a wrong-axis letter null — this method never throws

There is no zero switch here: 0 is the Greenwich meridian, a real place, and always comes back as 0.

Works like LatitudeOrNull, for the east-west number.

CkGeo.LongitudeOrNull("-74.006");  // -> -74.006
CkGeo.LongitudeOrNull("181");      // -> null  (no longitude goes past 180)

TryParse

A whole "lat, lon" pair in one call.

bool TryParse(
    string? input,
    out double latitude,
    out double longitude
)
Parameter Type Default What it does
input string? — The text holding the pair, latitude first.
latitude out double — The latitude read out; 90 (the North Pole's latitude) when the text could not be read.
longitude out double — The longitude read out; 0 (the North Pole's longitude) when the text could not be read.

Returns

When You get
Both halves read as a latitude and a longitude true — latitude and longitude carry the pair: 40.7128 and -74.006
The text is a whole point in WKT, EWKT or GeoJSON true — the two halves, swapped back into latitude, longitude
Anything else: missing text, not exactly two halves, or either half rejected false — the outputs hold the North Pole (90 and 0); it never throws

The true/false return is the whole answer — the library's one bool-verdict method.

Reads both halves from one piece of text — separated by a space, a comma or a semicolon, with or without wrapping brackets. This is the library's one bool-verdict method: the true/false return is the whole answer, and the coordinates ride out through the two out parameters. On false the outputs hold the North Pole. It never throws.

bool ok = CkGeo.TryParse(
    input: "40.7128, -74.0060",
    latitude: out double lat,
    longitude: out double lon
);   // -> true  (lat is 40.7128, lon is -74.006)

CkGeo.TryParse(
    input: "91 0",
    latitude: out double badLat,
    longitude: out double badLon
);   // -> false  (badLat is 90 and badLon is 0 — the North Pole)

One catch: because a space separates the two halves, a degrees-minutes-seconds half must be written without spaces here — "40°42'46"N", not "40 42 46 N". The spaced form is fine for DmsToDecimal, which reads one angle on its own.

Points from a database or a map

A whole point written the way the spatial standards write one is read too — the shapes ToWkt and ToGeoJson write, plus the variants other systems emit:

CkGeo.TryParse("POINT(-74.0060 40.7128)", out double a, out double b);        // -> true  (40.7128, -74.006)
CkGeo.TryParse("POINT (-74.0060 40.7128)", out _, out _);                     // a space before the bracket
CkGeo.TryParse("POINT Z (-74.0060 40.7128 12.5)", out _, out _);              // the height is read past
CkGeo.TryParse("SRID=4326;POINT(-74.0060 40.7128)", out _, out _);            // the extended form
CkGeo.TryParse("{\"type\":\"Point\",\"coordinates\":[-74.0060,40.7128]}", out _, out _);
CkGeo.TryParse("SRID=3857;POINT(-8238310 4970071)", out _, out _);            // -> false

Longitude first

WKT and GeoJSON both put the longitude before the latitude, which is the opposite of the order people say a coordinate in and the mistake these formats invite. TryParse swaps them back, so what comes out is latitude first as everywhere else in this class — and ToWkt and ToGeoJson take latitude first and write longitude first. Never reorder by hand.

Two rules go with them. A value that opens with POINT, SRID= or { is judged as a point and only as a point: a bad one answers false, never retried as a plain pair. And of the extended form, only SRID=4326 — WGS 84 — is a latitude and a longitude; every other reference is metres on some projection, and is refused.

Latitude and Longitude read the same shapes and take their own half out of one, which is what makes reading a stored geography column a single call.

Distance

How far apart two points are.

double Distance(
    double latitude1,
    double longitude1,
    double latitude2,
    double longitude2,
    GeoUnit unit = GeoUnit.Kilometers,
    double fallback = double.NaN,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
latitude1 double — Latitude of the first point, -90 to 90.
longitude1 double — Longitude of the first point, -180 to 180.
latitude2 double — Latitude of the second point.
longitude2 double — Longitude of the second point.
unit GeoUnit GeoUnit.Kilometers The unit of the result; kilometres by default.
fallback double double.NaN What comes back when a coordinate is invalid; NaN by default, which no real distance is.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkGeoException that says which coordinate is invalid and why, instead of answering the fallback.

Returns

When Default With throwIfInvalid: true
All four coordinates are valid The great-circle distance in unit, 0 for the same point: 5570.229873656523 (New York to London, kilometres) The great-circle distance in unit, 0 for the same point: 5570.229873656523 (New York to London, kilometres)
Rejected: a coordinate is NaN, infinite or outside its range The fallback (default NaN) Throws CkGeoException — carries Input, Expected and Reason

Good to about 0.3% — a perfect sphere stands in for the slightly flattened real Earth.

The great-circle distance: the shortest path over the surface of the globe, the way a plane flies. Pick the unit with GeoUnit; kilometres is the default. A bad coordinate answers the fallback, which is NaN unless you pass your own.

CkGeo.Distance(
    latitude1: 40.7128,
    longitude1: -74.0060,
    latitude2: 51.5074,
    longitude2: -0.1278
);   // -> 5570.229873656523  (New York to London, kilometres)

CkGeo.Distance(
    latitude1: 40.7128,
    longitude1: -74.0060,
    latitude2: 51.5074,
    longitude2: -0.1278,
    unit: GeoUnit.Miles
);   // -> 3461.1803776299676

CkGeo.Distance(
    latitude1: 91,
    longitude1: 0,
    latitude2: 0,
    longitude2: 0
);   // -> NaN  (91 is not a latitude)

Bearing

The compass direction to set off in.

double Bearing(
    double latitude1,
    double longitude1,
    double latitude2,
    double longitude2,
    double fallback = double.NaN,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
latitude1 double — Latitude of the starting point, -90 to 90.
longitude1 double — Longitude of the starting point, -180 to 180.
latitude2 double — Latitude of the destination.
longitude2 double — Longitude of the destination.
fallback double double.NaN What comes back when a coordinate is invalid; NaN by default, which no real bearing is.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkGeoException that says which coordinate is invalid and why, instead of answering the fallback.

Returns

When Default With throwIfInvalid: true
All four coordinates are valid The initial bearing in degrees clockwise from north, 0 to just under 360: 51.2126168241972 The initial bearing in degrees clockwise from north, 0 to just under 360: 51.2126168241972
Rejected: a coordinate is NaN, infinite or outside its range The fallback (default NaN) Throws CkGeoException — carries Input, Expected and Reason

The same point twice answers 0, not NaN.

The direction from the first point to the second, in degrees clockwise from north: 0 is north, 90 east, 180 south, 270 west. It is the initial bearing — on a long trip along the great circle the direction changes as you go; this is the one at the start.

CkGeo.Bearing(
    latitude1: 40.7128,
    longitude1: -74.0060,
    latitude2: 51.5074,
    longitude2: -0.1278
);   // -> 51.2126168241972  (set off north-east for London)

Midpoint

Halfway along the path.

(double Latitude, double Longitude) Midpoint(
    double latitude1,
    double longitude1,
    double latitude2,
    double longitude2,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
latitude1 double — Latitude of the first point, -90 to 90.
longitude1 double — Longitude of the first point, -180 to 180.
latitude2 double — Latitude of the second point.
longitude2 double — Longitude of the second point.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkGeoException that says which coordinate is invalid and why, instead of answering the North Pole.

Returns

When Default With throwIfInvalid: true
All four coordinates are valid The halfway point as a (Latitude, Longitude) pair: (52.36843958746243, -41.29030735624855) The halfway point as a (Latitude, Longitude) pair: (52.36843958746243, -41.29030735624855)
Rejected: a coordinate is NaN, infinite or outside its range The North Pole pair (90, 0) Throws CkGeoException — carries Input, Expected and Reason

Halfway along the great circle — not the average of the numbers.

The point halfway between two points along the great circle — not the average of the numbers, which drifts off the true path over long distances. It comes back as a (Latitude, Longitude) pair; a bad coordinate answers the North Pole pair (90, 0).

CkGeo.Midpoint(
    latitude1: 40.7128,
    longitude1: -74.0060,
    latitude2: 51.5074,
    longitude2: -0.1278
);   // -> (52.36843958746243, -41.29030735624855)  (out in the Atlantic)

Destination

Where a bearing and a distance take you.

(double Latitude, double Longitude) Destination(
    double latitude,
    double longitude,
    double bearing,
    double distance,
    GeoUnit unit = GeoUnit.Kilometers,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
latitude double — Latitude of the starting point, -90 to 90.
longitude double — Longitude of the starting point, -180 to 180.
bearing double — The initial bearing, degrees clockwise from north. Any real number; 370 is 10, -350 is 10.
distance double — How far to travel, in unit. Zero stays where it is; a negative distance is rejected.
unit GeoUnit GeoUnit.Kilometers Kilometers, Miles, NauticalMiles or Meters.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkGeoException that says what is invalid and why, instead of answering the North Pole.

Returns

When Default With throwIfInvalid: true
The start, the bearing and the distance are all usable The point reached, as a (Latitude, Longitude) pair, its longitude wrapped into -180 to 180 The same pair
Rejected: a coordinate out of range, a bearing or a distance that is NaN or infinite, a negative distance The North Pole pair (90, 0) Throws CkGeoException — carries Input, Expected and Reason

The inverse of Distance and Bearing, on the same sphere.

Take the bearing and the distance from A to B, hand them back here with A, and B comes out. Useful for drawing the edge of a radius, for offsetting a point a known way, and for checking that a distance and a bearing agree.

CkGeo.Destination(
    latitude: 40.7128,
    longitude: -74.0060,
    bearing: 51.2,
    distance: 5570
);   // -> about (51.52, -0.13) — London

CkGeo.Destination(0, 0, 90, 111.19492664455873);   // -> about (0, 1) — a degree east
CkGeo.Destination(0, 0, 370, 100);                 // -> the same as bearing 10
CkGeo.Destination(0, 179.5, 90, 100);              // -> about (0, -179.6) — across the antimeridian

The bearing is the one you start with, not one you hold: a great circle turns as you go, which is why a flight from New York to London leaves heading north-east and arrives heading south-east.

BoundsAround

The box around a radius.

(double South, double West, double North, double East) BoundsAround(
    double latitude,
    double longitude,
    double distance,
    GeoUnit unit = GeoUnit.Kilometers,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
latitude double — Latitude of the centre, -90 to 90.
longitude double — Longitude of the centre, -180 to 180.
distance double — The radius, in unit. Zero gives the centre alone; a negative radius is rejected.
unit GeoUnit GeoUnit.Kilometers Kilometers, Miles, NauticalMiles or Meters.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkGeoException that says what is invalid and why, instead of answering NaN.

Returns

When Default With throwIfInvalid: true
The centre and the radius are usable The four edges as a (South, West, North, East) tuple — the order IsInBounds takes The same tuple
Rejected: a coordinate out of range, a radius that is NaN, infinite or negative (NaN, NaN, NaN, NaN) Throws CkGeoException — carries Input, Expected and Reason

The cheap half of a "within radius" query. IsWithin is the exact half.

The "within radius" query, in two steps

"Everyone within 50 km of the office" over a table of coordinates is two steps, and both matter. BoundsAround gives a box an index can use, so the database narrows millions of rows to a few hundred; IsWithin then measures those few and throws away the corners of the box that lie outside the circle.

(double south, double west, double north, double east) =
    CkGeo.BoundsAround(40.7128, -74.0060, 50);

// 1. the prefilter, in SQL, where an index on (Lat, Lon) can do the work
//    WHERE Lat BETWEEN @south AND @north AND Lon BETWEEN @west AND @east

// 2. the exact check, in code, on what comes back
List<Member> near = rows
    .Where(row => CkGeo.IsWithin(40.7128, -74.0060, row.Lat, row.Lon, 50))
    .ToList();

Never use the box on its own: its corners are up to 40% further out than the radius, so a box-only answer includes people who are not within 50 km. And never skip it: without a box the database reads every row.

Three shapes of box to know about, all of which IsInBounds already reads:

  • A box that wraps the antimeridian comes back with West east of East — 170 and -170, say. IsInBounds understands it; a hand-written SQL BETWEEN does not, and needs two ranges joined by OR.
  • A circle that reaches a pole has no meaningful east or west, so the box spans every longitude, -180 to 180, and the pole's own latitude.
  • The box is always a little wider than the circle — a corner of a box is outside the circle it holds, and every edge carries CkSettings.BoundsMarginDegrees (a millionth of a degree, about eleven centimetres) on top, so that a point lying exactly on the radius can never round its way out of the box that is meant to hold it.

ToWkt

The point as well-known text.

string ToWkt(
    double latitude,
    double longitude,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
latitude double — The latitude, -90 to 90.
longitude double — The longitude, -180 to 180.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkGeoException that says which coordinate is invalid and why, instead of answering "".

Returns

When Default With throwIfInvalid: true
Both coordinates are valid "POINT(-74.006 40.7128)" — longitude first The same text
Rejected: a coordinate is NaN, infinite or outside its range Empty string Throws CkGeoException — carries Input, Expected and Reason

Longitude first, as the standard says. TryParse reads it back.

The spelling every spatial database and mapping library reads. The parameters are latitude first, as everywhere else in this class; the text that comes out is longitude first, as the standard requires.

CkGeo.ToWkt(40.7128, -74.0060);   // -> "POINT(-74.006 40.7128)"
CkGeo.ToWkt(0, 0);                // -> "POINT(0 0)"
CkGeo.ToWkt(91, 0);               // -> ""  (not a latitude)

Plain WKT, with no SRID in front: the value is WGS 84, which is what a column declared geography in SQL Server or geography(Point,4326) in PostGIS already means. Where a receiver wants the extended form, write "SRID=4326;" + CkGeo.ToWkt(lat, lon) — TryParse reads that back too.

ToGeoJson

The point as a GeoJSON object.

string ToGeoJson(
    double latitude,
    double longitude,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
latitude double — The latitude, -90 to 90.
longitude double — The longitude, -180 to 180.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkGeoException that says which coordinate is invalid and why, instead of answering "".

Returns

When Default With throwIfInvalid: true
Both coordinates are valid {"type":"Point","coordinates":[-74.006,40.7128]} — longitude first, no spaces The same text
Rejected: a coordinate is NaN, infinite or outside its range Empty string Throws CkGeoException — carries Input, Expected and Reason

Longitude first, as GeoJSON says. TryParse reads it back.

The shape a web map, a browser API and a document database expect. No spaces, so the text goes straight into a payload.

CkGeo.ToGeoJson(40.7128, -74.0060);   // -> "{\"type\":\"Point\",\"coordinates\":[-74.006,40.7128]}"
CkGeo.ToGeoJson(0, 181);              // -> ""  (not a longitude)

GeoJSON has no coordinate system of its own — it is WGS 84 by definition, which is what this class reads and writes. The numbers are written by CkText.NumberToString, so they are the same under every culture and read back as the values that went in.

IsWithin

Inside the radius?

bool IsWithin(
    double latitude1,
    double longitude1,
    double latitude2,
    double longitude2,
    double distance,
    GeoUnit unit = GeoUnit.Kilometers,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
latitude1 double — Latitude of the centre, -90 to 90.
longitude1 double — Longitude of the centre, -180 to 180.
latitude2 double — Latitude of the point to test.
longitude2 double — Longitude of the point to test.
distance double — The radius, inclusive, in unit.
unit GeoUnit GeoUnit.Kilometers The unit of distance; kilometres by default.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkGeoException that says what is invalid and why, instead of answering false.

Returns

When Default With throwIfInvalid: true
All coordinates and the radius are valid The verdict — true when the point is no further than distance, false when it is further The verdict — true when the point is no further than distance, false when it is further
Rejected: a coordinate is NaN, infinite or outside its range, or the radius is NaN, infinite or negative false Throws CkGeoException — carries Input, Expected and Reason

The radius is inclusive — a point exactly on the edge is within.

Answers whether the second point lies within distance of the first — "is this member within 50 miles of the office". The radius is inclusive, in the GeoUnit you pick.

CkGeo.IsWithin(
    latitude1: 40.7128,
    longitude1: -74.0060,
    latitude2: 40.0583,
    longitude2: -74.4057,
    distance: 50,
    unit: GeoUnit.Miles
);   // -> true  (Trenton is within 50 miles of New York)

A bad coordinate or a negative radius answers false too — so where "false because the data is broken" must not look like "false because they are far away", pass throwIfInvalid: true or check the coordinates first.

The last two methods carry an angle both ways between decimal degrees and DMS. DMS is the map-and-survey way of writing an angle: whole degrees, then minutes (a minute is 1/60 of a degree), then seconds (1/60 of a minute), plus a hemisphere letter.

DmsToDecimal

Degrees, minutes and seconds into a decimal.

double DmsToDecimal(
    string? input = null,
    double? fallback = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input string? null The DMS text.
fallback double? null Decimal degrees to use when the text cannot be read. It is checked too: NaN, infinite or beyond ±180 is no fallback.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkGeoException that says why, instead of answering the North Pole, when neither the text nor the fallback can be used.

Returns

When Default With throwIfInvalid: true
The text (or the fallback) is a readable angle The decimal degrees: 40.71277777777778 The decimal degrees: 40.71277777777778
Rejected: not DMS text, a minus against an N or E letter, a fraction anywhere but the last part, minutes or seconds at 60 or more, or degrees past the limit — and the fallback fails too 90 — the North Pole Throws CkGeoException — carries Input, Expected and Reason

The fallback is checked too: NaN, infinite or beyond ±180 is no fallback.

DmsToDecimal reads DMS text — symbols, spaces or colons between the parts, letter before or after — into plain decimal degrees. It reads one angle without knowing which axis it belongs to, so it allows anything up to ±180 — though an N or S letter still caps it at 90; Latitude and Longitude read DMS too and enforce the axis.

CkGeo.DmsToDecimal("40°42'46\"N");   // -> 40.71277777777778
CkGeo.DmsToDecimal("40 42 46 N");    // -> 40.71277777777778  (spaced form is fine here)
CkGeo.DmsToDecimal("-74 0 22");      // -> -74.00611111111111  (a minus works like W or S)
CkGeo.DmsToDecimal("junk");          // -> 90  (the North Pole)

DecimalToDms

A decimal back into degrees, minutes and seconds.

string DecimalToDms(
    double degrees,
    GeoAxis axis = GeoAxis.Latitude,
    int secondsDecimals = CkSettings.DmsSecondsDecimals,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
degrees double — The coordinate in decimal degrees.
axis GeoAxis GeoAxis.Latitude Latitude (N/S, -90 to 90) or longitude (E/W, -180 to 180).
secondsDecimals int CkSettings.DmsSecondsDecimals (1) Decimal places of the seconds, 0 to 6; values outside that are capped.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkGeoException that says why, instead of answering "", when the value cannot be written.

Returns

When Default With throwIfInvalid: true
The value fits the axis The DMS text with its hemisphere letter: "40°42'46.1"N" The DMS text with its hemisphere letter: "40°42'46.1"N"
Rejected: NaN, infinite, or outside the axis' range Empty string Throws CkGeoException — carries Input, Expected and Reason

Rounded seconds carry into the minutes and degrees when they reach 60.

DecimalToDms writes decimal degrees back out as DMS text; tell it the axis with GeoAxis (latitude is the default), and set the seconds' decimal places with secondsDecimals (default 1, capped at 0 to 6). It is the one string method here, so it rejects the string way: non-empty in, empty out (see How it works).

CkGeo.DecimalToDms(40.7128);         // -> "40°42'46.1"N"
CkGeo.DecimalToDms(
    degrees: -74.006,
    axis: GeoAxis.Longitude
);                                   // -> "74°0'21.6"W"
CkGeo.DecimalToDms(91);              // -> ""  (past the pole — rejected)

IsLatitude

Does it read as a latitude?

bool IsLatitude(
    object? input = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input object? null The value: a number, or text — decimal or degrees, minutes and seconds.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkGeoException that says why, instead of answering false, when the value is not a latitude.

Returns

When Default With throwIfInvalid: true
A number, decimal text or DMS text inside -90 to 90 true true
Rejected: out of range, minutes or seconds of 60 or more, junk, missing false Throws CkGeoException — carries Input, Expected and Reason

The yes-or-no form of Latitude: anything that reader accepts, and nothing it rejects. Call the reader instead when you want the decimal degrees back.

CkGeo.IsLatitude("40°42'46\"N");   // -> true
CkGeo.IsLatitude("90.0001");        // -> false

IsLongitude

Does it read as a longitude?

bool IsLongitude(
    object? input = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input object? null The value: a number, or text — decimal or degrees, minutes and seconds.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkGeoException that says why, instead of answering false, when the value is not a longitude.

Returns

When Default With throwIfInvalid: true
A number, decimal text or DMS text inside -180 to 180 true true
Rejected: out of range, malformed DMS, junk, missing false Throws CkGeoException — carries Input, Expected and Reason

The yes-or-no form of Longitude.

CkGeo.IsLongitude("74°00'22\"W");   // -> true
CkGeo.IsLongitude(181);              // -> false

IsCoordinate

Does the text read as a whole pair?

bool IsCoordinate(
    string? input = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input string? null The raw text: latitude, then longitude, separated by a comma, a semicolon or a space, brackets allowed around the pair.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkGeoException that says why, instead of answering false, when the text is not a coordinate pair.

Returns

When Default With throwIfInvalid: true
Two halves that read as a latitude and a longitude true true
Rejected: one half, three parts, a half out of range, junk false Throws CkGeoException — carries Input, Expected and Reason — Expected says whether the pair, the latitude or the longitude failed

The named twin of TryParse, for validation code, with a throwIfInvalid that says which half failed and why, which TryParse cannot. Latitude comes first; a pair written longitude-first still reads when both halves happen to be in range, since the text does not say which is which.

CkGeo.IsCoordinate("40.7128, -74.0060");           // -> true
CkGeo.IsCoordinate("40°42'46\"N 74°00'22\"W");     // -> true
CkGeo.IsCoordinate("91, -74.0060");                // -> false  (the latitude is out of range)

IsInBounds

Inside this box?

bool IsInBounds(
    double latitude,
    double longitude,
    double south,
    double west,
    double north,
    double east,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
latitude double — Latitude of the point, -90 to 90.
longitude double — Longitude of the point, -180 to 180.
south double — The southern edge of the box, inclusive.
west double — The western edge, inclusive.
north double — The northern edge, inclusive.
east double — The eastern edge, inclusive.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkGeoException that says why, instead of answering false, when a coordinate is invalid or the edges contradict each other.

Returns

When Default With throwIfInvalid: true
Every coordinate is valid and the point lies inside the box, edges included true true
Every coordinate is valid and the point lies outside false false
Rejected: an invalid coordinate, or a southern edge north of the northern one false Throws CkGeoException — carries Input, Expected and Reason — Expected names the coordinate or edge

The service-area and map-viewport check: "is this address in our delivery region". A box whose western edge is east of its eastern edge wraps across the antimeridian, so a box around the date line works. Like Distance and IsWithin it takes numbers, since a box is built from stored coordinates, not raw text.

CkGeo.IsInBounds(
    latitude: 40.7128,
    longitude: -74.0060,
    south: 40.4,
    west: -74.3,
    north: 40.9,
    east: -73.7
);   // -> true  (Manhattan, inside New York's box)
CkGeo.IsInBounds(0, 180, south: -10, west: 170, north: 10, east: -170);   // -> true  (across the antimeridian)

IsPlaceholder

A failed geocode rather than a place?

bool IsPlaceholder(
    double latitude,
    double longitude,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
latitude double — The latitude, -90 to 90.
longitude double — The longitude, -180 to 180.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkGeoException that says why, instead of answering false, when a coordinate is invalid.

Returns

When Default With throwIfInvalid: true
The North Pole — latitude 90, whatever the longitude — or Null Island, (0, 0) true true
Any other valid place false false
Rejected: an invalid coordinate false Throws CkGeoException — carries Input, Expected and Reason

The two "nobody entered anything" locations: this library's own rejection value, and the (0, 0) other systems emit when a geocoder failed. The CkDate.IsPlaceholder of coordinates, for finding bad rows before they land on a map. A latitude of 90 with a real longitude is the pole too — it is what a record holds when only its latitude failed to read. A genuine reading at exactly (0, 0) is a buoy in the Gulf of Guinea; in business data it is always a failed geocode.

CkGeo.IsPlaceholder(90, 0);                // -> true   (the rejection value)
CkGeo.IsPlaceholder(0, 0);                 // -> true   (Null Island)
CkGeo.IsPlaceholder(51.5, 0);              // -> false  (Greenwich is real)

Explain

Why not — in words, without an exception.

string Explain(
    object? input = null,
    GeoAxis axis = GeoAxis.Latitude
)
Parameter Type Default What it does
input object? null The value: a number, or text.
axis GeoAxis GeoAxis.Latitude Which coordinate it is meant to be.

Returns

When You get
The value passes the reader ""
It does not The reason, the same words the exception would carry — an answer, not a failure; it never throws

The form-friendly twin of throwIfInvalid: the message to show beside the field, without a try and catch. Every class with a reader has one, on the reader's own rules, so the reason is exactly what the reader would say.

CkGeo.Explain("91");                        // -> "outside -90 to 90"
CkGeo.Explain("181", GeoAxis.Longitude);    // -> "outside -180 to 180"

Good to know

  • Failed reads answer the North Pole, not zero. (0, 0) is a real spot in the Gulf of Guinea where bad data would hide; (90, 0) is a place no honest record points at. A longitude read on its own rejects to 0 — that is its half of the pole pair, since at the pole every longitude is the same place.
  • Failed calculations answer NaN. Distance and Bearing return the caller's fallback, NaN by default — and NaN is a loud value: any comparison with it is false, so a bad distance can never accidentally pass a "less than 50" check.
  • The OrNull twins have no zero switch. The rest of the OrNull family treats zero as a sentinel by default; here 0 latitude is the equator and 0 longitude is Greenwich — real places, always kept.
  • Fallbacks are checked too. A fallback of 91 for a latitude is no fallback; you still get the pole.
  • TryParse hands back the pair; IsCoordinate only judges it. Both read the same way; TryParse never throws and gives you the numbers, IsCoordinate can throw and says which half failed. The other validators — IsLatitude, IsLongitude, IsInBounds, IsPlaceholder, IsWithin — all answer false for anything invalid and take throwIfInvalid.
  • IsPlaceholder catches both sentinels. The library's North Pole and the (0, 0) other systems emit; check it before plotting, since both are real spots on a map.
  • Distance is good to about 0.3%. The maths uses a perfect sphere; the real Earth is slightly flattened. Plenty for "who is within 50 miles", not for surveying. Destination uses the same sphere, so it and Distance agree with each other exactly.
  • WKT and GeoJSON put the longitude first. Both formats do, and ToWkt, ToGeoJson, TryParse, Latitude and Longitude all take and give latitude first, as the rest of this class does — the swap happens inside. Never reorder by hand.
  • A "within radius" query is BoundsAround then IsWithin — the box for the index, the circle for the answer. See BoundsAround.
  • Two small enums ride along, both in SafeCoder.CommonKit.Enums: GeoUnit (Kilometers — the default — Miles, NauticalMiles, Meters) and GeoAxis (Latitude, Longitude). You saw both above: unit: GeoUnit.Miles, axis: GeoAxis.Longitude.

If you want errors instead

Pass throwIfInvalid: true and a value that cannot be used raises a CkGeoException (from SafeCoder.CommonKit.Exceptions) instead of the fallback. The exception carries the Input, what was Expected (which of the four coordinates, for the calculations) and a short Reason, flattened into a one-line message.

CkGeo.Latitude(
    input: "abc",
    throwIfInvalid: true
);   // throws CkGeoException — the message says why

Two exceptions to the rule: TryParse takes no throwIfInvalid — its false is the whole signal — and the OrNull twins never throw at all: null is their answer for anything unusable, not an error. When a bad coordinate should be an error, read it with Latitude or Longitude and throwIfInvalid: true instead.