Исходники.Ру - Программирование
Исходники
Статьи
Книги и учебники
Скрипты
Новости RSS
Магазин программиста

Главная » Статьи по программированию » .NET - Все статьи »

Обсудить на форуме Обсудить на форуме

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

Весьма значительная часть бизнес-приложений так или иначе связана с денежными операциями. Однако в C# нет специального типа данных для работы с денежными величинами.

Не торопитесь обвинять меня в склерозе - про тип decimal я не забыл. А вот для того, чтобы разобраться, зачем нужен специальный тип для работы с деньгами, давайте более подробно рассмотрим понятие «Деньги».

Деньги можно представить в виде сущности, содержащей некоторые атрибуты (сумма, валюта, курс расчета с другими валютами и пр.), и с которой можно выполнять некоторые операции. И если для большинства практических задач в качестве атрибута сущности «Деньги» достаточно зафиксировать сумму, то операций должно быть больше:

  • сложение;
  • вычитание;
  • умножение;
  • деление;
  • распределение;
  • сравнение;
  • преобразование.
Несмотря на то, что большинство операций интуитивно понятны, практически каждая из них может иметь «тонкие» особенности.

Рассмотрим операции более подробно.

Сложение

При реализации операции сложения денег необходимо определить ответы на следующие вопросы:

  • должны ли поддерживаться разные валюты, т.е. допустима ли операция типа «сложить 2 рубля и 10 долларов»;
  • если должны поддерживаться разные валюты, то как выполнять округление в случае операций между валютами с разной шкалой, т.е. что должно получаться, например, в результате операции «сложить 2 рубля и 10 долларов», если результат должен быть в долларах;
  • должны ли поддерживаться отрицательные денежные величины, т.е. является ли сложение эквивалентом соответствующей арифметической операции или нужно сложение по модулю;
  • какова должна быть максимальная разрядность сумм и что делать в случае переполнения.

Любой из перечисленных вопросов может как упростить, так и значительно усложнить реализацию сущности «Деньги» в программе. Так, если нужно поддерживать разные валюты, то операция сложения могла бы реализовываться, например, в соответствии с таким алгоритмом:

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

Вопрос округления приходится рассматривать отдельно, т.к. в разных задачах могут использоваться разные алгоритмы (x - дробная часть):

  • арифметическое округление - x < 0.5: -> (-1), x >= 0.5 -> (+1)
  • отбрасывание дробной части - x -> (-1)
Причем алгоритм округления может зависеть как от предметной области, так и от национальных особенностей. Например, в банковской системе США официально принято дробные части центов оставлять в пользу банка.

Вычитание

Т.к. вычитание в арифметическом смысле аналогично операции сложения, нужно определиться с такими же техническими решениями, как и для сложения.

Отдельный тонкий момент - если принимается решение оперировать только с положительными суммами, что делать, если при a - b возникает ситуация a < b? Пример частного решения - устанавливать нулевой результат и сигнализировать в вызывающий код о нарушении ограничения.

Умножение

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

Деление

Делить можно на целые и вещественные числа. В любом случае имеются потенциальные проблемы округления и потери точности.

Распределение

Специальная операция, которая заключается в делении некоторой суммы на части пропорционально определенным коэффициентам, причем так, чтобы после суммирования полученных частей в результате иметь исходную сумму. Для того чтобы тоньше прочувствовать смысл этой операции, попробуйте разделить рубль на троих. Что получится, если использовать обычное деление и затем округление? Правильно, по 33 копейки на брата. А теперь сложим обратно - и в итоге получим 0,99 рубля, потеряв копейку.

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

Такого рода ошибки округления провоцируют ситуацию, когда, например, при финансовых проверках фискальными органами (которые, как правило, выполняют проверку на уровне сводных сумм) могут возникнуть заметные расхождения. И если за «неточности» в пределах копеек и даже рублей штрафы обычно не накладывают, то доверие к таким расчетным задачам резко падает, что заставляет пользователя перепроверять результаты, т.е. выполнять лишний объем работы.

В бумажной практике расчетов бухгалтеры обычно добавляют лишнюю копейку нескольким людям, чтобы получить полный баланс. Пресловутый рубль на троих в бухгалтерии разделился бы так: 0,34 + 0,33 + 0,33 = 1,00

А приведенный выше расчет бригадного подряда должен был бы выглядеть примерно так:

Сравнение

Если не обеспечивать одновременную работу с разными валютами, то сравнение выполняется без особых проблем. Если же должны поддерживаться разные валюты, то необходимо учитывать погрешность сравнения. Попробуйте в качестве примера расставить правильные операторы сравнения при условии, что курс перевода 1 доллар = 31 руб. 57 коп.:

