Наши проекты:
Журнал · Discuz!ML · Wiki · DRKB · Помощь проекту |
||
ПРАВИЛА | FAQ | Помощь | Поиск | Участники | Календарь | Избранное | RSS |
[3.146.221.204] |
|
Сообщ.
#1
,
|
|
|
Эта статья рассказывает о том, как работать с денежными величинами в C# максимально точно и безболезненно, не прибегая к использованию имеющихся типов.
Почему не следует юзать встроенные типы данных (например, вполне подходящий decimal)? Казалось бы, взял такой тип и работаешь с деньгами, как душе угодно. Но, все имеющиеся типы имеют свои погрешности, которым при работе с финансами (а в особенности со своими ) просто недопустимы. Мы создадим собственный тип данных, который будет работать исключительно с деньгами. Однако, здесь есть свои особенности - например, понятия "валюта" или "преобразование". Поэтому сначала рассмотрим теоретическую часть работу с финансами. Итак, что можно делать со своими деньгами? Главное, конечно же, основные арифметические действия, то бишь, сложение, вычитание, умножение, деление. Здесь, я думаю, все понятно. Следует только отметить работу с валютами (например, складывание гривень с долларами или вычитание рублей от евро), а также максимальную точность в округлениях и работе с дробями. Из специфических операций выделю такие: сравнение, распределение и преобразование. Со сравнением и преобразованием, я думаю, тоже все ясно, а вот о распределении поговорим подробнее. C помощью операции "распределение" можно разделять некоторую сумму денег на несколько пропорциональных частей, причем эти части будут составлять исходную сумму. Например, при обычном делении 2 рублей на троих человек каждый человек получит по 66 копеек. Но, сложив три раза 66 копеек, мы получим уже не 2 рубля, а только 1 рубль 98 копеек. Так поступает, например, пресловутый Microsoft Excel. При распределении потерянные две копейки будут отданны двум людям, при это сохранится баланс: 67 копеек + 67 копеек + 66 копеек = 2 рубля. После этого маленького экскурса в теорию финансов перейдем к практике, т.е. кодингу. Разработаем новый тип данных, назовем его, естественно, Money. Первым делом создадим основную структуру типа, конструкторы. Также добавим дополнительный конструктор, с помощью которого будем проводить внутренние операции. 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); } } Создав костяк нашего типа данных Money, перейдем к реализации операций. Первыми будут, конечно, арифметические. Вот, например, реализация операции "умножение": // Умножение - функциональная форма 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)); } Операции сложения, вычитания и деления делаются по аналогии. Перейдем к реализации специфических денежных операций. Первой будет сравнение. // Пример перегруженного оператора != 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; } Операция "распределение": // Деление на одинаковые части // Количество частей должно быть не меньше 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; } В качестве операции "преобразование" приведу примеру для конвертировани в тип double и в строку. // Преобразование в тип double public static implicit operator double(Money r) { return (double) r.value / 100; } public static explicit operator Money(double d) { return new Money(d); } // Преобразования в строку аналогично double public override string ToString() { return ((double) this).ToString(); } 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); } Вроде бы все, но в качестве маленького штрижка можно добавить поддержку унарных операций, например ++: 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))); } |