Ежедневный бит (е) C++ # 70, Общая проблема интервью C++: валидатор чисел

Сегодня мы рассмотрим распространенную проблему интервью C++: валидатор чисел.

Учитывая потенциальное число в виде строки, определите, содержит ли строка (только) допустимое десятичное число.

Формат числа (в этом порядке):

  1. десятичное число или целое число
  2. необязательно: буква "e" или "E" с последующим целым числом

целое число — это:

  1. необязательно: знак "+-"
  2. одна или несколько цифр (начальные нули допустимы)

десятичное число:

  1. необязательно: знак "+-"
  2. один из следующих форматов:
    а) одна или несколько цифр, за которыми следует «.»
    б) одна или несколько цифр, за которыми следует «.» с последующей одной или несколькими цифрами
    c) «.» за которым следует одна или несколько цифр

Прежде чем вы продолжите читать решение, я рекомендую вам попробовать решить его самостоятельно. Вот ссылка на Compiler Explorer с парой тестов: https://compiler-explorer.com/z/KoKsnvveY.

Решение

Как это типично для задач такого типа, с алгоритмической точки зрения обсуждать нужно очень немногое, а только то, что мы должны стремиться обработать входные данные за один проход.

Основным испытанием для таких типов задач является декомпозиция проблемы и усердие. К счастью, спецификация проблемы завершена и намекает на потенциальную декомпозицию проблемы.

Одним из довольно простых способов обработки строки за один проход в C++ является использование итераторов, и мы можем начать с наиболее часто повторяющейся фразы в спецификации: «одна или несколько цифр».

// Greedily parse a number.
// Returns an iterator after the parsed portion
// (begin if parsing failed).
auto is_number(auto begin, auto end) {
    if (begin == end) return begin;
    auto it = begin;
    // If leading zeroes were not OK, this is the only place
    // we would have to modify.
    while (it != end && std::isdigit(*it))
        ++it;
    return it;
}

Мы пытаемся проанализировать число и вернуть итератор после проанализированной части. Это имеет два преимущества: мы можем легко обнаружить сбой на вызывающем сайте и легко продолжить синтаксический анализ.

Разбор целого числа — это просто вопрос добавления необязательного знака:

// Greedely parse an integer.
// Returns an iterator after the parsed portion (begin if parsing failed).
auto is_integer(auto begin, auto end) {
    if (begin == end) return begin;
    auto start = begin;
    if (*start == '+' || *start == '-') ++start;
    auto it = is_number(start, end);
    return it == start ? begin : it;
}

Код для синтаксического анализа всего десятичного числа — самая сложная часть проблемы, поскольку у нас есть три потенциальных формата вокруг десятичного разделителя и необязательная экспоненциальная часть:

// Greedely parse an integer.
// Returns an iterator after the parsed portion (begin if parsing failed).
auto is_integer(auto begin, auto end) {
    if (begin == end) return begin;
    auto start = begin;
    if (*start == '+' || *start == '-') ++start;
    auto it = is_number(start, end);
    return it == start ? begin : it;
}

// Greedily parse a decimal
// Returns an iterator after the parsed portion (begin if parsing failed).
auto is_decimal(auto begin, auto end) {
    if (begin == end) return begin;
    auto start = begin;
    if (*start == '+' || *start == '-') ++start;
    auto it = is_number(start,end);
    // If we do not have leading digits before the '.'
    // we must have both a '.' and digits after it.
    bool required = (it == start);
    if (it == end)
        return required ? begin : it;
    if (*it == '.') {
        ++it;
        auto tail = is_number(it, end);
        if (required && it == tail) return begin;
        it = tail;
    } else if (required) return begin;
    if (it == end)
        return end;
    // Optional exponent:
    if (*it == 'e' || *it == 'E') {
        ++it;
        auto tail = is_integer(it, end);
        if (tail == it) return begin;
        return tail;
    }
    return it;
}

Наконец, чтобы вернуть логический ответ, нам нужно проверить, полностью ли мы проанализировали ввод:

bool is_valid_number(const std::string& s) {
    // The is_decimal returns s.end() 
    // when the entire string parses as a number.
    return is_decimal(s.begin(), s.end()) == s.end();
}

Откройте решение в Compiler Explorer.