Калькулятор ставки подоходного налога на c ++

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

Я создаю std::map<double, std::string>, который будет представлять ставку налога в виде двойного числа, а требуемый доход — в виде строки. Пользователь вводит свой доход, и ему возвращается максимальная сумма налогов, подлежащих уплате в долларах. Вот код:

#include <stdio.h>
#include <iostream>
#include <cmath>
#include <map>
#include <iomanip>

double TaxesDue(double &income);
double HighestRate(double &income);

std::map<double, std::string> taxRate =
{
    {  0.35, "499999" },
    {0.31, "349999"},
    {0.25,"249999"},
    {0.21,"169999"},
    {0.18,"119999"},
    {0.14,"79999"},
    {0.10,"44999"},
    {0.05,"29999"}
};


int main()
{
    double income;

    std::cout << "Enter your income: " << std::endl;
    std::cin >> income;

    double taxesDue = TaxesDue(income);
    std::cout << taxesDue << std::endl;
}

double TaxesDue(double &income)
{
    double totalTaxes = 0.0;
    double taxDiff = 0.0; //as taxes are calculates, subtract the income

    double highestRate = HighestRate(income);

    for (double i = income; i > 29999; )//knowing the lowest tax rate is 0.05
    {
        taxDiff = (income - std::stod(taxRate[highestRate]));
        totalTaxes += (highestRate * taxDiff);
        i -= taxDiff;
        highestRate = HighestRate(i);
    }

    return totalTaxes;
}

double HighestRate(double &income)
{
    /*
    std::map<double, std::string> taxRate =
    {
        {  0.35, "499999" },
        {0.31,"349999"},
        {0.25,"249999"},
        {0.21,"169999"},
        {0.18,"119999"},
        {0.14,"79999"},
        {0.10,"44999"},
        {0.05,"29999"}
    };
    */

    if (income > std::stod(taxRate[0.35]))
    {
        return 0.35;
    }
    if (income > std::stod(taxRate[0.31]))
    {
        return 0.31;
    }
    if (income > std::stod(taxRate[0.25]))
    {
        return 0.25;
    }
    if (income > std::stod(taxRate[0.21]))
    {
        return 0.21;
    }
    if (income > std::stod(taxRate[0.18]))
    {
        return 0.18;
    }
    if (income > std::stod(taxRate[0.14]))
    {
        return 0.14;
    }
    if (income > std::stod(taxRate[0.10]))
    {
        return 0.10;
    }
    if (income > std::stod(taxRate[0.05]))
    {
        return 0.05;
    }

    return 0.0;
}

Моя самая большая задача — улучшить HighestRate(double &income) метод. Как мне преобразовать if (income > std::stod(taxRate[x])) сделать код лучше? То есть, есть ли какие-нибудь библиотеки, которые я мог бы использовать, чтобы найти ‘where value> then this std :: map value’?

Также я думаю, что мне, вероятно, следует изменить std::map<double, std::string> к std::map<double, double>.

2 ответа
2

Это неплохая идея для учебного проекта, и я бы сказал, что она реализована достаточно хорошо.

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

Обзор дизайна

Вы используете std::map назад

Итак, вам нужен список пар значений, где одно значение представляет собой ставку налога, а другое — нижнюю границу дохода для этой ставки налога. В вашем коде вы сделали ставку налога ключом поиска, а нижний доход ограничил значение.

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

Если бы на вашей карте было наоборот — с тем, что вы фактически хотите найти доход, как ключ — код поиска будет тривиальным:

