计算机编程
2023年3月26日星期日
Handling Decimal Numbers in C#, Swift, Protocol Buffers, and Postgres
Different languages and platforms have varying ways of handling decimal numbers.
cherry blossom, March 19, 2023

Recently, I've been developing a mobile application for tracking daily expenses and visualizing changes in balance. As you can imagine, there are a lot of arithmetic computations involved in the app. It's a client-server structured software composed of an iOS application using Swift for the front-end and a .NET program using C# for the back-end. I use GRPC as the transport method, and Postgres as the data store.

Without further ado, let's jump into the definitions of "decimal" on these platforms.

C#

"Decimal: Represents a decimal floating-point number."

decimal d1 = new decimal(3.1415926536);

Console.WriteLine(d1);
// 3.1415926536

decimal d2 = Convert.ToDecimal("3.1415926536");

Console.WriteLine(d2);
// 3.1415926536

decimal min = (decimal) (1 / Math.Pow(10, 28));
double minLog10 = Math.Log10((double)min);
Console.WriteLine(minLog10);
// -28

decimal max = decimal.MaxValue;
double maxLog10 = Math.Log10((double)max);
Console.WriteLine(maxLog10);
// 28.898879583742193
use_decimal.cs

It's worth noting that the binary representation of a "decimal" value is 128 bits consisting of a 96-bit integer number and a 32-bit set of flags.

var decimalPlaces = Math.Log10(Math.Pow(2,96));
Console.WriteLine(decimalPlaces);
// 28.898879583742193
decimal_places.cs

You can assume that there are 28 digits of numbers that are usable. As a currency application, the decimal places should be consistent across the system.

Swift:

"Decimal: A structure representing a base-10 number."

In Swift, the Decimal type is a struct that represents a base-10 number. However, the documentation doesn't provide much detail beyond that. After a bit of searching, I found that Decimal is actually a bridge to the NSDecimalNumber type, which states:

"An instance can represent any number that can be expressed as mantissa x 10^exponent where mantissa is a decimal integer up to 38 digits long, and exponent is an integer from –128 through 127."

To work with decimal numbers in Swift, you can use the same basic arithmetic operators that you would use with any other numeric type. Here's some sample code:

import Foundation

var d1: Decimal = 3.1415926536

print("\(d1)")
// 3.141592653600000512 🤔️

var d2: Decimal = Decimal(string: "3.1415926536")!

print("\(d2)")
// 3.1415926536 👌

var min: Decimal = Decimal.leastNonzeroMagnitude
let minLog10 = log10(Double(truncating: min as NSNumber))
print(minLog10)
// -127.0

var max: Decimal = Decimal.greatestFiniteMagnitude
let maxLog10 = log10(Double(truncating: max as NSNumber))
print(maxLog10)
// 165.5318394449896
use_decimal.swift

As you can see, Swift's Decimal type has some quirks when it comes to precision. It's worth noting that the Decimal type is part of the Foundation framework, which is available on iOS, macOS, watchOS, and tvOS.

Postgres:

Moving on to Postgres, the NUMERIC data type is used to store decimal numbers. The total number of digits and decimal places can be set to any value in the range -1000 to 1000. Here's an example of how to create a table with a NUMERIC column:

CREATE TABLE wallet (
id SERIAL PRIMARY KEY, 
name TEXT NOT NULL, 
balance NUMERIC(26,2)
);
wallet.sql

Protocol Buffers:

Finally, let's take a look at how to handle decimal values in Protocol Buffers. Unfortunately, there is no built-in decimal type in Protocol Buffers. However, you can define a message that expresses a decimal value. Here's an example:

message DecimalValue {
    // Whole units part of the amount
    int64 units = 1;

    // Number of nano (10^-8) units of the amount.
    // The value must be between -99,999,999 and +99,999,999 inclusive.
    // If `units` is positive, `nanos` must be positive or zero.
    // If `units` is zero, `nanos` can be positive, zero, or negative.
    // If `units` is negative, `nanos` must be negative or zero.
    int32 epsilon = 3;
}
decimal_value.proto

Actually, an int64 is enough to express an 18-digit number. You can use this message to represent decimal values in your Protocol Buffers messages.

In conclusion, decimal numbers can be handled in different ways depending on the language and platform you're working with. It's important to be aware of these differences to avoid unexpected behavior or errors in your code. Whether you're working with C#, Swift, Protocol Buffers, or Postgres, understanding how decimal numbers are handled can help you write more reliable and efficient code.