Gopher refers to north

Money, is the mystery of ancient, there is thick god strong, lack of heart shallow

In a system, especially one related to money, where money is of Paramount importance, the accuracy of the calculation will be the subject of this discussion.

Why is accuracy so important

The phrase “sink or swim” is most appropriate here. If the number of orders and transactions on an e-commerce platform is 1 billion every year, and each order is settled by 1 cent less, the cumulative loss will be 10 million! To put it bluntly, the money lost is a tenth of Mr. Wang’s small target. If due to precision problems in the settlement of customers, less will lose customers, more will lose money. Therefore, it is very important to calculate money accurately!

Why is there a precision problem

For a classic example, let’s see if 0.1 + 0.2 equals 0.3 on a computer.

If you have studied computers in the above case, you should know that computers are binary, and only a small number of numbers can be accurately represented in this way when using binary to represent floating-point numbers (IEEE754). Let’s take 0.3 as an example to look at the conversion from decimal to binary.

Computers have a limit on the number of bits, so it’s impossible to get accurate results when using floating-point numbers. This hard limit cannot be broken, so precision is needed to ensure that the money calculation is as accurate as possible within the allowable margin of error.

The actual representation of floating-point numbers in computers is not discussed further in this article, but can be learned by referring to the following connections:

Single-precision floating point representation:

En.wikipedia.org/wiki/Single…

Double precision floating point number representation:

En.wikipedia.org/wiki/Double…

Floating point converter:

www.h-schmidt.net/FloatConver…

Calculate in floating point numbers

Take the above 0.1 + 0.2 as an example, the error of 0.00000000000000004 can be completely ignored, we try to retain 5 decimal places of accuracy, see the following results.

At this point the results are in line with expectations. This is why it is common to determine whether two floating-point numbers are equal to each other in the form of a – b <= 0.00001, which is simply another form of preserving five digits of precision in the decimal part.

Use an integer

As mentioned earlier, only a small number of floating point numbers can be represented by the IEEE754 standard, whereas integers can accurately represent all numbers in the valid range. So it’s easy to think of using integers to represent floating point numbers.

For example, if the decimal number is set in advance and the 8-bit precision is reserved, 0.1 and 0.2 are expressed as integer 10000000 and 20000000 respectively, and the floating-point number operation is converted to integer operation. Again, take 0.1 + 0.2.

// Indicates that the decimal place retains 8 bits of precision
const prec = 100000000

func float2Int(f float64) int64 {
	return int64(f * prec)
}

func int2float(i int64) float64 {
	return float64(i) / prec
}
func main(a) {
	var a, b float64 = 0.1.0.2
	f := float2Int(a) + float2Int(b)
	fmt.Println(a+b, f, int2float(f))
	return
}
Copy the code

The output of the above code is as follows:

The output is exactly as expected, so using integers to represent floating-point numbers seems like a viable solution. However, we should not limit ourselves to individual cases. More tests are needed.

fmt.Println(float2Int(2.3))
Copy the code

The output of the above code is as follows:

This result is so unexpected, but it is reasonable.

The figure above shows the actual stored value of 2.3 in a computer, so the result when converted using float2Int is 229999999 instead of 230000000.

This result is obviously not in line with expectations, within a certain accuracy range there is still a loss of accuracy, if this code is posted online, there is a high probability that the next day the speed of departure. To solve this problem is also very simple, just introduce github.com/shopspring/decimal can, look at the revised code below.

// Indicates that the decimal place retains 8 bits of precision
const prec = 100000000

var decimalPrec = decimal.NewFromFloat(prec)

func float2Int(f float64) int64 {
	return decimal.NewFromFloat(f).Mul(decimalPrec).IntPart()
}

func main(a) {
	fmt.Println(float2Int(2.3)) // Output: 230000000
}
Copy the code

The result is as expected. Floating point operations (addition, subtraction, multiplication) inside the system can be converted to integer operations, and the result only needs a floating point conversion.

Up to this point, integer computation will suffice for most scenarios, but there are still two issues that need to be noted.

1. Integer indicates whether the range of floating point numbers meets system requirements.

2. Division still needs to be converted to floating-point operations when integers represent floating-point numbers.

An integer represents the range of floating point numbers

Take INT64 as an example, the value range is -9223372036854775808 ~ 9223372036854775807. If we reserve 8 bits of precision for the decimal part, then the rest means that the integer part still has 11 bits, that is, if it only represents money, it can still store tens of billions of dollars. This is more than enough for many systems and small to medium sized companies, but the scope for storing money in this way is still a matter of careful consideration.

An integer represents the division of a floating point number

There is no implicit conversion of integers to floating-point in Go, where the result is still an integer divided by an integer. When we represent floating-point numbers as integers, it is especially important to note that all decimal parts are lost in division, so be sure to convert to floating-point numbers before dividing.

