js8call/JS8_Include/Maidenhead.h
2026-01-18 19:19:15 -06:00

197 lines
6.1 KiB
C++

#ifndef MAIDENHEAD_HPP__
#define MAIDENHEAD_HPP__
#include <QString>
#include <QStringView>
#include <QValidator>
namespace Maidenhead {
// Given a numeric Unicode value, return the upper case version if
// it lies within the range of lower case alphabetic characters.
//
// A replacement for QChar::toUpper(), which isn't constexpr.
constexpr char16_t normalize(char16_t const u) noexcept {
return (u >= u'a' && u <= u'z') ? u - (u'a' - u'A') : u;
}
static_assert(normalize(u'0') == u'0');
static_assert(normalize(u'A') == u'A');
static_assert(normalize(u'Z') == u'Z');
static_assert(normalize(u'a') == u'A');
static_assert(normalize(u'z') == u'Z');
// Given a string view, return the index at which the view fails to
// contain a valid id, or QStringView::size() if the view is valid.
//
// Note carefully the following:
//
// 1. A view that's incomplete, but still valid up to the point
// of being incomplete, is valid.
// 2. An odd-length view is, therefore, valid.
// 3. A zero-length view is also valid.
//
// There is therfore more validation required above this point; the
// only assertion we make on completely valid input is that it's ok
// so far, but we're not asserting that it's complete.
//
// Validation is case-insensitive. While the standard defines pairs
// containing alphabetic characters as being upper case, the older
// QRA standard used lower case, and various software packages do
// either or both, so we're being liberal in what we accept.
constexpr auto invalidIndex(QStringView const view) noexcept {
// Standard Maidenhead identifiers must be exactly 4, 6 or 8
// characters. Indices and valid values for the pairs are:
//
// 1. Field: [0, 1]: [A, R]
// 2. Square: [2, 3]: [0, 9]
// 3. Subsquare: [4, 5]: [A, X]
// 4. Extended: [6, 7]: [0, 9]
//
// Nonstandard extensions exist in domains such as APRS, which
// add up to an additional two pairs:
//
// 5. Ultra Extended: [ 8, 9]: [A, X]
// 6. Hyper Extended: [10, 11]: [0, 9]
auto const size = view.size();
for (qsizetype i = 0; i < size; ++i) {
auto const u = normalize(view[i].unicode());
switch (i) {
case 0:
case 1:
if (u >= u'A' && u <= u'R')
continue;
break;
case 2:
case 3:
case 6:
case 7:
case 10:
case 11:
if (u >= u'0' && u <= u'9')
continue;
break;
case 4:
case 5:
case 8:
case 9:
if (u >= u'A' && u <= u'X')
continue;
break;
}
return i;
}
return size;
}
static_assert(invalidIndex(u"") == 0);
static_assert(invalidIndex(u"S") == 0);
static_assert(invalidIndex(u"AZ") == 1);
static_assert(invalidIndex(u"AAA") == 2);
static_assert(invalidIndex(u"AA00AA00AA00A") == 12);
// Given a string view, return true if it has a length compatible with
// containment of the range of pairs requested, and the data within it
// is valid over the complete span, false otherwise.
template <qsizetype Min = 2, qsizetype Max = 6>
constexpr auto valid(QStringView const view) noexcept {
static_assert(Min >= 1 && Max >= 1 && Max <= 6 && Min <= Max);
if (auto const size = view.size();
!(size & 1) && (size >= 2 * Min) && (size <= 2 * Max)) {
return invalidIndex(view) == size;
}
return false;
}
static_assert(valid(u"AA00"));
static_assert(valid(u"AA00AA"));
static_assert(valid(u"AA00AA00"));
static_assert(valid(u"BP51AD95RF"));
static_assert(valid(u"BP51AD95RF00"));
static_assert(valid(u"aa00"));
static_assert(valid(u"AA00aa"));
static_assert(valid(u"RR00XX"));
static_assert(!valid(u""));
static_assert(!valid(u"A"));
static_assert(!valid(u"0"));
static_assert(!valid(u"AA00 "));
static_assert(!valid(u"AA00 "));
static_assert(!valid(u"AA00 "));
static_assert(!valid(u" AA00"));
static_assert(!valid(u" AA00"));
static_assert(!valid(u"00"));
static_assert(!valid(u"aa00a"));
static_assert(!valid(u"AA00ZZA"));
static_assert(!valid(u"!@#$%^"));
static_assert(!valid(u"123456"));
static_assert(!valid(u"AA00ZZ"));
static_assert(!valid(u"ss00XX"));
static_assert(!valid(u"rr00yy"));
static_assert(!valid(u"AAA1aa"));
static_assert(!valid(u"BP51AD95RF00A"));
static_assert(!valid(u"BP51AD95RF0000"));
// Template specifying a QValidator, where the minimum and maximum
// number of acceptable pairs must be specified.
//
// In order for an input to be acceptable, at least the minimum number
// of pairs must be provided, no more than the maximum can be provided,
// and all pairs must be valid.
template <qsizetype Min, qsizetype Max>
class Validator final : public QValidator {
static_assert(Min >= 1 && Max >= 1 && Max <= 6 && Min <= Max);
using QValidator::QValidator;
State validate(QString &input, int &pos) const override {
// Ensure the input is upper case and get the size.
input = input.toUpper();
auto const size = input.size();
// If nothing's been entered, we need more from them; if over
// the maximum, less.
if (size == 0)
return Intermediate;
if (size > Max * 2)
return Invalid;
// If anything up to the cursor is invalid, then we're invalid.
// Anything after the cursor, we're willing to be hopeful about.
if (auto const index = invalidIndex(input); index != size) {
return index < pos ? Invalid : Intermediate;
}
// Entire input was valid. If the count is odd, or we haven't yet
// hit the minimum, we need more from them, otherwise, we're good.
return ((size & 1) || (size < Min * 2)) ? Intermediate : Acceptable;
}
};
// Convenience definitions:
//
// Standard: User must specify field and square, and can optionally
// specify subsquare. Ideal for QSO logging.
//
// Extended: User must specify field and square, and can optionally
// specify subsquare, extended, ultra extended, and hyper
// extended. Ideal for station grid entry.
using StandardValidator = Validator<2, 3>;
using ExtendedValidator = Validator<2, 6>;
} // namespace Maidenhead
#endif