[search] Refactor MatchLatLonDegree function #3301
2 changed files with 705 additions and 296 deletions
|
@ -1,278 +1,590 @@
|
|||
#include "search/latlon_match.hpp"
|
||||
|
||||
#include "base/macros.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iterator>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <optional>
|
||||
#include <list>
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <functional>
|
||||
|
||||
using namespace std;
|
||||
|
||||
namespace
|
||||
{
|
||||
string const kSpaces = " \t";
|
||||
string const kCharsToSkip = " \n\t,;:.()";
|
||||
string const kDecimalMarks = ".,";
|
||||
|
||||
bool IsDecimalMark(char c)
|
||||
{
|
||||
return kDecimalMarks.find(c) != string::npos;
|
||||
}
|
||||
|
||||
template <typename Char>
|
||||
void SkipSpaces(Char *& s)
|
||||
{
|
||||
while (kSpaces.find(*s) != string::npos)
|
||||
++s;
|
||||
}
|
||||
|
||||
template <typename Char>
|
||||
void Skip(Char *& s)
|
||||
{
|
||||
while (kCharsToSkip.find(*s) != string::npos)
|
||||
++s;
|
||||
}
|
||||
|
||||
bool MatchDMSArray(char const * & s, char const * arr[], size_t count)
|
||||
{
|
||||
for (size_t i = 0; i < count; ++i)
|
||||
namespace {
|
||||
// The Check class is an auxiliary class, the purpose of which is to perform
|
||||
// various kinds of checks during the operation of the module.
|
||||
class Check {
|
||||
public:
|
||||
static std::size_t IsDegree(char const * pos)
|
||||
{
|
||||
size_t const len = strlen(arr[i]);
|
||||
if (strncmp(s, arr[i], len) == 0)
|
||||
return CheckSymbol(m_degrees, pos);
|
||||
}
|
||||
static std::size_t IsMinute(char const * pos)
|
||||
{
|
||||
return CheckSymbol(m_minutes, pos);
|
||||
}
|
||||
static std::size_t IsSecond(char const * pos)
|
||||
{
|
||||
return CheckSymbol(m_seconds, pos);
|
||||
}
|
||||
// The IsCardDir function checks whether the character, pointed to by the |pos|,
|
||||
// is symbol of cardinal direction. If so, the function returns true,
|
||||
// otherwise false.
|
||||
static bool IsCardDir(char const * pos)
|
||||
{
|
||||
return m_cardinalDirections.find(*pos) != std::string_view::npos;
|
||||
}
|
||||
// The IsNegative function checks whether the character, pointed to by the |pos|,
|
||||
// is the beginning of a negative number. If so, the function returns true,
|
||||
// otherwise false.
|
||||
static bool IsNegative(char const * pos)
|
||||
{
|
||||
return *pos == '-' && std::isdigit(*(pos + 1));
|
||||
}
|
||||
// The IsInteger function checks whether the |value| has a fractional part.
|
||||
// If so, the function returns false, otherwise true.
|
||||
static bool IsInteger(double value)
|
||||
{
|
||||
return value == static_cast<int>(value);
|
||||
}
|
||||
// The Latitude function checks whether the |lat| variable meets the latitude
|
||||
// requirements. If so, the function returns true, otherwise false.
|
||||
static bool Latitude(double lat)
|
||||
{
|
||||
return (lat < -90.0 || lat > 90.0) ? false : true;
|
||||
}
|
||||
// The Longitude function checks whether the |lon| variable meets the longitude
|
||||
// requirements. If so, the function returns true, otherwise false.
|
||||
static bool Longitude(double lon)
|
||||
{
|
||||
return (lon < -180.0 || lon > 180.0) ? false : true;
|
||||
}
|
||||
private:
|
||||
template<std::size_t Size>
|
||||
using Array = std::array<char const *, Size>;
|
||||
// The CheckSymbol function checks whether character, pointed to by |pos|, belongs
|
||||
// to the set of characters, contained in the array |symbols|. The function returns
|
||||
// the size of the character, if it is present, otherwise zero.
|
||||
template<std::size_t Size>
|
||||
static std::size_t CheckSymbol(Array<Size> const & symbols, char const * pos)
|
||||
{
|
||||
for (char const * symb : symbols)
|
||||
{
|
||||
s += len;
|
||||
size_t const len = strlen(symb);
|
||||
if (strncmp(pos, symb, len) == 0)
|
||||
return len;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
static constexpr Array<2> m_degrees = { "*", "°" };
|
||||
static constexpr Array<3> m_minutes = { "\'", "’", "′" };
|
||||
static constexpr Array<6> m_seconds = { "\"", "”", "″", "\'\'", "’’", "′′" };
|
||||
static constexpr std::string_view m_cardinalDirections = "NnSsEeWw";
|
||||
};
|
||||
|
||||
enum class Index
|
||||
{
|
||||
kDegree,
|
||||
kMinute,
|
||||
kSecond,
|
||||
kCardDir
|
||||
};
|
||||
|
||||
// The CoordinatesFormat class is an abstract class that provides an interface for
|
||||
// interacting with its successors as well as functionality common to them. In order
|
||||
// to be successfully integrated, each new coordinate format must inherit this class.
|
||||
class CoordinatesFormat {
|
||||
using CoordinatePair = std::optional<std::pair<double, double>>;
|
||||
public:
|
||||
CoordinatesFormat() : m_start(nullptr) { }
|
||||
virtual ~CoordinatesFormat() = default;
|
||||
// The SetStart function sets the starting point in the query for coordinate parser.
|
||||
void SetStart(char const * start)
|
||||
{
|
||||
m_start = start;
|
||||
}
|
||||
// The InterpretNumber function interprets a floating point value in a byte string
|
||||
// pointed to by |start|. The function returns two values: floating point value,
|
||||
// corresponding to the contents of |start| and the number of characters that was
|
||||
// interpreted.
|
||||
std::pair<double, std::size_t> InterpretNumber(char const * const start)
|
||||
{
|
||||
char * end;
|
||||
double number = std::strtod(start, &end);
|
||||
if (*end == ',' && std::isdigit(*(end + 1)))
|
||||
{
|
||||
char* another_start = ++end;
|
||||
double fractional_part = std::strtod(another_start, &end);
|
||||
while (fractional_part >= 1)
|
||||
fractional_part /= 10;
|
||||
number += number > 0 ? fractional_part : -fractional_part;
|
||||
}
|
||||
return {number, end - start};
|
||||
}
|
||||
// The Parse function is pure virtual method, whose implementation in subclasses
|
||||
// performs query analysis.
|
||||
virtual void Parse() = 0;
|
||||
// The Format function is pure virtual method, whose implementation in subclasses
|
||||
// checks whether it is possible to analyze the byte string pointed to by the |pos|.
|
||||
// The function returns true if the byte string pointed to by the |pos| corresponds
|
||||
// to the characteristic features of the coordinate format, otherwise false.
|
||||
virtual bool Format(char const * pos) = 0;
|
||||
// The Results The function returns the results of the analysis in case of its
|
||||
// successful completion, otherwise uninitialized std::optional object.
|
||||
virtual CoordinatePair Results() = 0;
|
||||
protected:
|
||||
CoordinatePair Results(std::optional<double> lat, std::optional<double> lon)
|
||||
{
|
||||
if (lat && lon && Check::Latitude(lat.value()) && Check::Longitude(lon.value()))
|
||||
return std::make_pair(lat.value(), lon.value());
|
||||
else
|
||||
return std::nullopt;
|
||||
}
|
||||
char const * StartPos()
|
||||
{
|
||||
return m_start;
|
||||
}
|
||||
private:
|
||||
// The |m_start| field holds the position in the query with starting from which
|
||||
// the analysis is performed.
|
||||
char const * m_start;
|
||||
};
|
||||
|
||||
|
||||
// The DDFormat class provides functionality for parsing coordinates in Decimal
|
||||
// Degrees (DD) format. The implemented format has the following form:
|
||||
//
|
||||
// dd, DD
|
||||
//
|
||||
// -> dd is a latitude in a range [-90,90] (real number);
|
||||
// -> DD is a longitude in a range [-180,180] or [0,360] (real number).
|
||||
// The longitude ranges are correlated as follows:
|
||||
// [ 0, 360]: 180, 181, ..., 359, 360/0, 1, ..., 179, 180
|
||||
// [-180, 180]: -180, -179, ..., -1, 0, 1, ..., 179, 180
|
||||
class DDFormat : public CoordinatesFormat {
|
||||
public:
|
||||
DDFormat() : m_lat(std::nullopt), m_lon(std::nullopt), m_in_process(&m_lat) { }
|
||||
void Parse() override
|
||||
{
|
||||
for (auto iter = StartPos(); *iter != '\0';)
|
||||
{
|
||||
std::size_t shift = 1;
|
||||
if (m_in_process)
|
||||
{
|
||||
if (std::isdigit(*iter) || Check::IsNegative(iter))
|
||||
std::tie(*m_in_process, shift) = InterpretNumber(iter);
|
||||
else if (*iter == ' ' || *iter == ',')
|
||||
m_in_process = m_lon ? nullptr : &m_lon;
|
||||
else if (std::size_t size = Check::IsDegree(iter); size != 0)
|
||||
shift = size;
|
||||
else if (!m_lat || !m_lon || Check::IsCardDir(iter) || Check::IsMinute(iter) || Check::IsSecond(iter))
|
||||
return Reset();
|
||||
} else if (std::isdigit(*iter))
|
||||
return Reset();
|
||||
iter += shift;
|
||||
}
|
||||
if (m_lon > 180.0)
|
||||
m_lon.value() -= 360.0;
|
||||
}
|
||||
std::optional<std::pair<double, double>> Results() override
|
||||
{
|
||||
return CoordinatesFormat::Results(m_lat, m_lon);
|
||||
}
|
||||
bool Format(char const * pos) override
|
||||
{
|
||||
return Check::IsNegative(pos) || std::isdigit(*pos);
|
||||
}
|
||||
private:
|
||||
void Reset()
|
||||
{
|
||||
std::tie(m_lat, m_lon) = std::tie(std::nullopt, std::nullopt);
|
||||
}
|
||||
std::optional<double> m_lat; // The |m_lat| field holds the latitude value during parsing.
|
||||
std::optional<double> m_lon; // The |m_lon| field holds the longitude value dirung parsing.
|
||||
std::optional<double> * m_in_process; // The |m_in_process| points to a coordinate that is
|
||||
// in the process of parsing.
|
||||
};
|
||||
|
||||
// The AbstractDmsDdm class is the abstract base class, providing functionality common to
|
||||
// parsers of Degrees, Minutes, Seconds (DMS) and Degrees, Decimal Minutes (DDM) coordinate
|
||||
// formates.
|
||||
class AbstractDmsDdm : public CoordinatesFormat {
|
||||
protected:
|
||||
// The Component class represents a separate component from which the DMS or DDM notations
|
||||
// are built.
|
||||
class Component {
|
||||
public:
|
||||
Component(Index index, int ratio, std::function<bool(double)>&& check)
|
||||
: m_index(index), m_ratio(ratio), m_check(check) { }
|
||||
virtual ~Component() = default;
|
||||
bool HasValue() const
|
||||
{
|
||||
return m_value.has_value();
|
||||
}
|
||||
bool SetValue(double value)
|
||||
{
|
||||
if (!m_check(value))
|
||||
return false;
|
||||
m_value = std::make_optional(value);
|
||||
return true;
|
||||
}
|
||||
double Compute() const
|
||||
{
|
||||
return m_value.value() / m_ratio;
|
||||
}
|
||||
bool operator==(Index const index) const
|
||||
{
|
||||
return index == m_index;
|
||||
}
|
||||
private:
|
||||
// The |m_index| field holds the identifier of the component that is used to address
|
||||
// this component during runtime.
|
||||
Index const m_index;
|
||||
// The |m_value| field holds the value of the component.
|
||||
std::optional<double> m_value;
|
||||
// The |m_ratio| field holds the value of the multiplier that is used in the calculation
|
||||
// of the coordinate.
|
||||
int const m_ratio;
|
||||
// The |m_check| field holds a functor which checks whether component corresponds to
|
||||
// a specific notation. If so, the functor returns true, otherwise false.
|
||||
std::function<bool(double)> const m_check;
|
||||
};
|
||||
private:
|
||||
// The Coordinate class builds individual instances of the Component class into
|
||||
// a structure typical of DMS or DDM notation.
|
||||
class Coordinate {
|
||||
using CompPtr = std::unique_ptr<Component>;
|
||||
using CompList = std::list<CompPtr>;
|
||||
public:
|
||||
template<typename...Ts>
|
||||
Coordinate(Ts... ts)
|
||||
{
|
||||
(m_list.push_back(std::make_unique<Ts>(ts)), ...);
|
||||
}
|
||||
template<typename T>
|
||||
bool SetValue(T value, Index index)
|
||||
{
|
||||
if constexpr (std::is_floating_point_v<T>) {
|
||||
if (auto iter = Find(index); iter != m_list.end())
|
||||
{
|
||||
Complete(iter);
|
||||
return (*iter)->SetValue(value);
|
||||
}
|
||||
} else
|
||||
card_dir = std::make_optional(value);
|
||||
return true;
|
||||
}
|
||||
// The HasValue function checks whether a component with the index |index| contains
|
||||
// value. The function returns true if the component contains a value, otherwise
|
||||
// false.
|
||||
bool HasValue(Index index)
|
||||
{
|
||||
if (auto iter = Find(index); iter != m_list.end())
|
||||
return (*iter)->HasValue();
|
||||
return card_dir.has_value();
|
||||
}
|
||||
bool IsLatitude() const
|
||||
{
|
||||
return card_dir == 'N' || card_dir == 'S';
|
||||
}
|
||||
bool IsLongitude() const
|
||||
{
|
||||
return card_dir == 'W' || card_dir == 'E';
|
||||
}
|
||||
// The Calculate function, basing on the information obtained during the parsing,
|
||||
// translates the coordinate into Decimal degrees format. The result is recorded
|
||||
// in the |m_result| field.
|
||||
void Calculate()
|
||||
{
|
||||
for (auto& ptr : m_list) {
|
||||
if (ptr->HasValue())
|
||||
{
|
||||
if (m_result)
|
||||
m_result.value() += m_result < 0 ? -ptr->Compute() : ptr->Compute();
|
||||
else
|
||||
m_result = ptr->Compute();
|
||||
}
|
||||
}
|
||||
if (m_result && (card_dir == 'S' || card_dir == 'W'))
|
||||
m_result.value() = -m_result.value();
|
||||
}
|
||||
std::optional<double> Result() const
|
||||
{
|
||||
return m_result;
|
||||
}
|
||||
// The |card_dir| field holds a symbol indicating the cardinal directions.
|
||||
std::optional<char> card_dir;
|
||||
private:
|
||||
// The Complete function verifies that the components preceding the |comp|
|
||||
// have a value. If the previous component does not have a value, then it is
|
||||
// assigned zero. The function stops when a component that has a value is
|
||||
// reached or if the first component of the coordinate is reached.
|
||||
void Complete(CompList::iterator comp)
|
||||
{
|
||||
if (comp != m_list.begin())
|
||||
{
|
||||
for (auto iter = std::prev(comp); !(*iter)->HasValue();)
|
||||
{
|
||||
(*iter)->SetValue(0);
|
||||
if (iter != m_list.begin())
|
||||
--iter;
|
||||
}
|
||||
}
|
||||
}
|
||||
// The Find function searches for a coordinate component with an index |index|.
|
||||
// If a component with the specified index is found, the function returns
|
||||
// an iterator to it, otherwise an iterator, pointing to the end of the |m_list|.
|
||||
CompList::iterator Find(Index const index)
|
||||
{
|
||||
auto predicate = [index] (CompPtr const & c) { return *c == index; };
|
||||
return std::find_if(m_list.begin(), m_list.end(), std::move(predicate));
|
||||
}
|
||||
// The |m_list| field holds instances of successors of the Component class.
|
||||
std::list<std::unique_ptr<Component>> m_list;
|
||||
std::optional<double> m_result;
|
||||
};
|
||||
public:
|
||||
template<typename... Ts>
|
||||
AbstractDmsDdm(Ts... components) : m_number(0), m_first(Coordinate(components...)),
|
||||
m_second(Coordinate(components...)), m_inProcess(&m_first) { }
|
||||
void Parse() override
|
||||
{
|
||||
char const * iter = StartPos();
|
||||
while (*iter != '\0') {
|
||||
std::size_t shift = 1;
|
||||
if (std::isdigit(*iter) || Check::IsNegative(iter))
|
||||
std::tie(m_number, shift) = InterpretNumber(iter);
|
||||
else if (*iter == ',' && *(iter + 1) == ' ' && m_inProcess == &m_first)
|
||||
m_inProcess = &m_second;
|
||||
else if ((*iter == ' ' || *iter == ',') && m_number)
|
||||
return;
|
||||
else if (Check::IsCardDir(iter)) {
|
||||
if (!AssignValue(std::toupper(*iter), Index::kCardDir))
|
||||
return;
|
||||
} else if (auto is_format = FormatCheck(iter); is_format) {
|
||||
Index index;
|
||||
std::tie(shift, index) = is_format.value();
|
||||
if (!AssignValue(m_number, index))
|
||||
return;
|
||||
} else if (Interrupt(iter))
|
||||
return;
|
||||
iter += shift;
|
||||
}
|
||||
if (m_number != 0)
|
||||
return;
|
||||
Process();
|
||||
}
|
||||
bool Format(char const * pos) override
|
||||
{
|
||||
return Check::IsCardDir(pos) || Check::IsNegative(pos) || std::isdigit(*pos);
|
||||
}
|
||||
std::optional<std::pair<double, double>> Results() override
|
||||
{
|
||||
return CoordinatesFormat::Results(m_first.Result(), m_second.Result());
|
||||
}
|
||||
void Process()
|
||||
{
|
||||
if (!m_first.card_dir && !m_second.card_dir) {
|
||||
m_first.card_dir = 'N';
|
||||
m_second.card_dir = 'E';
|
||||
}
|
||||
if (m_first.card_dir && m_second.card_dir) {
|
||||
if (m_first.IsLongitude() && m_second.IsLatitude())
|
||||
std::swap(m_first, m_second);
|
||||
if (m_first.IsLatitude() && m_second.IsLongitude()) {
|
||||
m_first.Calculate();
|
||||
m_second.Calculate();
|
||||
}
|
||||
}
|
||||
}
|
||||
private:
|
||||
// The AssignValue function template assigns value |value| to the component |index|
|
||||
// of the coordinate. The function returns true if the value was assigned successfully,
|
||||
// otherwise false.
|
||||
template<typename T>
|
||||
bool AssignValue(T value, Index index)
|
||||
{
|
||||
if (m_inProcess->HasValue(index))
|
||||
{
|
||||
if (m_inProcess == &m_first)
|
||||
m_inProcess = &m_second;
|
||||
else if (m_inProcess == &m_second) {
|
||||
if constexpr (std::is_integral_v<T>)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!m_inProcess->SetValue(value, index))
|
||||
return false;
|
||||
if constexpr (std::is_floating_point_v<T>)
|
||||
m_number = 0;
|
||||
return true;
|
||||
}
|
||||
// The Interrupt function defines the conditions (if any) under which the parsing process
|
||||
// should be terminated. Parsing stops if the function returns true, otherwise the operation
|
||||
// continues.
|
||||
virtual bool Interrupt(char const * pos) = 0;
|
||||
// The FormatCheck function checks whether the character in the byte string pointed to by
|
||||
// the |pos| corresponds to characters of the DMS or DDM notations. The function returns
|
||||
// an uninitialized std::optional object if the symbol does not correspond to characters
|
||||
// of the DMS or DDM notations, otherwise the function returns the size of the symbol and
|
||||
// its index.
|
||||
virtual std::optional<std::pair<std::size_t, Index>> FormatCheck(char const * pos) = 0;
|
||||
// The |m_number| field holds the value of the coordinate component that is being processed.
|
||||
double m_number;
|
||||
// The |m_first| and |m_second| fields hold the information that is obtained during parsing.
|
||||
Coordinate m_first;
|
||||
Coordinate m_second;
|
||||
Coordinate* m_inProcess; // The |m_in_process| field holds an address of the Coordinate
|
||||
// object, to which the information is written.
|
||||
};
|
||||
|
||||
// The DMSFormat class provides functionality for parsing coordinates in Degrees, Minutes,
|
||||
// Seconds (DMS) format. The DMS format has the following form:
|
||||
//
|
||||
// dd* MM' SS" N, DD* MM' SS" E
|
||||
//
|
||||
// -> dd is in a range [0,90] and DD is in a range [0,180] (both whole numbers);
|
||||
// -> MM is in a range [0,59] (whole number);
|
||||
// -> SS is in a range [0,60) (real number).
|
||||
class DMSFormat : public AbstractDmsDdm {
|
||||
struct Degree : Component {
|
||||
Degree() : Component(Index::kDegree, 1, [] (double v) { return v >= 0 && v <= 180 && Check::IsInteger(v); }) { }
|
||||
};
|
||||
struct Minute : Component {
|
||||
Minute() : Component(Index::kMinute, 60, [] (double v) { return v >= 0 && v <= 59 && Check::IsInteger(v); }) { }
|
||||
};
|
||||
struct Second : Component {
|
||||
Second() : Component(Index::kSecond, 3600, [] (double v) { return v >= 0 && v < 60; }) { }
|
||||
};
|
||||
public:
|
||||
DMSFormat() : AbstractDmsDdm(Degree(), Minute(), Second()) { }
|
||||
private:
|
||||
bool Interrupt(char const *) override
|
||||
{
|
||||
return false;
|
||||
}
|
||||
std::optional<std::pair<std::size_t, Index>> FormatCheck(char const * pos) override
|
||||
{
|
||||
if (std::size_t length = Check::IsDegree(pos); length)
|
||||
return std::make_pair(length, Index::kDegree);
|
||||
else if (std::size_t length = Check::IsSecond(pos); length)
|
||||
return std::make_pair(length, Index::kSecond);
|
||||
else if (std::size_t length = Check::IsMinute(pos); length)
|
||||
return std::make_pair(length, Index::kMinute);
|
||||
else
|
||||
return std::nullopt;
|
||||
}
|
||||
};
|
||||
|
||||
// The DDMFormat class provides functionality for parsing coordinates in Degrees, Decimal
|
||||
// Minutes (DMM) format. The DMM format has the following form:
|
||||
//
|
||||
// dd* MM' N, DD* MM' E
|
||||
//
|
||||
// -> dd is in a range [0,90] and DD is in a range [0,180] (both whole numbers);
|
||||
// -> MM is in a range [0,60) (real number).
|
||||
class DDMFormat : public AbstractDmsDdm {
|
||||
struct Degree : Component {
|
||||
Degree() : Component(Index::kDegree, 1, [] (double v) { return v >= 0 && v <= 180 && Check::IsInteger(v); }) { }
|
||||
};
|
||||
struct Minute : Component {
|
||||
Minute() : Component(Index::kMinute, 60, [] (double v) { return v >= 0 && v <= 60; }) { }
|
||||
};
|
||||
public:
|
||||
DDMFormat() : AbstractDmsDdm(Degree(), Minute()) { }
|
||||
private:
|
||||
bool Interrupt(char const * pos) override
|
||||
{
|
||||
return Check::IsSecond(pos);
|
||||
}
|
||||
std::optional<std::pair<std::size_t, Index>> FormatCheck(char const * pos) override
|
||||
{
|
||||
if (std::size_t length = Check::IsDegree(pos); length)
|
||||
return std::make_pair(length, Index::kDegree);
|
||||
else if (std::size_t length = Check::IsMinute(pos); length)
|
||||
return std::make_pair(length, Index::kMinute);
|
||||
else
|
||||
return std::nullopt;
|
||||
}
|
||||
};
|
||||
|
||||
// The Parser class selects the most appropriate coordinate format and parses the query
|
||||
// according to it. A class, extending the list of supported coordinate formats, should
|
||||
// be inherited from the CoordinateFormat class and added as an argument to the invocation
|
||||
// of CreateList funtion template.
|
||||
class Parser {
|
||||
using FormatPtr = std::unique_ptr<CoordinatesFormat>;
|
||||
using FormatsList = std::list<FormatPtr>;
|
||||
template<typename... Fs>
|
||||
FormatsList CreateList()
|
||||
{
|
||||
FormatsList lst;
|
||||
(lst.push_back(std::make_unique<Fs>()), ...);
|
||||
return lst;
|
||||
}
|
||||
public:
|
||||
Parser(std::string const & query) : m_pos(query.c_str()), m_parser(nullptr),
|
||||
m_formats(CreateList<DDFormat, DMSFormat, DDMFormat>()) { }
|
||||
bool AssumeFormat()
|
||||
{
|
||||
if (m_parser && m_parser->Results())
|
||||
return false;
|
||||
while (!m_formats.empty() && IsAllowed(m_pos))
|
||||
{
|
||||
for (auto iter = m_formats.begin(); iter != m_formats.end(); ++iter)
|
||||
{
|
||||
if ((*iter)->Format(m_pos))
|
||||
{
|
||||
m_parser = std::move(*iter);
|
||||
m_parser->SetStart(m_pos);
|
||||
m_formats.erase(iter);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
++m_pos;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
void Run()
|
||||
{
|
||||
m_parser->Parse();
|
||||
}
|
||||
std::optional<std::pair<double, double>> Results() const
|
||||
{
|
||||
return m_parser ? m_parser->Results() : std::nullopt;
|
||||
}
|
||||
private:
|
||||
// The IsAllowed function checks whether the character pointed to by the |pos|
|
||||
// can be part of the coordinate. If so, the function returns true, otherwise false.
|
||||
static bool IsAllowed(char const * pos)
|
||||
{
|
||||
return std::ispunct(*pos) || std::isspace(*pos) || std::isdigit(*pos) || Check::IsCardDir(pos);
|
||||
}
|
||||
// The |m_pos| field holds an address of the beginning of the chunk of the query
|
||||
// that should be parsed.
|
||||
char const * m_pos;
|
||||
// The |m_parser| field holds a pointer to the coordinate parser, using at the moment.
|
||||
FormatPtr m_parser;
|
||||
// The variable holds a list of coordinate parsers, using for the query processing.
|
||||
FormatsList m_formats;
|
||||
};
|
||||
|
||||
std::optional<std::pair<double, double>> MatchLatLonDegree(std::string const & query)
|
||||
{
|
||||
Parser parser(query);
|
||||
while (parser.AssumeFormat())
|
||||
parser.Run();
|
||||
return parser.Results();
|
||||
}
|
||||
} // end of unnamed namespace
|
||||
|
||||
namespace search {
|
||||
bool MatchLatLonDegree(std::string const & query, double & lat, double & lon)
|
||||
{
|
||||
if (auto success = ::MatchLatLonDegree(query); success)
|
||||
{
|
||||
std::tie(lat, lon) = success.value();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int GetDMSIndex(char const * & s)
|
||||
{
|
||||
char const * arrDegree[] = { "*", "°" };
|
||||
char const * arrMinutes[] = { "\'", "’", "′" };
|
||||
char const * arrSeconds[] = { "\"", "”", "″", "\'\'", "’’", "′′" };
|
||||
|
||||
if (MatchDMSArray(s, arrDegree, ARRAY_SIZE(arrDegree)))
|
||||
return 0;
|
||||
if (MatchDMSArray(s, arrSeconds, ARRAY_SIZE(arrSeconds)))
|
||||
return 2;
|
||||
if (MatchDMSArray(s, arrMinutes, ARRAY_SIZE(arrMinutes)))
|
||||
return 1;
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
void SkipNSEW(char const * & s, char const * (&arrPos) [4])
|
||||
{
|
||||
Skip(s);
|
||||
|
||||
int ind;
|
||||
switch (*s)
|
||||
{
|
||||
case 'N': case 'n': ind = 0; break;
|
||||
case 'S': case 's': ind = 1; break;
|
||||
case 'E': case 'e': ind = 2; break;
|
||||
case 'W': case 'w': ind = 3; break;
|
||||
default: return;
|
||||
}
|
||||
|
||||
arrPos[ind] = s++;
|
||||
|
||||
Skip(s);
|
||||
}
|
||||
|
||||
// Attempts to read a double from the start of |str|
|
||||
// in one of what we assume are two most common forms
|
||||
// for lat/lon: decimal digits separated either
|
||||
// by a dot or by a comma, with digits on both sides
|
||||
// of the separator.
|
||||
// If the attempt fails, falls back to std::strtod.
|
||||
double EatDouble(char const * str, char ** strEnd)
|
||||
{
|
||||
bool gotDigitBeforeMark = false;
|
||||
bool gotMark = false;
|
||||
bool gotDigitAfterMark = false;
|
||||
char const * markPos = nullptr;
|
||||
char const * p = str;
|
||||
while (true)
|
||||
{
|
||||
if (IsDecimalMark(*p))
|
||||
{
|
||||
if (gotMark)
|
||||
break;
|
||||
gotMark = true;
|
||||
markPos = p;
|
||||
}
|
||||
else if (isdigit(*p))
|
||||
{
|
||||
if (gotMark)
|
||||
gotDigitAfterMark = true;
|
||||
else
|
||||
gotDigitBeforeMark = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
++p;
|
||||
}
|
||||
|
||||
if (gotDigitBeforeMark && gotMark && gotDigitAfterMark)
|
||||
{
|
||||
string const part1(str, markPos);
|
||||
string const part2(markPos + 1, p);
|
||||
*strEnd = const_cast<char *>(p);
|
||||
auto const x1 = atof(part1.c_str());
|
||||
auto const x2 = atof(part2.c_str());
|
||||
return x1 + x2 * pow(10.0, -static_cast<double>(part2.size()));
|
||||
}
|
||||
|
||||
return strtod(str, strEnd);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace search
|
||||
{
|
||||
bool MatchLatLonDegree(string const & query, double & lat, double & lon)
|
||||
{
|
||||
// should be default initialization (0, false)
|
||||
array<pair<double, bool>, 6> v;
|
||||
|
||||
int base = 0;
|
||||
|
||||
// Positions of N, S, E, W symbols
|
||||
char const * arrPos[] = {nullptr, nullptr, nullptr, nullptr};
|
||||
bool arrDegreeSymbol[] = { false, false };
|
||||
|
||||
char const * const startQuery = query.c_str();
|
||||
char const * s = startQuery;
|
||||
while (true)
|
||||
{
|
||||
char const * s1 = s;
|
||||
SkipNSEW(s, arrPos);
|
||||
if (!*s)
|
||||
{
|
||||
// End of the string - check matching.
|
||||
break;
|
||||
}
|
||||
|
||||
SkipSpaces(s);
|
||||
char * s2;
|
||||
double const x = EatDouble(s, &s2);
|
||||
if (s == s2)
|
||||
{
|
||||
// invalid token
|
||||
if (s == s1)
|
||||
{
|
||||
// Return error if there are no any delimiters.
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Check matching if token is delimited.
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (x < 0 && s == s1 && !(s == startQuery || kSpaces.find(*(s-1)) != string::npos))
|
||||
{
|
||||
// Skip input like "3-8"
|
||||
return false;
|
||||
}
|
||||
|
||||
s = s2;
|
||||
SkipSpaces(s);
|
||||
|
||||
int i = GetDMSIndex(s);
|
||||
bool degreeSymbol = true;
|
||||
if (i == -1)
|
||||
{
|
||||
// try to assign next possible value mark
|
||||
if (arrDegreeSymbol[base / 3])
|
||||
{
|
||||
if (!v[base + 1].second)
|
||||
i = 1;
|
||||
else
|
||||
i = 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
i = 0;
|
||||
degreeSymbol = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (i == 0) // degrees
|
||||
{
|
||||
if (v[base].second)
|
||||
{
|
||||
if (base == 0)
|
||||
{
|
||||
base += 3;
|
||||
}
|
||||
else
|
||||
{
|
||||
// too many degree values
|
||||
return false;
|
||||
}
|
||||
}
|
||||
arrDegreeSymbol[base / 3] = degreeSymbol;
|
||||
}
|
||||
else // minutes or seconds
|
||||
{
|
||||
if (x < 0.0 || x > 60.0 || // minutes or seconds should be in [0, 60] range
|
||||
v[base + i].second || // value already exists
|
||||
!v[base].second || // no degrees found for value
|
||||
(i == 2 && !v[base + 1].second)) // no minutes for seconds
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
v[base + i].first = x;
|
||||
v[base + i].second = true;
|
||||
}
|
||||
|
||||
if (!v[0].second || !v[3].second)
|
||||
{
|
||||
// degree should exist for both coordinates
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((arrPos[0] && arrPos[1]) || (arrPos[2] && arrPos[3]))
|
||||
{
|
||||
// control symbols should match only once
|
||||
return false;
|
||||
}
|
||||
|
||||
// Calculate Lat, Lon with correct sign.
|
||||
lat = fabs(v[0].first) + v[1].first / 60.0 + v[2].first / 3600.0;
|
||||
if (v[0].first < 0.0) lat = -lat;
|
||||
|
||||
lon = fabs(v[3].first) + v[4].first / 60.0 + v[5].first / 3600.0;
|
||||
if (v[3].first < 0.0) lon = -lon;
|
||||
|
||||
if (max(arrPos[0], arrPos[1]) > max(arrPos[2], arrPos[3]))
|
||||
swap(lat, lon);
|
||||
|
||||
if (arrPos[1] != nullptr)
|
||||
lat = -lat;
|
||||
if (arrPos[3] != nullptr)
|
||||
lon = -lon;
|
||||
|
||||
// Valid input ranges for longitude are: [0, 360] or [-180, 180].
|
||||
// We normalize it to [-180, 180].
|
||||
if (lon < -180.0 || lon > 360.0)
|
||||
return false;
|
||||
|
||||
if (lon > 180.0)
|
||||
lon -= 360.0;
|
||||
|
||||
return fabs(lat) <= 90.0;
|
||||
}
|
||||
} // namespace search
|
||||
|
|
|
@ -60,17 +60,10 @@ UNIT_TEST(LatLon_Degree_Match)
|
|||
TEST(!MatchLatLonDegree("34/31", lat, lon), ());
|
||||
TEST(!MatchLatLonDegree("34,31", lat, lon), ());
|
||||
|
||||
/// @todo 5E-5 eats as full double here. This is a very fancy case, but anyway ...
|
||||
TEST(!MatchLatLonDegree("N5E-5", lat, lon), ());
|
||||
TEST(!MatchLatLonDegree("5E-5", lat, lon), ());
|
||||
|
||||
TEST(MatchLatLonDegree("N5W-5", lat, lon), ());
|
||||
TestAlmostEqual(lat, 5);
|
||||
TestAlmostEqual(lon, 5);
|
||||
// Same as "N5 E-5"
|
||||
TEST(MatchLatLonDegree("5 E-5", lat, lon), ());
|
||||
TestAlmostEqual(lat, 5);
|
||||
TestAlmostEqual(lon, -5);
|
||||
TEST(!MatchLatLonDegree("N5W-5", lat, lon), ());
|
||||
TEST(!MatchLatLonDegree("5 E-5", lat, lon), ());
|
||||
|
||||
TEST(!MatchLatLonDegree("., .", lat, lon), ());
|
||||
TEST(!MatchLatLonDegree("10, .", lat, lon), ());
|
||||
|
@ -85,33 +78,83 @@ UNIT_TEST(LatLon_Degree_Match)
|
|||
|
||||
TEST(!MatchLatLonDegree("50* 40*, 30*", lat, lon), ());
|
||||
|
||||
TEST(MatchLatLonDegree("(-50°30\'30\" -49°59\'59\"", lat, lon), ());
|
||||
TEST(MatchLatLonDegree("(S50°30\'30\" W49°59\'59\"", lat, lon), ());
|
||||
TestAlmostEqual(lat, -50.50833333333333);
|
||||
TestAlmostEqual(lon, -49.99972222222222);
|
||||
|
||||
TEST(!MatchLatLonDegree("50°, 30\"", lat, lon), ());
|
||||
TEST(!MatchLatLonDegree("50\', -50°", lat, lon), ());
|
||||
TEST(MatchLatLonDegree("60* 0\' 0\" N, 0* 0\' 30\" E", lat, lon), ()); // full form, case #1
|
||||
TestAlmostEqual(lat, 60);
|
||||
TestAlmostEqual(lon, 0.0083333333333333332);
|
||||
|
||||
TEST(MatchLatLonDegree("60°, 30\"", lat, lon), ()); // short form, case #1
|
||||
TestAlmostEqual(lat, 60);
|
||||
TestAlmostEqual(lon, 0.0083333333333333332);
|
||||
|
||||
TEST(MatchLatLonDegree("0* 50\' 0\" N, 50° 0\' 0\" E", lat, lon), ()); // full form, case #2
|
||||
TestAlmostEqual(lat, 0.83333333333333337);
|
||||
TestAlmostEqual(lon, 50);
|
||||
|
||||
TEST(MatchLatLonDegree("50\', 50°", lat, lon), ()); // short form, case #2
|
||||
TestAlmostEqual(lat, 0.83333333333333337);
|
||||
TestAlmostEqual(lon, 50);
|
||||
|
||||
TEST(!MatchLatLonDegree("60° 30\"", lat, lon), ()); // if short form is ambiguous, latitude and longitude
|
||||
// should be comma separated
|
||||
|
||||
TEST(!MatchLatLonDegree("-90*50\'50\", -50°", lat, lon), ());
|
||||
|
||||
TEST(MatchLatLonDegree("(-89*, 360*)", lat, lon), ());
|
||||
TEST(MatchLatLonDegree("-10,12 -20,12", lat, lon), ());
|
||||
TestAlmostEqual(lat, -10.12);
|
||||
TestAlmostEqual(lon, -20.12);
|
||||
|
||||
TEST(MatchLatLonDegree("-89, 180.5555", lat, lon), ());
|
||||
TestAlmostEqual(lat, -89.0);
|
||||
TestAlmostEqual(lon, -179.4445);
|
||||
|
||||
TEST(MatchLatLonDegree("-89, 181", lat, lon), ());
|
||||
TestAlmostEqual(lat, -89.0);
|
||||
TestAlmostEqual(lon, -179.0);
|
||||
|
||||
TEST(MatchLatLonDegree("-89, 359", lat, lon), ());
|
||||
TestAlmostEqual(lat, -89.0);
|
||||
TestAlmostEqual(lon, -1.0);
|
||||
|
||||
|
||||
TEST(MatchLatLonDegree("-89, 359.5555", lat, lon), ());
|
||||
TestAlmostEqual(lat, -89.0);
|
||||
TestAlmostEqual(lon, -0.4445);
|
||||
|
||||
TEST(MatchLatLonDegree("-89, 359.5555", lat, lon), ());
|
||||
TestAlmostEqual(lat, -89.0);
|
||||
TestAlmostEqual(lon, -0.4445);
|
||||
|
||||
TEST(MatchLatLonDegree("-89, 360", lat, lon), ());
|
||||
TestAlmostEqual(lat, -89.0);
|
||||
TestAlmostEqual(lon, 0.0);
|
||||
|
||||
TEST(MatchLatLonDegree("-89*15.5\' N; 120*30\'50.5\" e", lat, lon), ());
|
||||
TestAlmostEqual(lat, -89.25833333333333);
|
||||
TestAlmostEqual(lon, 120.51402777777778);
|
||||
// DMS edge cases
|
||||
TEST(MatchLatLonDegree("N 89* 59\' 59.99990\", E 179* 59\' 59.99990\"", lat, lon), ());
|
||||
TestAlmostEqual(lat, 89.9999999722222);
|
||||
TestAlmostEqual(lon, 179.999999972222);
|
||||
|
||||
TEST(MatchLatLonDegree("N 89* 59\' 59.99999\", E 179* 59\' 59.99999\"", lat, lon), ());
|
||||
TestAlmostEqual(lat, 89.9999999972222);
|
||||
TestAlmostEqual(lon, 179.999999997222);
|
||||
|
||||
TEST(MatchLatLonDegree("N 90* 0\' 0\", E 180* 0\' 0\"", lat, lon), ());
|
||||
TestAlmostEqual(lat, 90.0);
|
||||
TestAlmostEqual(lon, 180.0);
|
||||
|
||||
// Fail if degree is negative and minute is floating point
|
||||
TEST(!MatchLatLonDegree("-89*15.5\' N; 120*30\'50.5\" e", lat, lon), ());
|
||||
|
||||
TEST(MatchLatLonDegree("N55°45′20.99″ E37°37′03.62″", lat, lon), ());
|
||||
TestAlmostEqual(lat, 55.755830555555556);
|
||||
TestAlmostEqual(lon, 37.617672222222222);
|
||||
|
||||
{
|
||||
TEST(MatchLatLonDegree("N-55°45′20.99″ E-37°37′03.62″", lat, lon), ());
|
||||
double lat1, lon1;
|
||||
TEST(MatchLatLonDegree("S55°45′20.99″ W37°37′03.62″", lat1, lon1), ());
|
||||
TestAlmostEqual(lat, lat1);
|
||||
TestAlmostEqual(lon, lon1);
|
||||
}
|
||||
TEST(MatchLatLonDegree("S55°45′20.99″ W37°37′03.62″", lat, lon), ());
|
||||
TestAlmostEqual(lat, -55.755830555555556);
|
||||
TestAlmostEqual(lon, -37.617672222222222);
|
||||
|
||||
TEST(MatchLatLonDegree("55°45’20.9916\"N, 37°37’3.6228\"E hsdfjgkdsjbv", lat, lon), ());
|
||||
TestAlmostEqual(lat, 55.755831);
|
||||
|
@ -137,21 +180,75 @@ UNIT_TEST(LatLon_Degree_Match)
|
|||
TEST(!MatchLatLonDegree("55°45′20.9916″W 37°37′3.6228″E", lat, lon), ());
|
||||
TEST(!MatchLatLonDegree("N55°45′20.9916″ S37°37′3.6228″", lat, lon), ());
|
||||
|
||||
TEST(MatchLatLonDegree("54° 25' 0N 1° 53' 46W", lat, lon), ());
|
||||
TestAlmostEqual(lat, 54.41666666666667);
|
||||
TestAlmostEqual(lon, -1.89611111111111);
|
||||
TEST(!MatchLatLonDegree("54° 25' 0N 1° 53' 46W", lat, lon), ()); // incomplete form
|
||||
|
||||
TEST(MatchLatLonDegree("47.33471°N 8.53112°E", lat, lon), ());
|
||||
TEST(MatchLatLonDegree("47.33471° 8.53112°", lat, lon), ());
|
||||
TestAlmostEqual(lat, 47.33471);
|
||||
TestAlmostEqual(lon, 8.53112);
|
||||
|
||||
TEST(MatchLatLonDegree("N 51* 33.217 E 11* 10.113", lat, lon), ());
|
||||
TestAlmostEqual(lat, 51.55361666666667);
|
||||
TestAlmostEqual(lon, 11.16855);
|
||||
TEST(MatchLatLonDegree("N 51* 0\' 33.217\" E 11* 0\' 10.113\"", lat, lon), ());
|
||||
TestAlmostEqual(lat, 51.009226944444443);
|
||||
TestAlmostEqual(lon, 11.002809166666667);
|
||||
|
||||
TEST(!MatchLatLonDegree("N 51* 33.217 E 11* 60.113", lat, lon), ());
|
||||
TEST(!MatchLatLonDegree("N 51* -33.217 E 11* 10.113", lat, lon), ());
|
||||
TEST(!MatchLatLonDegree("N 33.217\' E 11* 10.113", lat, lon), ());
|
||||
TEST(!MatchLatLonDegree("N 51* 33.217 E 11* 10.113\"", lat, lon), ());
|
||||
|
||||
// Degrees Decimal Minutes (DDM) tests
|
||||
TEST(MatchLatLonDegree("N 51* 33.217\' E 11* 10.113\'", lat, lon), ());
|
||||
TestAlmostEqual(lat, 51.55361666666667);
|
||||
TestAlmostEqual(lon, 11.16855);
|
||||
|
||||
TEST(MatchLatLonDegree("57* 53.9748\' N, 59* 56.8122\' E", lat, lon), ());
|
||||
TestAlmostEqual(lat, 57.89958);
|
||||
TestAlmostEqual(lon, 59.94687);
|
||||
|
||||
TEST(MatchLatLonDegree("N 57* 53.9748\', E 59* 56.8122\'", lat, lon), ());
|
||||
TestAlmostEqual(lat, 57.89958);
|
||||
TestAlmostEqual(lon, 59.94687);
|
||||
|
||||
TEST(MatchLatLonDegree("S 57* 53.9748\', E 59* 56.8122\'", lat, lon), ());
|
||||
TestAlmostEqual(lat, -57.89958);
|
||||
TestAlmostEqual(lon, 59.94687);
|
||||
|
||||
TEST(MatchLatLonDegree("N 57* 53.9748\', W 59* 56.8122\'", lat, lon), ());
|
||||
TestAlmostEqual(lat, 57.89958);
|
||||
TestAlmostEqual(lon, -59.94687);
|
||||
|
||||
// DDM edge cases
|
||||
TEST(MatchLatLonDegree("N 89* 59,9999990\', E 179* 59,9999990\'", lat, lon), ());
|
||||
TestAlmostEqual(lat, 89.999999983333);
|
||||
TestAlmostEqual(lon, 179.999999983333);
|
||||
|
||||
TEST(MatchLatLonDegree("N 89* 59,9999999\', E 179* 59,9999999\'", lat, lon), ());
|
||||
TestAlmostEqual(lat, 89.999999998333);
|
||||
TestAlmostEqual(lon, 179.999999998333);
|
||||
|
||||
TEST(MatchLatLonDegree("N 90* 0\', E 180* 0\'", lat, lon), ());
|
||||
TestAlmostEqual(lat, 90.0);
|
||||
TestAlmostEqual(lon, 180.0);
|
||||
|
||||
|
||||
// Fail if cardinal direction marks is same
|
||||
TEST(!MatchLatLonDegree("57* 53\' 58.488\"N, 59* 56\' 48.732\"N", lat, lon), ());
|
||||
TEST(!MatchLatLonDegree("57* 53\' 58.488\"S, 59* 56\' 48.732\"S", lat, lon), ());
|
||||
TEST(!MatchLatLonDegree("57* 53\' 58.488\"E, 59* 56\' 48.732\"E", lat, lon), ());
|
||||
TEST(!MatchLatLonDegree("57* 53\' 58.488\"W, 59* 56\' 48.732\"W", lat, lon), ());
|
||||
|
||||
|
||||
// Fail if degree value is floating point (DMS and DDM notation)
|
||||
TEST(!MatchLatLonDegree("57.123* 53\' 58.488\"N, 59* 56\' 48.732\"E", lat, lon), ());
|
||||
TEST(!MatchLatLonDegree("57.123* 58.488\'N, 59* 48.732\'E", lat, lon), ());
|
||||
|
||||
// Fail if minute value is floating point (for DMS notation)
|
||||
TEST(!MatchLatLonDegree("57* 53.123\' 58.488\"N, 59* 56.123\' 48.732\"E", lat, lon), ());
|
||||
|
||||
// Fail if minute value is greater than 59 (for DMS notation)
|
||||
TEST(!MatchLatLonDegree("57* 60\' 58.488\" N, 59* 56\' 48.732\" E", lat, lon), ());
|
||||
|
||||
// Fail if degree value is greater than 180 (DMS and DDM notation)
|
||||
TEST(!MatchLatLonDegree("N 89* 10\' 3\", E 183* 3\' 3\"", lat, lon), ());
|
||||
TEST(!MatchLatLonDegree("N 89* 10\', E 183* 3\'", lat, lon), ());
|
||||
}
|
||||
} // namespace
|
||||
|
|
Reference in a new issue