1 // Copyright 2014 Google Inc. All rights reserved.
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 
15 /**
16  * Convert locations to and from convenient short codes.
17  *
18  * Open Location Codes are short, ~10 character codes that can be used instead of street
19  * addresses. The codes can be generated and decoded offline, and use a reduced character set that
20  * minimises the chance of codes including words.
21  *
22  * Ported from the Java interface.
23  *
24  * @author Jiri Semecky
25  * @author Doug Rinckes
26  * @author Jan Jurzitza
27  */
28 module openlocationcode;
29 @safe:
30 
31 import std.algorithm;
32 import std.array;
33 import std.format;
34 import std.math;
35 import std.string;
36 
37 /// Provides a normal precision code, approximately 14x14 meters.
38 enum codePrecisionNormal = 10;
39 
40 /// The character set used to encode the values.
41 static immutable ubyte[] codeAlphabet = "23456789CFGHJMPQRVWX".representation;
42 
43 /// A separator used to break the code into two parts to aid memorability.
44 enum separator = '+';
45 
46 /// The character used to pad codes.
47 enum paddingCharacter = '0';
48 
49 /// The number of characters to place before the separator.
50 private enum separatorPosition = 8;
51 
52 /// The max number of digits to process in a plus code.
53 enum maxDigitCount = 15;
54 
55 /// The max number of characters in an open location code. This is the max number of digits plus a separator character.
56 enum maxOLCLength = maxDigitCount + 1;
57 
58 /// Maximum code length using just lat/lng pair encoding.
59 private enum pairCodeLength = 10;
60 
61 /// Number of digits in the grid coding section.
62 private enum gridCodeLength = maxDigitCount - pairCodeLength;
63 
64 /// The base to use to convert numbers to/from.
65 private enum encodingBase = cast(int) codeAlphabet.length;
66 
67 /// The maximum value for latitude in degrees.
68 private enum long latitudeMax = 90;
69 
70 /// The maximum value for longitude in degrees.
71 private enum long longitudeMax = 180;
72 
73 /// Number of columns in the grid refinement method.
74 private enum gridColumns = 4;
75 
76 /// Number of rows in the grid refinement method.
77 private enum gridRows = 5;
78 
79 /// Value to multiple latitude degrees to convert it to an integer with the maximum encoding
80 /// precision. I.e. encodingBase**3 * gridRows**gridCodeLength
81 private enum long latIntegerMultiplier = 8000 * 3125;
82 
83 /// Value to multiple longitude degrees to convert it to an integer with the maximum encoding
84 /// precision. I.e. encodingBase**3 * gridColumns**gridCodeLength
85 private enum long lngIntegerMultiplier = 8000 * 1024;
86 
87 /// Value of the most significant latitude digit after it has been converted to an integer.
88 private enum long latMspValue = latIntegerMultiplier * encodingBase * encodingBase;
89 
90 /// Value of the most significant longitude digit after it has been converted to an integer.
91 private enum long lngMspValue = lngIntegerMultiplier * encodingBase * encodingBase;
92 
93 /**
94  * Coordinates of a decoded Open Location Code.
95  *
96  * The coordinates include the latitude and longitude of the lower left and upper right corners
97  * and the center of the bounding box for the area the code represents.
98  */
99 struct OpenLocationCodeArea
100 {
101 @nogc nothrow pure:
102 	private double _southLatitude = 0;
103 	private double _westLongitude = 0;
104 	private double _northLatitude = 0;
105 	private double _eastLongitude = 0;
106 	private int _length;
107 
108 	/// Creats this coordinate area from min/max longitude and latitude and a code length.
109 	this(double southLatitude, double westLongitude, double northLatitude,
110 			double eastLongitude, int length)
111 	{
112 		_southLatitude = southLatitude;
113 		_westLongitude = westLongitude;
114 		_northLatitude = northLatitude;
115 		_eastLongitude = eastLongitude;
116 		_length = length;
117 	}
118 
119 	double southLatitude() const @property
120 	{
121 		return _southLatitude;
122 	}
123 
124 	double westLongitude() const @property
125 	{
126 		return _westLongitude;
127 	}
128 
129 	double latitudeHeight() const @property
130 	{
131 		return _northLatitude - _southLatitude;
132 	}
133 
134 	double longitudeWidth() const @property
135 	{
136 		return _eastLongitude - _westLongitude;
137 	}
138 
139 	double centerLatitude() const @property
140 	{
141 		return (_southLatitude + _northLatitude) * 0.5;
142 	}
143 
144 	double centerLongitude() const @property
145 	{
146 		return (_westLongitude + _eastLongitude) * 0.5;
147 	}
148 
149 	double northLatitude() const @property
150 	{
151 		return _northLatitude;
152 	}
153 
154 	double eastLongitude() const @property
155 	{
156 		return _eastLongitude;
157 	}
158 
159 	int length() const @property
160 	{
161 		return _length;
162 	}
163 
164 	/// Returns: `true` if this area contains the given coordinate.
165 	bool contains(double latitude, double longitude) const
166 	{
167 		return southLatitude <= latitude && latitude < northLatitude
168 			&& westLongitude <= longitude && longitude < eastLongitude;
169 	}
170 }
171 
172 /// Thrown in methods validating a user input string code.
173 class OLCFormatException : Exception
174 {
175 	///
176 	this(string msg, string file = __FILE__, size_t line = __LINE__, Throwable nextInChain = null) pure nothrow @nogc @safe
177 	{
178 		super(msg, file, line, nextInChain);
179 	}
180 }
181 
182 /// Represents an open location code.
183 struct OpenLocationCode
184 {
185 pure:
186 	/// The current code for the object
187 	private immutable(ubyte)[] _code; // note: ubyte[] because it's only ascii
188 
189 	string code() const @property nothrow @nogc @trusted
190 	{
191 		return cast(string) _code;
192 	}
193 
194 	/**
195 	 * Creates Open Location Code object for the provided code.
196 	 *
197 	 * Params:
198 	 * 	code = A valid OLC code. Can be a full code or a shortened code.
199 	 * Throws: $(D OLCFormatException) when the passed code is not valid.
200 	 */
201 	static OpenLocationCode fromString(string code)
202 	{
203 		code = code.toUpper;
204 		if (!isValidCode(code))
205 		{
206 			throw new OLCFormatException(
207 					format!"The provided code '%s' is not a valid Open Location Code."(code));
208 		}
209 		return fromTrustedString(code);
210 	}
211 
212 	/**
213 	 * Creates Open Location Code object for the provided code performing no validation. This should call isValidCode before and abort if not valid.
214 	 *
215 	 * Params:
216 	 * 	code = A valid OLC code. Can be a full code or a shortened code.
217 	 */
218 	static OpenLocationCode fromTrustedString(string code) nothrow @nogc
219 	{
220 		// trim to max length, valid check might allow more characters
221 		return OpenLocationCode(code.representation[0 .. min(maxOLCLength, $)]);
222 	}
223 
224 	/**
225 	 * Creates Open Location Code.
226 	 *
227 	 * Params:
228 	 * 	buffer = The character buffer to write to. (ASCII only = ubyte[]) Should be at least maxOLCLength in size to avoid crashes.
229 	 * 	latitude = The latitude in decimal degrees.
230 	 * 	longitude = The longitude in decimal degrees.
231 	 * 	codeLength = The desired number of digits in the code. Defaults to the default precision. Must be a valid code length, otherwise contract violation = assertion error.
232 	 */
233 	static OpenLocationCode encode(double latitude, double longitude,
234 			int codeLength = codePrecisionNormal) nothrow
235 	{
236 		ubyte[maxOLCLength] buffer;
237 		auto ret = encode(buffer[], latitude, longitude, codeLength);
238 		return OpenLocationCode(ret.idup);
239 	}
240 
241 	/// ditto
242 	static ubyte[] encode(scope return ubyte[] buffer, double latitude,
243 			double longitude, int codeLength = codePrecisionNormal) nothrow @nogc
244 	in(codeLength.isValidCodeLength, "Illegal code length")
245 	{
246 		// Limit the maximum number of digits in the code.
247 		codeLength = min(codeLength, maxDigitCount);
248 		// Ensure that latitude and longitude are valid.
249 		latitude = clipLatitude(latitude);
250 		longitude = normalizeLongitude(longitude);
251 
252 		// Latitude 90 needs to be adjusted to be just less, so the returned code can also be decoded.
253 		if (latitude == latitudeMax)
254 		{
255 			latitude = latitude - 0.9 * computeLatitudePrecision(codeLength);
256 		}
257 
258 		// Store the code - we build it in reverse and reorder it afterwards.
259 		auto len = 0;
260 
261 		// Compute the code.
262 		// This approach converts each value to an integer after multiplying it by
263 		// the final precision. This allows us to use only integer operations, so
264 		// avoiding any accumulation of floating point representation errors.
265 
266 		// Multiply values by their precision and convert to positive. Rounding
267 		// avoids/minimises errors due to floating point precision.
268 		long latVal = cast(long)(round((latitude + latitudeMax) * latIntegerMultiplier * 1e6) / 1e6);
269 		long lngVal = cast(long)(round((longitude + longitudeMax) * lngIntegerMultiplier * 1e6) / 1e6);
270 
271 		// Compute the grid part of the code if necessary.
272 		if (codeLength > pairCodeLength)
273 		{
274 			foreach (i; 0 .. gridCodeLength)
275 			{
276 				const latDigit = latVal % gridRows;
277 				const lngDigit = lngVal % gridColumns;
278 				int ndx = cast(int)(latDigit * gridColumns + lngDigit);
279 				buffer[len++] = codeAlphabet[ndx];
280 				latVal /= gridRows;
281 				lngVal /= gridColumns;
282 			}
283 		}
284 		else
285 		{
286 			latVal = cast(long)(latVal / pow(gridRows, gridCodeLength));
287 			lngVal = cast(long)(lngVal / pow(gridColumns, gridCodeLength));
288 		}
289 		// Compute the pair section of the code.
290 		for (int i = 0; i < pairCodeLength / 2; i++)
291 		{
292 			buffer[len++] = codeAlphabet[cast(int)(lngVal % encodingBase)];
293 			buffer[len++] = codeAlphabet[cast(int)(latVal % encodingBase)];
294 			latVal /= encodingBase;
295 			lngVal /= encodingBase;
296 			// If we are at the separator position, add the separator.
297 			if (i == 0)
298 			{
299 				buffer[len++] = separator;
300 			}
301 		}
302 		// Reverse the code.
303 		auto codeBuilder = buffer[0 .. len];
304 		codeBuilder.reverse();
305 
306 		// If we need to pad the code, replace some of the digits.
307 		if (codeLength < separatorPosition)
308 		{
309 			if (codeBuilder.length < separatorPosition)
310 			{
311 				codeBuilder = buffer[0 .. separatorPosition];
312 				codeBuilder[len .. $] = paddingCharacter;
313 			}
314 			else
315 			{
316 				codeBuilder[codeLength .. separatorPosition] = paddingCharacter;
317 			}
318 		}
319 		return codeBuilder[0 .. max(separatorPosition + 1, codeLength + 1)];
320 	}
321 
322 	/**
323 	 * Decodes this object into a $(D OpenLocationCodeArea) object encapsulating latitude/longitude bounding box. This method doesn't allocate when not throwing an exception.
324 	 *
325 	 * Returns: A OpenLocationCodeArea object.
326 	 *
327 	 * Throws: $(D OLCFormatException) if this is not a valid full code.
328 	 */
329 	OpenLocationCodeArea decode() const
330 	{
331 		if (!isFull)
332 		{
333 			throw new OLCFormatException(
334 					format!"Method decode() could only be called on valid full codes, code was '%s'."(code));
335 		}
336 		return decodeTrustedFull();
337 	}
338 
339 	/// ditto
340 	static OpenLocationCodeArea decode(string code)
341 	{
342 		return OpenLocationCode.fromString(code).decode();
343 	}
344 
345 	/**
346 	 * Decodes this object into a $(D OpenLocationCodeArea) object encapsulating latitude/longitude bounding box. This method doesn't allocate when not throwing an exception.
347 	 *
348 	 * Returns: A OpenLocationCodeArea object.
349 	 */
350 	OpenLocationCodeArea decodeTrustedFull() const nothrow @nogc
351 	in(isFull)
352 	{
353 		// Strip padding and separator characters out of the code.
354 		ubyte[maxOLCLength] buffer;
355 		int buflen;
356 		foreach (c; _code)
357 			if (c != separator && c != paddingCharacter)
358 				buffer[buflen++] = c;
359 		auto code = buffer[0 .. buflen];
360 
361 		// Initialise the values. We work them out as integers and convert them to doubles at the end.
362 		long latVal = -latitudeMax * latIntegerMultiplier;
363 		long lngVal = -longitudeMax * lngIntegerMultiplier;
364 		// Define the place value for the digits. We'll divide this down as we work through the code.
365 		long latPlaceVal = latMspValue;
366 		long lngPlaceVal = lngMspValue;
367 		for (int i = 0; i < min(code.length, pairCodeLength); i += 2)
368 		{
369 			latPlaceVal /= encodingBase;
370 			lngPlaceVal /= encodingBase;
371 			latVal += codeAlphabet.countUntil(code[i]) * latPlaceVal;
372 			lngVal += codeAlphabet.countUntil(code[i + 1]) * lngPlaceVal;
373 		}
374 		for (int i = pairCodeLength; i < min(code.length, maxDigitCount); i++)
375 		{
376 			latPlaceVal /= gridRows;
377 			lngPlaceVal /= gridColumns;
378 			const digit = codeAlphabet.countUntil(code[i]);
379 			const row = digit / gridColumns;
380 			const col = digit % gridColumns;
381 			latVal += row * latPlaceVal;
382 			lngVal += col * lngPlaceVal;
383 		}
384 		const latitudeLo = cast(double) latVal / latIntegerMultiplier;
385 		const longitudeLo = cast(double) lngVal / lngIntegerMultiplier;
386 		const latitudeHi = cast(double)(latVal + latPlaceVal) / latIntegerMultiplier;
387 		const longitudeHi = cast(double)(lngVal + lngPlaceVal) / lngIntegerMultiplier;
388 		return OpenLocationCodeArea(latitudeLo, longitudeLo, latitudeHi,
389 				longitudeHi, min(code.length, maxDigitCount));
390 	}
391 
392 	/**
393    * Returns whether this $(D OpenLocationCode) is a full Open Location Code.
394    *
395    * Returns: `true` if it is a full code.
396    */
397 	bool isFull() const @property nothrow @nogc
398 	{
399 		return _code.length > separatorPosition && _code[separatorPosition] == separator;
400 	}
401 
402 	/**
403    * Returns whether this $(D OpenLocationCode) is a short Open Location Code.
404    *
405    * Returns: True if it is short.
406    */
407 	bool isShort() const @property nothrow @nogc
408 	{
409 		const index = _code.countUntil(separator);
410 		return index >= 0 && index < separatorPosition;
411 	}
412 
413 	/**
414    * Returns whether this $(D OpenLocationCode) is a padded Open Location Code, meaning that it
415    * contains less than 8 valid digits.
416    *
417    * @return True if this code is padded.
418    */
419 	bool isPadded() const @property nothrow @nogc
420 	{
421 		return _code.canFind(paddingCharacter);
422 	}
423 
424 	/**
425 	 * Returns short $(D OpenLocationCode) from the full Open Location Code created by removing
426 	 * four or six digits, depending on the provided reference point. It removes as many digits as
427 	 * possible.
428 	 *
429 	 * Params:
430 	 * 	referenceLatitude = Reference point latitude degrees.
431 	 * 	referenceLongitude = Reference point longitude degrees.
432 	 * Returns: A short code if possible or OpenLocationCode.init if it is too far away.
433 	 * Throws: $(D OLCFormatException) if this is not called on a valid full code or called on a padded code.
434 	 */
435 	OpenLocationCode shorten(double referenceLatitude, double referenceLongitude) const
436 	{
437 		if (!isFull())
438 			throw new OLCFormatException("shorten() method could only be called on a full code.");
439 		if (isPadded())
440 			throw new OLCFormatException("shorten() method can not be called on a padded code.");
441 		return shortenTrustedFullNoPad(referenceLatitude, referenceLongitude);
442 	}
443 
444 	/**
445 	 * Returns short $(D OpenLocationCode) from the full Open Location Code created by removing
446 	 * four or six digits, depending on the provided reference point. It removes as many digits as
447 	 * possible. Assumes this is a full code which isn't padded.
448 	 *
449 	 * Params:
450 	 * 	referenceLatitude = Reference point latitude degrees.
451 	 * 	referenceLongitude = Reference point longitude degrees.
452 	 * Returns: A short code if possible or OpenLocationCode.init if it is too far away.
453 	 */
454 	OpenLocationCode shortenTrustedFullNoPad(double referenceLatitude, double referenceLongitude) const nothrow @nogc
455 	{
456 		const codeArea = decodeTrustedFull();
457 		const range = max(abs(referenceLatitude - codeArea.centerLatitude),
458 				abs(referenceLongitude - codeArea.centerLongitude));
459 		// We are going to check to see if we can remove three pairs, two pairs or just one pair of
460 		// digits from the code.
461 		for (int i = 4; i >= 1; i--)
462 		{
463 			// Check if we're close enough to shorten. The range must be less than 1/2
464 			// the precision to shorten at all, and we want to allow some safety, so
465 			// use 0.3 instead of 0.5 as a multiplier.
466 			if (range < (computeLatitudePrecision(i * 2) * 0.3))
467 			{
468 				// We're done.
469 				return OpenLocationCode(_code[i * 2 .. $]);
470 			}
471 		}
472 		return OpenLocationCode.init;
473 	}
474 
475 	/**
476 	 * Recover the nearest match (if the code was a short code) representing a full Open
477 	 * Location Code from this (short) Open Location Code, given the reference location.
478 	 *
479 	 * Params:
480 	 * 	referenceLatitude = Reference point latitude degrees.
481 	 * 	referenceLongitude = Reference point longitude degrees.
482 	 * @return The nearest matching full code.
483 	 */
484 	OpenLocationCode recover(double referenceLatitude, double referenceLongitude) const
485 	{
486 		if (isFull)
487 		{
488 			// Note: each code is either full xor short, no other option.
489 			return this;
490 		}
491 		referenceLatitude = clipLatitude(referenceLatitude);
492 		referenceLongitude = normalizeLongitude(referenceLongitude);
493 
494 		const digitsToRecover = separatorPosition - _code.countUntil(separator);
495 		// The precision (height and width) of the missing prefix in degrees.
496 		const prefixPrecision = pow(cast(double) encodingBase, 2.0 - (digitsToRecover / 2));
497 
498 		// Use the reference location to generate the prefix.
499 		auto recoveredPrefix = OpenLocationCode.encode(referenceLatitude,
500 				referenceLongitude)._code[0 .. digitsToRecover];
501 		// Combine the prefix with the short code and decode it.
502 		const recovered = OpenLocationCode(recoveredPrefix ~ _code);
503 		const recoveredCodeArea = recovered.decode();
504 		// Work out whether the new code area is too far from the reference location. If it is, we
505 		// move it. It can only be out by a single precision step.
506 		double recoveredLatitude = recoveredCodeArea.centerLatitude;
507 		double recoveredLongitude = recoveredCodeArea.centerLongitude;
508 
509 		// Move the recovered latitude by one precision up or down if it is too far from the reference,
510 		// unless doing so would lead to an invalid latitude.
511 		const latitudeDiff = recoveredLatitude - referenceLatitude;
512 		if (latitudeDiff > prefixPrecision / 2 && recoveredLatitude - prefixPrecision > -latitudeMax)
513 		{
514 			recoveredLatitude -= prefixPrecision;
515 		}
516 		else if (latitudeDiff < -prefixPrecision / 2 && recoveredLatitude + prefixPrecision < latitudeMax)
517 		{
518 			recoveredLatitude += prefixPrecision;
519 		}
520 
521 		// Move the recovered longitude by one precision up or down if it is too far from the
522 		// reference.
523 		const longitudeDiff = recoveredCodeArea.centerLongitude - referenceLongitude;
524 		if (longitudeDiff > prefixPrecision / 2)
525 		{
526 			recoveredLongitude -= prefixPrecision;
527 		}
528 		else if (longitudeDiff < -prefixPrecision / 2)
529 		{
530 			recoveredLongitude += prefixPrecision;
531 		}
532 
533 		return OpenLocationCode.encode(recoveredLatitude, recoveredLongitude,
534 				(cast(int) recovered.code.length) - 1);
535 	}
536 
537 	/**
538    * Returns whether the bounding box specified by the Open Location Code contains provided point.
539    *
540 	 * Params:
541    * 	latitude = Latitude degrees.
542    * 	longitude = Longitude degrees.
543    * Returns: $(D true) if the coordinates are contained by the code.
544 	 * Throws: $(D OLCFormatException) if this is not a valid full code.
545    */
546 	bool contains(double latitude, double longitude) const @safe
547 	{
548 		return decode().contains(latitude, longitude);
549 	}
550 
551 	/**
552    * Returns whether the bounding box specified by the Open Location Code contains provided point.
553    *
554 	 * Params:
555    * 	latitude = Latitude degrees.
556    * 	longitude = Longitude degrees.
557    * Returns: $(D true) if the coordinates are contained by the code.
558    */
559 	bool containsTrustedFull(double latitude, double longitude) const nothrow @nogc @safe
560 	{
561 		return decodeTrustedFull().contains(latitude, longitude);
562 	}
563 
564 	string toString() const
565 	{
566 		return code;
567 	}
568 }
569 
570 /**
571  * Returns whether the provided code length 
572  */
573 bool isValidCodeLength(int codeLength) nothrow @nogc pure
574 {
575 	const c = min(codeLength, maxDigitCount);
576 	return c >= 4 && !(c < pairCodeLength && c % 2 == 1);
577 }
578 
579 // Exposed static helper methods.
580 
581 /**
582  * Returns whether the provided string is a valid Open Location code.
583  *
584  * Params:
585  * 	codeString = The code to check.
586  * Returns: True if it is a valid full or short code.
587  */
588 bool isValidCode(string codeString) pure
589 {
590 	return isValidUppercaseCode(codeString.toUpper);
591 }
592 
593 /**
594  * Same as isValidCode but doesn't convert the code to uppercase before handling, thus failing validation when using lowercase characters.
595  */
596 bool isValidUppercaseCode(string codeString) nothrow @nogc pure
597 {
598 	if (codeString.length < 2)
599 		return false;
600 
601 	const code = codeString.representation;
602 
603 	// there must be exactly one separator
604 	const separatorPosition = code.countUntil(separator);
605 	if (separatorPosition == -1 || code[separatorPosition + 1 .. $].canFind(separator))
606 		return false;
607 
608 	// there must be an even number of at most 8 characters before the separator
609 	if (separatorPosition % 2 != 0 || separatorPosition > .separatorPosition)
610 		return false;
611 
612 	// Check first two characters: only some values from the alphabet are permitted.
613 	if (separatorPosition == .separatorPosition)
614 	{
615 		// First latitude character can only have first 9 values.
616 		if (codeAlphabet.countUntil(code[0]) > 8)
617 			return false;
618 
619 		// First longitude character can only have first 18 values.
620 		if (codeAlphabet.countUntil(code[1]) > 17)
621 			return false;
622 	}
623 
624 	// Check the characters before the separator.
625 	bool paddingStarted;
626 	foreach (i, c; code[0 .. separatorPosition])
627 	{
628 		if (!codeAlphabet.canFind(c) && c != paddingCharacter)
629 			return false; // invalid character
630 
631 		if (paddingStarted)
632 		{
633 			// once padding starts, there must not be anything but padding.
634 			if (c != paddingCharacter)
635 				return false;
636 		}
637 		else if (c == paddingCharacter)
638 		{
639 			paddingStarted = true;
640 
641 			// short codes cannot have padding
642 			if (separatorPosition < .separatorPosition)
643 				return false;
644 
645 			// padding can start on even character: 2, 4 or 6.
646 			if (!i.among!(2, 4, 6))
647 				return false;
648 		}
649 	}
650 
651 	if (code.length > separatorPosition + 1)
652 	{
653 		if (paddingStarted)
654 			return false;
655 
656 		// only one character after separator is forbidden
657 		const extra = code[separatorPosition + 1 .. $];
658 		if (extra.length == 1)
659 			return false;
660 		if (extra.any!(a => !codeAlphabet.canFind(a)))
661 			return false;
662 	}
663 
664 	return true;
665 }
666 
667 /**
668  * Returns if the code is a valid full Open Location Code.
669  *
670  * Params:
671  * 	code = The code to check.
672  * Returns: True if it is a valid full code.
673  */
674 bool isFullCode(string code) pure
675 {
676 	return code.toUpper.isFullUppercaseCode;
677 }
678 
679 /**
680  * Does the same as isFullCode, assuming the code is already uppercase, making this @nogc nothrow.
681  */
682 bool isFullUppercaseCode(string code) nothrow @nogc pure
683 {
684 	if (!code.isValidUppercaseCode)
685 		return false;
686 	return OpenLocationCode.fromTrustedString(code).isFull;
687 }
688 
689 /**
690  * Returns if the code is a valid short Open Location Code.
691  *
692  * Params:
693  * 	code = The code to check.
694  * Returns: True if it is a valid short code.
695  */
696 bool isShortCode(string code) pure
697 {
698 	return code.toUpper.isShortUppercaseCode;
699 }
700 
701 /**
702  * Does the same as isShortCode, assuming the code is already uppercase, making this @nogc nothrow.
703  */
704 bool isShortUppercaseCode(string code) nothrow @nogc pure
705 {
706 	if (!code.isValidUppercaseCode)
707 		return false;
708 	return OpenLocationCode.fromTrustedString(code).isShort;
709 }
710 
711 // Private static methods.
712 
713 private double clipLatitude(double latitude) nothrow @nogc pure
714 {
715 	return min(max(latitude, -latitudeMax), latitudeMax);
716 }
717 
718 private double normalizeLongitude(double longitude) nothrow @nogc pure
719 {
720 	while (longitude < -longitudeMax)
721 	{
722 		longitude = longitude + longitudeMax * 2;
723 	}
724 	while (longitude >= longitudeMax)
725 	{
726 		longitude = longitude - longitudeMax * 2;
727 	}
728 	return longitude;
729 }
730 
731 /**
732    * Compute the latitude precision value for a given code length. Lengths <= 10 have the same
733    * precision for latitude and longitude, but lengths > 10 have different precisions due to the
734    * grid method having fewer columns than rows. Copied from the JS implementation.
735    */
736 private double computeLatitudePrecision(int codeLength) nothrow @nogc pure
737 {
738 	if (codeLength <= codePrecisionNormal)
739 		return pow(encodingBase, cast(double)(codeLength / -2 + 2));
740 	else
741 		return pow(encodingBase, -3) / pow(gridRows, codeLength - pairCodeLength);
742 }
743 
744 // ========== TESTS ==========
745 
746 version (unittest)
747 {
748 	import unit_threaded;
749 
750 	enum epsilon = 1e-10;
751 
752 	void epsilonTest(A, B)(A a, B b, in string file = __FILE__, in size_t line = __LINE__)
753 	{
754 		shouldApproxEqual(a, b, epsilon);
755 	}
756 }
757 
758 @("test validity")
759 @system unittest
760 {
761 	import std.csv;
762 	import std.stdio;
763 	import std.typecons;
764 
765 	auto file = File("test_data/test_validity.csv", "r");
766 	foreach (record; file.byLine.filter!(a => !a.startsWith("#")).joiner("\n")
767 			.csvReader!(Tuple!(string, "code", bool, "isValid", bool, "isShort", bool, "isFull")))
768 	{
769 		void test() @safe
770 		{
771 			shouldEqual(isValidCode(record.code), record.isValid);
772 			shouldEqual(isShortCode(record.code), record.isShort);
773 			shouldEqual(isFullCode(record.code), record.isFull);
774 		}
775 
776 		test();
777 	}
778 }
779 
780 @("test shortening")
781 @system unittest
782 {
783 	import std.csv;
784 	import std.stdio;
785 	import std.typecons;
786 
787 	auto file = File("test_data/test_short_codes.csv", "r");
788 	foreach (record; file.byLine.filter!(a => !a.startsWith("#")).joiner("\n")
789 			.csvReader!(Tuple!(string, "code", double, "lat", double, "lng", string,
790 				"shortCode", string, "testType")))
791 	{
792 		void testShorten() @safe
793 		{
794 			auto olc = OpenLocationCode.fromString(record.code);
795 			shouldEqual(olc.shorten(record.lat, record.lng).code, record.shortCode);
796 		}
797 
798 		void testRecovery() @safe
799 		{
800 			auto olc = OpenLocationCode.fromString(record.shortCode);
801 			shouldEqual(olc.recover(record.lat, record.lng).code, record.code);
802 		}
803 
804 		if (record.testType == "B" || record.testType == "S")
805 			testShorten();
806 
807 		if (record.testType == "B" || record.testType == "R")
808 			testRecovery();
809 	}
810 }
811 
812 @("test encoding")
813 @system unittest
814 {
815 	import std.csv;
816 	import std.stdio;
817 	import std.typecons;
818 
819 	auto file = File("test_data/test_encoding.csv", "r");
820 	foreach (record; file.byLine.filter!(a => !a.startsWith("#")).joiner("\n")
821 			.csvReader!(Tuple!(double, "lat", double, "lng", int, "length", string, "code")))
822 	{
823 		shouldEqual(OpenLocationCode.encode(record.lat, record.lng, record.length).code, record.code);
824 	}
825 }
826 
827 @("test decoding")
828 @system unittest
829 {
830 	import std.csv;
831 	import std.stdio;
832 	import std.typecons;
833 
834 	auto file = File("test_data/test_decoding.csv", "r");
835 	foreach (record; file.byLine.filter!(a => !a.startsWith("#")).joiner("\n")
836 			.csvReader!(Tuple!(string, "code", int, "length", double, "latLo",
837 				double, "lngLo", double, "latHi", double, "lngHi")))
838 	{
839 		auto olc = OpenLocationCode.fromString(record.code);
840 		auto area = olc.decode();
841 		// test decode
842 		shouldEqual(record.length, area.length); // Wrong length
843 		epsilonTest(record.latLo, area.southLatitude); // Wrong low latitude
844 		epsilonTest(record.latHi, area.northLatitude); // Wrong high latitude
845 		epsilonTest(record.lngLo, area.westLongitude); // Wrong low longitude
846 		epsilonTest(record.lngHi, area.eastLongitude); // Wrong high longitude
847 
848 		// test contains
849 		shouldBeTrue(olc.contains(area.centerLatitude, area.centerLongitude)); // Containment relation is broken for the decoded middle point of code
850 		shouldBeTrue(olc.contains(area.southLatitude, area.westLongitude)); // Containment relation is broken for the decoded bottom left corner
851 		shouldBeFalse(olc.contains(area.northLatitude, area.eastLongitude)); // Containment relation is broken for the decoded top right corner
852 		shouldBeFalse(olc.contains(area.southLatitude, area.eastLongitude)); // Containment relation is broken for the decoded bottom right corner
853 		shouldBeFalse(olc.contains(area.northLatitude, area.westLongitude)); // Containment relation is broken for the decoded top left corner
854 	}
855 }
856 
857 @("test clipping")
858 unittest
859 {
860 	shouldEqual(OpenLocationCode.encode(-90, 5), OpenLocationCode.encode(-91, 5));
861 	shouldEqual(OpenLocationCode.encode(90, 5), OpenLocationCode.encode(91, 5));
862 	shouldEqual(OpenLocationCode.encode(5, 175), OpenLocationCode.encode(5, -185));
863 	shouldEqual(OpenLocationCode.encode(5, 175), OpenLocationCode.encode(5, -905));
864 	shouldEqual(OpenLocationCode.encode(5, -175), OpenLocationCode.encode(5, 905));
865 }
866 
867 @("test max code length")
868 unittest
869 {
870 	// Check that we do not return a code longer than is valid.
871 	string code = OpenLocationCode.encode(51.3701125, -10.202665625, 1_000_000).code;
872 	shouldEqual(maxDigitCount + 1, code.length); // Encoded code should have a length of maxDigitCount + 1 for the plus symbol
873 	shouldBeTrue(isValidCode(code));
874 	// Extend the code with a valid character and make sure it is still valid.
875 	string tooLongCode = code ~ "W";
876 	shouldBeTrue(isValidCode(tooLongCode)); // Too long code with all valid characters should be valid.
877 	// Extend the code with an invalid character and make sure it is invalid.
878 	tooLongCode = code ~ "U";
879 	shouldBeFalse(isValidCode(tooLongCode)); // Too long code with invalid character should be invalid.
880 }
881 
882 @("test recovery near south pole")
883 unittest
884 {
885 	shouldEqual(OpenLocationCode.fromString("XXXXXX+XX").recover(-81.0, 0.0).code, "2CXXXXXX+XX");
886 }
887 
888 @("test recovery near north pole")
889 unittest
890 {
891 	shouldEqual(OpenLocationCode.fromString("2222+22").recover(89.6, 0.0).code, "CFX22222+22");
892 }
893 
894 @("test width in degrees")
895 unittest
896 {
897 	epsilonTest(OpenLocationCode.fromString("67000000+").decode().longitudeWidth, 20.);
898 	epsilonTest(OpenLocationCode.fromString("67890000+").decode().longitudeWidth, 1.);
899 	epsilonTest(OpenLocationCode.fromString("6789CF00+").decode().longitudeWidth, 0.05);
900 	epsilonTest(OpenLocationCode.fromString("6789CFGH+").decode().longitudeWidth, 0.0025);
901 	epsilonTest(OpenLocationCode.fromString("6789CFGH+JM").decode().longitudeWidth, 0.000125);
902 	epsilonTest(OpenLocationCode.fromString("6789CFGH+JMP").decode().longitudeWidth, 0.00003125);
903 }
904 
905 @("test height in degrees")
906 unittest
907 {
908 	epsilonTest(OpenLocationCode.fromString("67000000+").decode().latitudeHeight, 20.);
909 	epsilonTest(OpenLocationCode.fromString("67890000+").decode().latitudeHeight, 1.);
910 	epsilonTest(OpenLocationCode.fromString("6789CF00+").decode().latitudeHeight, 0.05);
911 	epsilonTest(OpenLocationCode.fromString("6789CFGH+").decode().latitudeHeight, 0.0025);
912 	epsilonTest(OpenLocationCode.fromString("6789CFGH+JM").decode().latitudeHeight, 0.000125);
913 	epsilonTest(OpenLocationCode.fromString("6789CFGH+JMP").decode().latitudeHeight, 0.000025);
914 }