// income lower bound is the key, tax rate is the value
auto const taxRate = std::map<double, double>{
    {499'999, 0.35},
    {349'999, 0.31},
    // and so on
};

double HighestRate(double income)
{
    // lower_bound() finds the key that is NOT LESS than the argument given
    // (in other words, the key that is greater or equal). So whatever income
    // you give it, it will give you the NEXT greater tax bracket. That means
    // we have to BACKWARDS one tax bracket to get the proper answer.
    //
    // If the data were encoded differently, this could be much simpler, but
    // you have to be careful not to change the logic.

    auto p = taxRate.lower_bound(income);

    // If we got the first tax bracket, there is no lower tax bracket... so
    // no taxes due.
    if (p == taxRate.begin())
        return 0.0;

    // Go backwards one tax bracket.
    --p;

    // Now just return the rate.
    return p->second;
}

На самом деле код будет еще проще, если вы не использовать HighestRate():

double TaxesDue(double income)
{
    double totalTaxes = 0.0;

    // Starting tax bracket:
    auto p = taxRate.lower_bound(income);

    // Convert the starting tax bracket iterator to a REVERSE iterator so we
    // can go backwards (from highest tax bracket down to lowest).
    for (auto r = std::map<double, double>::reverse_iterator{p}; r != taxRate.rend(); ++r)
    {
        // For each tax bracket:

        // 1) subtract the lower bound from the income to get the amount
        //    taxable in that bracket
        auto taxable_amount = income - r->first;

        // 2) calculate the taxes, and add to the total
        totalTaxes += taxable_amount * r->second;

        // 3) the remaining income is everything not yet taxed... which is
        //    the same as the lower bound of the bracket
        income = r->first;
    }

    return totalTaxes;
}

Не относитесь к двум вышеуказанным функциям слишком серьезно; Я не тестировал их и особо не задумывался над их написанием, так что могут быть ошибки.

Я хочу сказать здесь о форме вашей карты. Вы должны спросить, для чего вы используете карту; что ты используешь для поиска? В вашем случае вы используете доход для поиска на карте (чтобы найти налоговую ставку). Это означает, что доход должен быть ключевым, а не ставкой налога.

Дополнительная подсказка для карты

Обратите внимание на функции, которые я написал выше, что мне пришлось вернуться назад и использовать обратные итераторы, все из которых немного увеличили сложность. Что ж, причина в том, что std::map сохранит свои элементы в порядке возрастания, начиная от самого низкого до самого высокого. Но когда вы рассчитываете налоги, вы хотите пойти по другому пути: вы хотите начать с самой высокой налоговой категории и постепенно снижаться. Вы тоже записали данные так: от самой большой до самой маленькой налоговой категории.

Есть способ изменить порядок std::map. std::map имеет 3 параметра шаблона (ну, собственно, 4, включая распределитель). Третий параметр шаблона — это функция компаратора, по умолчанию std::less. Итак, чтобы изменить порядок, все, что вам нужно сделать, это использовать std::greater вместо.

Наблюдать:

auto const taxRate = std::map<double, double, std::greater<>>{
    {499'999, 0.35},
    {349'999, 0.31},
    // and so on
};

double TaxesDue(double income)
{
    double totalTaxes = 0.0;

    // Easy peasy.
    //
    // (Just note we switched to upper_bound() because the
    // greater-than/less-than logic is reversed.)
    for (auto p = taxRate.upper_bound(income); p != taxRate.end(); ++p)
    {
        auto taxable_amount = income - p->first;

        totalTaxes += taxable_amount * p->second;

        income = p->first;
    }

    return totalTaxes;
}

Еще одна вещь, о которой я должен упомянуть: я заметил, что вы использовали тег «хеш-карта». Фактически вы не используете хэш-карту; вы используете карту нормалей. В стандартной библиотеке хеш-карта называется std::unordered_map… но ты делаешь нет хотите использовать это в данном случае, потому что он, как следует из названия, неупорядочен … и вы хочу заказ, чтобы упростить поиск и выполнение расчетов.

Примечание о деньгах и значениях с плавающей запятой

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

Чтобы понять почему, попробуйте запустить эту простую программу:

#include <iostream>

auto main() -> int
{
    auto my_cash = 100.0;

    // Set up cout to print cash values nicely:
    std::cout.setf(std::ios_base::fixed, std::ios_base::floatfield);
    std::cout.precision(2);

    std::cout << "I have in my account: " << my_cash << "nn";

    my_cash += 1e24;
    std::cout << "The bank screwed up and deposited a septillion dollars!n";
    std::cout << "I have in my account: " << my_cash << "nn";

    my_cash -= 1e24;
    std::cout << "The bank took their septillion dollars back.n";
    std::cout << "I have in my account: " << my_cash << "nn";

    std::cout << ":(n";
}

Пример вывода:

I have in my account: 100.00

The bank screwed up and deposited a trillion dollars!
I have in my account: 999999999999999983222784.00

The bank took their trillion dollars back.
I have in my account: 0.00

:(

Лучшая идея — использовать целочисленный тип, фиксированный для наименьшей денежной доли, с которой вы можете захотеть работать. Например, вместо использования double значение 100.0 долларов, вы можете использовать int значение 10000 центов. Или, если вам нужна более высокая точность, вы можете использовать миллионные доли доллара, поэтому 100 долларов будут int со значением 100'000'000. Если int слишком маленький, вы бы использовали long или же long long или же std::int_fast64_t вместо.

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

class money_t
{
    // ...

private:
    std::int_fast64_t _value;
};

constexpr auto calculate_taxes(money_t income)
{
    return income * 0.05; // 0.05 is the tax rate
}

auto income = 100_dollars;

auto taxes = calculate_taxes(income);

std::cout << "you pay " << taxes << " in tax.n";
std::cout << "you have " << (income - tax) << " left over.n";

// the output is automatically nicely formatted as:
//      you pay $5.00 in tax.
//      you have $95.00 left over.

Я бы порекомендовал начать с чего-нибудь более простого:

using money_t = std::int_fast64_t; // or "= long long;" or whatever

… Затем создание надлежащего money_t класс. Это может быть ваш следующий проект! C ++ — язык со строгой типизацией, и я всегда говорю своим студентам, что если вы получите типы прямо в C ++, то все остальное потому что легко. Создание типа денежного значения не является жесткий, но делаю это хорошо займется некоторой работой и научит вас много о языке попутно.

Обзор кода

#include <stdio.h>

Это не заголовок C ++. Правильный заголовок здесь будет <cstdio>… Но, честно говоря, я не понимаю, зачем тебе это нужно.

Вы также ничего не используете из <cmath> или же <iomanip> насколько я могу судить.

Но ты делать использовать std::stod()… который в <string>… Который ты не включать. Это «работает», вероятно, потому что <string> включается чем-то еще… но вы не можете рассчитывать на это.

double TaxesDue(double &income);
double HighestRate(double &income);

это очень редко принимать параметры функции не-const ссылка lvalue. Обычно это делается только в нескольких особых случаях.

Когда вы принимаете аргумент не-const ссылка, вы говорите людям, читающим ваш код, что вы собираетесь менять это значение. Итак, когда я вижу TaxesDue(income), Я предполагаю incomeзначение изменится после вызова функции. Компилятор также предполагает это, что может помешать оптимизации.

Если вы не собираетесь изменять значение аргумента, оно должно быть const:

double TaxesDue(double const& income);
double HighestRate(double const& income);

Тем не мение, double является фундаментальным типом, поэтому вам вообще не нужно брать его по ссылке. Хотя это не больно.

Также обратите внимание, что стандартной практикой в ​​C ++ является размещение модификатора типа с типом:

  • double &income: это стиль C.
  • double& income: это стиль C ++.
std::map<double, std::string> taxRate =

Вы можете захотеть сделать это const.

    double income;

    std::cout << "Enter your income: " << std::endl;
    std::cin >> income;

Пара вещей здесь. Вы должны поместить объявление переменной как можно ближе к точке инициализации. Это лучше:

    std::cout << "Enter your income: " << std::endl;

    double income;
    std::cin >> income;

Также разумно избегать неинициализированных переменных. Ничего страшного, если вы сделаете то, что я сделал выше, и поставите double income; ВЕРНО выше std::cin это фактически инициализирует его. Но как вы это написали, с double income; несколько строк от места инициализации… это опасно.

Один из способов избежать инициализированных переменных — использовать более современную практику «всегда auto”:

// double income;       // <- bad, uninitialized
auto income = double{}; // <- better, cannot possibly be uninitialized

Наконец, избегайте std::endl. Я знаю много обучающих программ, показывающих, как его использовать, но это почти всегда неверно (или, мягко говоря, ненужно). Каждое использование std::endl в вашем коде не так; в каждом случае это вызывает ненужную очистку выходного потока.

Если вам нужна новая строка, не используйте std::endl. Просто используйте n:

int main()
{
    std::cout << "Enter your income: n";

    auto income = double{};
    std::cin >> income;

    auto taxesDue = TaxesDue(income);
    std::cout << taxesDue << 'n';
}

TaxesDue() это круто. Его можно было бы упростить и сделать намного более эффективным, но проблемы на самом деле не в функции, а в способе структурирования данных, как я упоминал в обзоре дизайна.

Единственная большая проблема в TaxesDue() это магическое число 29999. Вы не должны жестко кодировать это в функции. Вам следует использовать карту налоговых ставок; просто получите нижнюю границу дохода из самой низкой налоговой категории … что, с учетом вашего текущего дизайна, просто std::stod(taxRate.begin()->second). (Но если вы поменяли местами ключ карты и значение и использовали doubleс, это было бы taxRate.begin()->first.)

Так что оставляет HighestRate():

double HighestRate(double &income)
{
    /*
    std::map<double, std::string> taxRate =
    {
        {  0.35, "499999" },
        {0.31,"349999"},
        {0.25,"249999"},
        {0.21,"169999"},
        {0.18,"119999"},
        {0.14,"79999"},
        {0.10,"44999"},
        {0.05,"29999"}
    };
    */

    if (income > std::stod(taxRate[0.35]))
    {
        return 0.35;
    }
    if (income > std::stod(taxRate[0.31]))
    {
        return 0.31;
    }

    // ...

Eсть много ненужного повторения здесь. Возьмем, например, налоговую ставку: 0.35 повторяется три раза, один раз на карте налоговых ставок (это нормально), затем дважды в HighestRate().

К сожалению, это не может быть просто, потому что у вас есть карта наоборот. Если бы доход был ключевым, нет проблем, вы просто выполняете обычный поиск по карте с lower_bound() (как я продемонстрировал в разделе обзора дизайна). Но поскольку доход — это ценность, а ставка — ключ, вам придется делать это трудным путем.

Что вам нужно сделать, так это пройтись по карте, проверяя значение. Мало того, вы также должны убедиться, что получаете наибольшую ценность. Что-то вроде:

double HighestRate(double income)
{
    auto highest_amount = 0.0; // or "= std::numeric_limits<double>::lowest();"
    auto result = 0.0;

    for (auto&& [rate, amount_s] : taxRate)
    {
        auto amount = std::stod(amount_s);

        if ((income > amount) and (amount < highest_amount))
            result = rate;
            
    }

    return result;
}

Что, как видите, довольно сложно. Вот почему вы действительно хотите, чтобы доход был ключевым.

  • Спасибо, это здорово. Я рад что могу использовать lower_bound() это все упростит. Также с использованием long long вместо double для большей точности это было бы здорово, и именно поэтому я добавил #include <iomanip> потому что я пытался использовать setprecision(2) но не получил желаемого результата. В C # я бы использовал десятичный тип вместо double или long. Мне нужно многому научиться, но спасибо за помощь.

    — глупые вопросы


В C ++ STL, то set и тесно связанные map контейнеры отсортированный и предоставить эффективный способ поиска заданного элемента (или ключа карты), который не меньше заданного значения. Это достигается с помощью lower_bound метод, доступный как для set а также map. Вы можете использовать это в своих интересах здесь.

Вместо того, чтобы иметь taxRate сопоставьте ставки с минимальным уровнем дохода, сделайте наоборот. Попросите его сопоставить минимальный уровень дохода с налоговыми ставками.

static const IncomeToRateMap TAX_RATE = {
    {29999, 0},       // income <= 29999, rate is 0
    {44999, 0.05},    // income <= 44999, rate is 0.05
    {79999, 0.1},     // ...
};

Таким образом, вы можете запросить карту следующим образом:

double getTaxRateFromIncome(unsigned long income) {
    // This map cannot be modified after initialization (it is constant)
    // and it is only initialized once (not on each function call)
    using IncomeToRateMap = std::map<unsigned long, double>;
    static const IncomeToRateMap TAX_RATE = {
      {29999, 0},       // income <= 29999, rate is 0
      {44999, 0.05},    // income <= 44999, rate is 0.05
      {79999, 0.1},     // ...
      // ...
      {499999, 0.31}//,
      //{std::numeric_limits<unsigned long>::max(), 0.35}
    };
    // Handle the maximum case separately
    // As an alternative, you could add {std::numeric_limits<unsigned long>::max(), 0.35} to the TAX_RATE map
    // which requires #include <limits>
    if (income > 499999) {
        return 0.35;
    }
    // Otherwise use map::lower_bound to get an iterator to the key which is "not less than" income
    IncomeToRateMap::const_iterator pos = TAX_RATE.lower_bound(income);
    return pos->second;
}

Живая демонстрация

Строка против числа на карте

Также я думаю, что мне, вероятно, следует изменить std::map<double, std::string> к std::map<double, double>

Согласитесь, что вы должны сделать только цифры внутренней структуры данных (без строк) и просто преобразовать ввод пользователя из строки в число один раз. С числами работать быстрее, чем со строками.

Хотя я бы предложил изменить доход от double (с плавающей запятой) в unsigned long (неотрицательное целое число), поскольку (по крайней мере, в США) оно округляется до следующего доллара и в любом случае упрощает задачу. Примечание long вместо int так как unsigned int лимиты на уровне ~ 4 миллиардов, и некоторые люди могут иметь доход, превышающий это. (Справедливо это или нет — другая история.)

Переход по ссылке

double HighestRate(double &income)

Это проходит в double по ссылке, что означает, что когда мы вызываем

double myIncome = 133337;
double rate = HighestRate(myIncome);

Переменная myIncome за пределами HighestRate(), а также income внутри HighestRate() одинаковы double переменная в памяти. Так что если как-то HighestRate() измененный incomeвы бы увидели myIncome изменить после HighestRate() отделка.

Иногда это то, что вы хотите сделать, но в данном случае похоже, что это не намерение.

Для небольшого типа данных (например, double), вы можете просто опустить & и это будет означать, что HighestRate()::income это отдельный double переменная и инициализируется / копируется из myIncome. Если HighestRate()::income изменяется, myIncome не трогают.

Если тип данных большой и вы хотите избежать копирования (из-за памяти или производительности во время выполнения), вы можете добавить const к ссылке: HighestRate(const double& income), что означает, что это все та же переменная в памяти, что и myIncome, но он доступен только для чтения (не может быть изменен). Но опять же, для double, в этом нет необходимости, и вам, вероятно, следует просто использовать HighestRate(double).

Живая демонстрация по ссылкам

  • 2

    К вашему сведению, а long в Windows — 32 бита. А long long Однако всегда 64 бита. И, вероятно, имеет смысл использовать для этой задачи целые числа со знаком, поскольку возможен отрицательный доход.

    — Рэй Хэмел

  • Спасибо, это помогло.

    — глупые вопросы

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *