Как оптимизировать функции выбора окончаний и конвертации числа в словесное представление?

Рейтинг: 0Ответов: 1Опубликовано: 19.08.2023

У меня есть код на C++, который осуществляет конвертацию чисел в их текстовое представление на русском языке. Но мне кажется, что некоторые части кода могут быть оптимизированы и улучшены.

Структуры "Endings" и "Numbers" содержат массивы строк, используемые для формирования словесных окончаний и числительных. Функция "chooseEnding" выбирает правильное окончание в зависимости от последних цифр числа. Функция "convertNumberToWords" осуществляет конвертацию числа в текст, используя структуры "Endings" и "Numbers".

struct Endings {
    std::array<std::string, 3> rubleEndings = { " рублей", " рубль", " рубля" };
    std::array<std::string, 3> thousandEndings = { " тысяч", " тысяча", " тысячи" };
    std::array<std::string, 3> millionEndings = { " миллионов", " миллион", " миллиона" };
    std::array<std::string, 3> billionEndings = { " миллиардов", " миллиард", " миллиарда" };
};

struct Numbers {
    std::array<std::string, 10> units = { "", " один", " два", " три", " четыре", " пять", " шесть", " семь", " восемь", " девять" };
    std::array<std::string, 10> unitsThousand = { "", " одна", " две", " три", " четыре", " пять", " шесть", " семь", " восемь", " девять" };
    std::array<std::string, 10> teens = { " десять", " одиннадцать", " двенадцать", " тринадцать", " четырнадцать", " пятнадцать", " шестнадцать", " семнадцать", " восемнадцать", " девятнадцать" };
    std::array<std::string, 10> tens = { "", "", " двадцать", " тридцать", " сорок", " пятьдесят", " шестьдесят", " семьдесят", " восемьдесят", " девяносто" };
    std::array<std::string, 10> hundreds = { "", " сто", " двести", " триста", " четыреста", " пятьсот", " шестьсот", " семьсот", " восемьсот", " девятьсот" };
};

std::string chooseEnding(long long number, const std::array<std::string, 3>& endings) {
    long long lastDigit = number % 10;
    long long penultimateDigit = (number / 10) % 10;

    if (penultimateDigit == 1) {
        return endings[0];
    }
    else if (lastDigit == 1) {
        return endings[1];
    }
    else if (lastDigit >= 2 && lastDigit <= 4) {
        return endings[2];
    }
    else {
        return endings[0];
    }
}
    

std::string convertNumberToWords(long long number, Numbers& numbers, Endings& endings)
{
    std::string handleError = "error";
    std::string result;

    const std::vector<std::pair<long long, std::array<std::string, 3>>> divisions = {
     { Constants::Billion, endings.billionEndings },
     { Constants::Million, endings.millionEndings },
     { Constants::Thousand, endings.thousandEndings }
    };

    for (const auto& division : divisions)
    {
        if (number >= division.first)
        {
            long long divisionResult = number / division.first; // Вычисляем результат деления заранее

            if (division.first == Constants::Thousand && (divisionResult == 1 || divisionResult == 2))
                result += numbers.unitsThousand[divisionResult] +chooseEnding(divisionResult, division.second);
            else
                result += convertNumberToWords(divisionResult, numbers, endings) + chooseEnding(divisionResult, division.second);
            number %= division.first;
        }
    }

    if (number >= Constants::Hundred)
    {
        result += numbers.hundreds[number / Constants::Hundred];
        number %= Constants::Hundred;
    }

    if (number >= Constants::Twenty)
    {
        result += numbers.tens[number / Constants::Ten];
        number %= Constants::Ten;
    }

    if (number >= Constants::Ten)
    {
        result += numbers.teens[number - Constants::Ten];
    }
    else if (number > 0)
    {
        result += numbers.units[number];
    }

    return result.empty() ? handleError : result;
}

Мои вопросы:

  1. Как можно оптимизировать функцию "chooseEnding" для повышения эффективности?

  2. Есть ли более лаконичные способы выразить логику выбора окончаний?

  3. Какие методы оптимизации можно предложить для функции "convertNumberToWords"?

  4. Есть ли общие структурные или стилевые советы по этому коду, которые можно было бы учесть

  5. Как можно было бы переписать код, используя указатели на функции и стоит ли это того?