Maximum precision of floating-point and integer types

The value of int64 ranges from -9223372036854775808 to 9223372036854775807. If an integer is used to represent the floating point type, the valid decimal digits of the integer part and the decimal part are a maximum of 19.

Uint64 The value ranges from 0 to 18446744073709551615. When the floating point type is expressed as an integer, the valid decimal part of the integer and decimal part is a maximum of 20 digits. The system does not store negative numbers when expressing amount, so it is recommended to use the integer rather than INT64.

Float64 is based on IEEE754 standards and wikipedia with valid decimal bits ranging from 15 to 17.

Let’s look at the following example.

var (
	a float64 = 123456789012345.678
	b float64 = 1.23456789012345678
)

fmt.Println(a, b, decimal.NewFromFloat(a), a == 123456789012345.67)
return
Copy the code

The output of the above code is as follows:

Based on the output, float64 cannot represent a decimal number with more than 17 significant digits. In terms of valid decimal bits, Xu prefers to use integers for floating-point numbers.

Try to retain as much precision as possible in your calculations

Given the importance of precision and the maximum precision that integer and floating point types can represent, let’s use a practical example to explore whether to keep the specified precision in the calculation.

var (
	// Total AD platform revenue is $7.11
	fee float64 = 7.1100
	// Below are the number of hits from different channels
	clkDetails = []int64{220.127.172.1.17.1039.1596.200.236.151.91.87.378.289.2.14.4.439.1.2373.90}
	totalClk   int64
)
// Count the total number of hits from all channels
for _, c := range clkDetails {
	totalClk += c
}
var (
	floatTotal float64
	// Calculate the revenue per click in floating-point numbers
	floatCPC float64 = fee / float64(totalClk)
	intTotal int64
	// Calculate revenue per click with 8-bit precision (revenue per click converted to integer)
	intCPC        int64 = float2Int(fee / float64(totalClk))
	intFloatTotal float64
	// Calculate the payoff per click as an 8-bit integer (keep the payoff per click as floating-point)
	intFloatCPC  float64 = float64(float2Int(fee)) / float64(totalClk)
	decimalTotal         = decimal.Zero
	// Compute revenue per click in Decimal
	decimalCPC = decimal.NewFromFloat(fee).Div(decimal.NewFromInt(totalClk))
)
// Calculate the revenue from each channel and add it up
for _, c := range clkDetails {
	floatTotal += floatCPC * float64(c)
	intTotal += intCPC * c
	intFloatTotal += intFloatCPC * float64(c)
	decimalTotal = decimalTotal.Add(decimalCPC.Mul(decimal.NewFromInt(c)))
}
// add the result comparison
fmt.Println(floatTotal) / / 7.11
fmt.Println(intTotal) / / 710992893
fmt.Println(decimal.NewFromFloat(intFloatTotal).IntPart()) / / 711000000
fmt.Println(decimalTotal.InexactFloat64()) / / 7.1100000000002375
Copy the code

Compared with the above calculation results, only the second one has the lowest accuracy, and the main reason for the loss of accuracy is that float2Int(fee/FLOAT64 (totalClk)) only retains the accuracy of the intermediate calculation result of 8 bits, so there is an error in the result. Other calculation methods retain as much accuracy as possible during the intermediate calculation so that the results are in line with expectations.

A combination of division and subtraction

According to the previous description, floating-point numbers are used in division calculations with as much precision as possible. That still doesn’t solve everything. Let’s look at an example.

// 1 yuan is divided among 3 people. How much does each person share?
var m float64 = float64(1) / 3
fmt.Println(m, m+m+m)
Copy the code

The output of the above code is as follows:

By the calculation results know, each share 0.333333333333 yuan, and will each share of the money again summarized into 1 yuan, then this 0.0000000000000001 yuan jumped out of the stone! Sometimes I really don’t understand these computers.

This result is obviously not in line with human intuition. In order to be more intuitive, we combined subtraction to complete the calculation.

// 1 yuan is divided among 3 people. How much does each person share?
var m float64 = float64(1) / 3
fmt.Println(m, m+m+m)
// The last person's share of the money is subtracted
m3 := 1 - m - m
fmt.Println(m3, m+m+m3)
Copy the code

The output of the above code is as follows:

By subtracting we finally found the missing 0.0000000000000001 yuan. Of course, this is just one of hsu’s examples, and actual calculations may require subtraction from decimal to ensure that money does not disappear or increase.

The above are Lao Xu’s superficial views, any doubts and mistakes please point out in time, sincerely hope that this article can be of some help to all readers.

Note:

When writing this article, the author used the GO version: GO 1.16.6

Some examples used in the article: github.com/Isites/go-c…