1 доллар ? 31 руб. 56 коп.
1 доллар ? 31 руб. 58 коп.

Означает ли это, что 31 руб. 56 коп. равно 31 руб. 58 коп.?

Преобразование

Самые, казалось бы, простые действия:

  • представить в виде двух целых чисел (рубли и копейки)
  • представить в виде вещественного числа
  • представить в виде строки.

Однако и здесь есть вопросы:

  • если сумма достаточно большая, не произойдет ли потеря значащих разрядов при преобразовании в вещественное число?
  • как представлять в виде строки - как число, как число с символом соответствующей валюты или как «сумма прописью»?

Что делать

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

Остается одно - разработать свой тип данных для работы с деньгами.

Примем несколько предварительных решений по реализации такого типа данных.

  1. Использовать ли класс или структуру. И то, и другое хорошо описано в документации C#. Исходя из соображений эффективности выберем вариант структуры.
  2. Внутреннее представление денег в контексте структуры. Неплохим решением могло бы быть представление сумм в виде decimal. Строго говоря, именно для операций с деньгами декларирован этот тип в спецификации C#. Но исходя из желания сэкономить память при работе с большими массивами данных используем более компактное решение - long (64 бита против 128 для decimal). В long будем записывать полное количество копеек, а количество рублей при необходимости будем вычислять делением на 100. Конечно, при желании оперировать астрономическими суммами (что там дальше за биллионом?) можно написать другую реализацию на основе decimal – принципиально ничего не изменится.
  3. В пределах одной операции используем только одну валюту. Более того, будем работать только с валютами, аналогичными рублю, где мелкая единица равна 1/100 крупной единицы (копейка/рубль, цент/доллар, пфенниг/марка и пр.).
  4. Обеспечим возможность использования отрицательных сумм.
  5. Округление будем выполнять в соответствии с арифметическими правилами, т.е. x < 0.5: -> (-1), x >= 0.5 -> (+1).
  6. Реализуем 2 операции распределения – «разделить поровну» и «разделить пропорционально коэффициентам». Разные реализации позволят оптимизировать вычислительные алгоритмы.

    Обозначим некоторые ключевые моменты реализации.

    Общая структура

     

    public struct Money {
      // Внутреннее представление - количество копеек
      private long value;
      ...
    }
    
    Конструкторы
    
      public Money(double value) 
      {
        this.value = (long) Math.Round(100 * value, 2);
      }
      public Money(long high, byte low) 
      {
        if (low < 0 || low > 99) throw new ArgumentException();
        if (high >= 0) value = 100 * high + low;
        else value = 100 * high - low;
      }
      // Вспомогательный конструктор
      private Money(long copecks)  
        { this.value = copecks; }
    

    Вспомогательный конструктор будем использовать для внутренних операций с деньгами.

    Свойства

     

      // Количество рублей
      public long High
        { get { return value / 100; } }
      // Количество копеек
      public byte Low
        { get { return (byte) (value % 100); } }
    
    Арифметические операции

    Реализуем следующий набор операций: сложение, вычитание, умножение, деление, остаток от целочисленного деления. В тех случаях, когда в результате операции может получаться дробный остаток, используем арифметическое округление. Операции реализуем как в функциональной форме, так и в виде перегруженных операторов.

    Пример реализации умножения:

     

      // Умножение - функциональная форма
      public Money Multiply(double value) 
        { return new Money((long) Math.Round(this.value * value, 2)); }
      // Умножение - операторная форма
      public static Money operator*(double a, Money b) 
        { return new Money((long) Math.Round(a * b.value, 2)); }
      public static Money operator*(Money a, double b)
        { return new Money((long) Math.Round(a.value * b, 2)); }
    

    Операции распределения

      // Деление на одинаковые части
      // Количество частей должно быть не меньше 2
      public Money[] Share(uint n) 
      {
        if (n < 2) throw new ArgumentException();
        Money lowResult = new Money(value / n);
        Money highResult = 
          lowResult.value >= 0 ? 
            new Money(lowResult.value + 1) : 
            new Money(lowResult.value - 1);
        Money[] results = new Money[n];
        long remainder = Math.Abs(value % n);
        for (long i = 0; i < remainder; i++) results[i] = highResult;
        for (long i = remainder; i < n; i++) results[i] = lowResult;
        return results;
      }
      // Деление пропорционально коэффициентам
      // Количество коэффициентов должно быть не меньше 2
      public Money[] Allocate(params uint[] ratios) 
      {
        if (ratios.Length < 2) throw new ArgumentException();
        long total = 0;
        for (int i = 0; i < ratios.Length; i++) total += ratios[i];
        long remainder = value;
        Money[] results = new Money[ratios.Length];
        for (int i = 0; i < results.Length; i++) 
        {
          results[i] = new Money(value * ratios[i] / total);
          remainder -= results[i].value;
        }
        if (remainder > 0)      
          for (int i = 0; i < remainder; i++) results[i].value++;
        else
          for (int i = 0; i > remainder; i--) results[i].value--;
        return results;
      }
    

    Операции сравнения

    Реализуем операции ==, !=, >, >=, <, <= в виде перегруженных операторов. Пример реализации операции !=

      public static bool operator!=(Money a, Money b) 
        { return a.value != b.value; }
    

    Дополнительно реализуем функциональную форму операции сравнения:

     

      public int CompareTo(Money r) 
      {
        if (value < r.value) return -1;
        else if (value == r.value) return 0;
        else return 1;
      }
    

    Операции преобразования

    Реализуем операторы явного преобразования к типу double и обратно:

      public static implicit operator double(Money r) 
        { return (double) r.value / 100; }
      public static explicit operator Money(double d) 
        { return new Money(d); }
    

    Кроме того, реализуем преобразование денег в строку:

      public override string ToString() 
        { return ((double) this).ToString(); }
      // Преобразования в строку аналогично double
      public string ToString(IFormatProvider provider)
      { 
        if (provider is IMoneyToStringProvider)
          // здесь - формирование числа прописью
          return ((IMoneyToStringProvider) provider).MoneyToString(this);
        else
          // а здесь - обычный double с учетом стандартного провайдера
          return ((double) this).ToString(provider);
      }
      public string ToString(string format)
        { return ((double) this).ToString(format); }
      public string ToString(string format, IFormatProvider provider)
        { return ((double) this).ToString(format, provider); }
    
    

    В принципе можно было бы реализовать и обратное преобразование – из строки в деньги. Но оставим это для любознательных. Для практических же целей достаточно выполнить примерно следующее:

      Money m = (Money) double.Parse(s);
    
    Обещанный бонус «сумма прописью» реализован в методе
      public string ToString(IFormatProvider provider) 
    
    Естественно, реальное преобразование производится с помощью специализированного провайдера.

    Дополнительные операции

    Введем поддержку унарных операций +, -, ++, --. Пример реализации ++:

      public static Money operator++(Money r) 
        { return new Money(r.value++); }
    

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

    В завершение

    Пример использования разработанного типа данных:

        static void Main(string[] args)
        {
          Money a = new Money(10, 50);
          Money b = new Money(5.5);
          double x = 3;
          Money c = new Money();
          Money[] result;
          Console.WriteLine("--- new ---");
          Console.WriteLine("a = {0}", a);
          Console.WriteLine("b = {0}", b);
          Console.WriteLine("c = {0}", c);
          Console.WriteLine("--- Add ---");
          Console.WriteLine("{0} + {1} = {2}", a, b, a.Add(b));
          Console.WriteLine("{0} + {1} = {2}", a, b, a + b);
          Console.WriteLine("--- Subtract ---");
          Console.WriteLine("{0} - {1} = {2}", a, b, a.Subtract(b));
          Console.WriteLine("{0} - {1} = {2}", a, b, a - b);
          Console.WriteLine("--- Multiply ---");
          Console.WriteLine("{0} * {1} = {2}", x, a, a.Multiply(x));
          Console.WriteLine("{0} * {1} = {2}", x, a, x * a);
          Console.WriteLine("--- Divide ---");
          Console.WriteLine("{0} / {1} = {2}", a, x, a.Divide(x));
          Console.WriteLine("{0} / {1} = {2}", a, x, a / x);
          Console.WriteLine("--- % ---");
          Console.WriteLine("{0} % {1} = {2}", a, 4, a % 4);
          Console.WriteLine("--- GetRemainder ---");
          Console.WriteLine("{0} % {1} = {2}", a, 4, a.GetRemainder(4));
          Console.WriteLine("--- Allocate ---");
          c = new Money(1);
          result = c.Allocate(1, 1, 1);
          for (int i = 0; i < result.Length; i++) 
            Console.WriteLine("#{0} = {1}", i, result[i]);
          Console.WriteLine("--- ToString ---");
          Money d = (Money) 1234567.89;
          Console.WriteLine("{0} = {1}", d, 
    d.ToString(new RoubleToStringProvider(false, false, true)));
        }


Может пригодится:


Автор: Ivanov_SV
Прочитано: 5024
Рейтинг:
Оценить: 1 2 3 4 5

Комментарии: (0)

Добавить комментарий
Ваше имя*:
Ваш email:
URL Вашего сайта:
Ваш комментарий*:
Код безопастности*:

Рассылка новостей
Рейтинги
© 2007, Программирование Исходники.Ру