Буду очень благодарен за ваши ценные советы и рекомендации по улучшению данного кода. Спасибо!

Ответы

▲ -2Принят
  1. Как можно оптимизировать функцию "chooseEnding" для повышения эффективности?

IMHO, нечего здесь оптимизировать, и так выглядит довольно эффективно. Единственное, что можно улучшить, это читаемость. Вместо использования связки if-else взять switch. Тут, как бы, на вкус и цвет, все карандаши разные, каждый своё предпочитает, кто-то switch не пользуется никогда, у кого-то штаны обугливаются от длинных if-else. Я бы поменял по одной простой причине: у вас в условном операторе условия строятся по двум переменным penultimateDigit и lastDigit, всё это с одинаковым отступом, можно запутаться.

std::string chooseEnding(long long number, const std::array<std::string, 3>& endings) {
    long long lastDigit = number % 10;
    long long penultimateDigit = (number / 10) % 10;

    switch (penultimateDigit) {
        case 1:
            return endings[0];
        default:
            switch (lastDigit) {
                case 1:
                    return endings[1];
                case 2:
                case 3:
                case 4:
                    return endings[2];
                default:
                    return endings[0];
            }
    }
}

Так, я думаю, более читаемо, но, опять же, на вкус и цвет.
Потом вот ещё. Раз уж сравниваются разные переменные, и хочется использовать if-else, чтобы не запутывать переменными, если penultimateDigit == 1, можно сразу делать return. Если она не равна 1, то делать блок if-else с lastDigit, т.е. выхода из функции не будет и будет обработано следующее условие. Две переменные сравнивать по отдельности в одном if-else не комильфо.

  1. Есть ли более лаконичные способы выразить логику выбора окончаний?

Да вроде всё неплохо, но вместо деления и затем модуля можно ограничиться одной операцией — модулем, и смотреть две последние цифры:

#include <iostream>
#include <array>
#include <string>

struct Endings {
    std::array<std::string, 3> rubleEndings = { " рублей", " рубль", " рубля" };
};

std::string chooseEnding(long long number, const std::array<std::string, 3>& endings) {
    long long lastDigit = number % 10;
    long long lastTwoDigits = number % 100;

    if (lastTwoDigits >= 11 && lastTwoDigits <= 19) {
        return endings[0];
    }

    switch (lastDigit) {
        case 1:
            return endings[1];
        case 2:
        case 3:
        case 4:
            return endings[2];
        default:
            return endings[0];
    }
}


int main() {
    Endings endings;

    for (long long number = 1; number <= 30; ++number) {
        std::string ending = chooseEnding(number, endings.rubleEndings);
        std::cout << number << ending << std::endl;
    }

    return 0;
}

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

  1. Какие методы оптимизации можно предложить для функции "convertNumberToWords"?

Она уже выглядит довольно громоздкой и сложной. Что можно предпринять?

  • Использовать кэширование: если код часто вызывается с одними и теми же числами, можно реализовать кэширование результатов, чтобы избежать повторных вычислений
  • Мемоизация: использовать технику мемоизации, чтобы избежать повторных вычислений для одних и тех же аргументов функции
  • Улучшение структуры данных: использовать более подходящие структуры данных, такие как unordered_map для хранения числительных и окончаний
  1. Есть ли общие структурные или стилевые советы по этому коду, которые можно было бы учесть
  • Избегать магические числа: в коде есть числа, такие как Constants::Billion, Constants::Million и т.д. Лучше использовать константы с понятными именами вместо магических чисел, чтобы код был более читаемым и модифицируемым. Например OneBillion, OneMillion и т.д.
  • Комментарии: тут, кажется, без комментариев 😀 Для сложных функций таких как convertNumberToWords они просто необходимы
  1. Как можно было бы переписать код, используя указатели на функции и стоит ли это того?

Не думаю, что это сильно улучшит код. Его основная сложность связана с рекурсивными вызовами и логикой выбора окончаний, указатели на функции скорее повысят сложность и снизят читаемость, так что я бы сказал, что не стоит. Но если очень хочется, почему нет?