[Money Processing] How can business calculations ensure that accuracy is not lost?

Project-driven learning and practice-tested knowledge

foreword

Many systems have the requirement of "processing money", such as e-commerce systems, financial systems, cashier systems, and so on. As long as it has something to do with money, you have to treat it with 120,000 points of energy, and you can't make a single mistake, otherwise it will be a disaster for the system and users.

There are two main aspects to the accuracy of guaranteed amounts: overflow and precision . Overflow means that there is enough space to store data, and it cannot be stored if the amount is too large. Accuracy means that there should be no deviation when calculating the amount, and neither more nor less will work.

Everyone knows how to solve the overflow problem. Just choose a numeric type with a long number of digits, that is, don’t  float use it  double . The precision problem double cannot be solved, because floating-point numbers will cause precision loss.

Let's intuitively feel the loss of precision:

double money = 1.0 - 0.9;

Everyone knows the result of this calculation  0.1, but the actual result is  0.09999999999999998. This phenomenon occurs because the bottom layer of the computer is binary operations, and binary cannot accurately represent decimal decimals. Therefore, in precise calculations such as commercial calculations, other data types must be used to ensure that the precision is not lost, and floating-point numbers must not be used.

Next, this crab will explain in detail how to perform commercial calculations in actual development, and put all the code and SQL statements on Github (the address is at the end of the article), and it can be run after being cloned.

solution

There are two data types that can meet the needs of business computing. The first is naturally the Decimal type specially designed for business computing   , and the second is a fixed-length integer .

Decimal

Regarding the choice of data types, one must consider the database, and the other must consider the programming language. That is, what type is used to store data in the database , and what type is used to process data in the code .

At the database level, the type is naturally used  decimal , because there is no loss of precision in this type, and it is perfect for commercial calculations.

decimal The syntax  for defining a field as  is decimal(M,N), M represents how many digits to store, and N represents how many decimal places to store. Assume  decimal(20,2), it means that a total of 20 digits are stored, of which decimals occupy 2 digits.

We create a new user table with two simple fields, primary key and balance:

Here, 2 points are reserved for the decimal position, which means that the amount is only stored in cents , and the unit stored in the actual project depends on the business needs, which is all possible.

The database level is done. Let's look at the code level. In Java, the type corresponds to the database  decimal ,  java.math.BigDecimaland it can naturally ensure that the precision is completely accurate.

BigDecimalThere are three main ways to create :

BigDecimal d1 = new BigDecimal(0.1); // BigDecimal(double val)
BigDecimal d2 = new BigDecimal("0.1"); // BigDecimal(String val)
BigDecimal d3 = BigDecimal.valueOf(0.1); // static BigDecimal valueOf(double val)

The first two are constructors, and the latter is a static method. All three methods are very convenient, but the first method is forbidden! Take a look at the print results of each of these three objects to see why:

d1: 0.1000000000000000055511151231257827021181583404541015625
d2: 0.1
d3: 0.1

In the first method,  double the parameters of the type passed in through the constructor cannot accurately obtain the value. If you want to create it correctly  BigDecimal, you must either  double convert it to a string and then call the constructor, or call the static method directly. In fact, inside the static method is also  double the constructor that will be converted to a string and then called:

If the decimal value is queried from the database, or the decimal value is passed from the front end, the data will be accurately mapped into  BigDecimal an object, so we don't have to worry about this.

After talking about creation, let's talk about the most important numerical operations. Operations are nothing more than addition, subtraction, multiplication and division, which  BigDecimal provide corresponding methods:

BigDecimal add(BigDecimal); // 加
BigDecimal subtract(BigDecimal); // 减
BigDecimal multiply(BigDecimal); // 乘
BigDecimal divide(BigDecimal); // 除

BigDecimal It is an immutable object, which means that these operations will not change the value of the original object, and only a new object will be returned after the method is executed. If you want to update the original value after the operation, you can only reassign it:

d1 = d1.subtract(d2);

There is no proof, let's verify whether the accuracy will be lost:

BigDecimal d1 = new BigDecimal("1.0");
BigDecimal d2 = new BigDecimal("0.9");
System.out.println(d1.subtract(d2));

The output is undoubtedly   0.1.

The code has been able to guarantee that the precision will not be lost, but the division in mathematics may be inexhaustible. For example, if we divide  10 by  3, the following exception will be thrown:


In order to solve the problem of infinite decimals caused by indivisibility, we need to artificially control the precision of decimals. There is another method of division operation that is used to control precision:

BigDecimal divide(BigDecimal divisor, int scale, int roundingMode)

scale The parameter indicates how many decimal places are retained after the operation, and roundingMode the parameter indicates the way to calculate the decimal.

BigDecimal d1 = new BigDecimal("1.0");
BigDecimal d2 = new BigDecimal("3");
System.out.println(d1.divide(d2, 2, RoundingMode.DOWN)); // 小数精度为2,多余小数直接舍去。输出结果为0.33

Enumeration can be used  RoundingMode to conveniently specify the decimal calculation method. In addition to direct rounding, there are also multiple methods such as rounding and rounding up, which can be specified according to specific business needs.

Note that the decimal precision should be controlled in the code as much as possible, not through the database. By default, the database uses rounding to preserve decimal precision.

For example, the decimal precision set in the database is 2,  0.335and if I store it, the final stored value will become  0.34.

Now that we know how to create and manipulate  BigDecimal objects, there is only one last operation left: comparison. Because it is not a basic data type,  == it is definitely not possible to use double equal signs, so let's try to use  equalscomparison:

BigDecimal d1 = new BigDecimal("0.33");
BigDecimal d2 = new BigDecimal("0.3300");
System.out.println(d1.equals(d2)); // false

The output result is  false, because  BigDecimal the  equals method not only compares the value, but also compares the precision, even if the value is the same but the precision is different  false. If you want to determine whether the values ​​are the same, you need to use int compareTo(BigDecimal val)the method:

BigDecimal d1 = new BigDecimal("0.33");
BigDecimal d2 = new BigDecimal("0.3300");
System.out.println(d1.compareTo(d2) == 0); // true

d1 greater than  d2, return  1;

d1 less than  d2, return  -1;

If the two values ​​are equal, return  0.

BigDecimal The usage of is introduced here, let's look at the second solution next.

fixed-length integer

Fixed-length integers, as the name implies, are integers of fixed (decimal) length. It is just a concept, not a new data type, we still use ordinary integers.

Amounts seem to have decimals as a matter of course, but a little thought reveals that decimals are not necessary. The unit of the amount we demonstrated before is yuan , 1.55 which is one yuan and five cents. Then if our unit is angle , the value of one yuan, five cents and five cents will become  15.5. If the unit is then narrowed down to minutes , the value is  155. That's right, as long as the smallest unit is reached, decimals can be omitted! The minimum unit is determined according to business requirements. For example, if the system requires accuracy to centimeters , then the value is 1550. Of course, it is generally accurate to the point, and the units we will demonstrate next are all points.

Let's create a new field now, the type is  bigintand the unit is points:

The corresponding data type in the code is naturally  Long. We are very familiar with the numerical operations of basic types, and we can use the operation operators directly:

long d1 = 10000L; // 100元
d1 += 500L; // 加五元
d1 -= 500L; // 减五元

There is nothing to say about addition and subtraction. There may be decimals in multiplication and division. For example, if a product is discounted by 20%, the operation is to multiply  0.8:

long d1 = 2366L; // 23.66元
double result = d1 * 0.8; // 打八折,运算后结果为1892.8
d1 = (long)result; // 转换为整数,舍去所有小数,值为1892。即18.92元

For decimal operations, the type will naturally become a floating-point number, so we also need to convert the floating-point number to an integer.

Forced transfer will round off all decimals, this rounding does not mean loss of precision . What is the minimum unit of business requirements, we only keep what is, and we don't need to keep the units that are lower than the score. This point  BigDecimal is consistent with that. If only points are needed in the system, the decimal precision is  2, and the remaining decimals are discarded.

However, some business calculations may require other operations such as rounding, which we can  Mathdo through classes:

long d1 = 2366L; // 23.66元
double result = d1 * 0.8; // 运算后结果为1892.8
d1 = (long)result; // 强转舍去所有小数,值为1892
d1 = (long)Math.ceil(result); // 向上取整,值为1893
d1 = (long)Math.round(result); // 四舍五入,值为1893
...

Let's look at the division operation. When dividing integers by integers, all decimals are automatically rounded off:

long d1 = 2366L;
long result = d1 / 3; // 正确的值本应该为788.6666666666666,舍去所有小数,最终值为788

If you want to perform other decimal operations such as rounding, perform floating-point operations first, and then convert to integers:

long d1 = 2366L;
double result = d1 / 3.0; // 注意,这里除以不是 3,而是 3.0 浮点数
d1 = (long)Math.round(result); // 四射勿入,最终值为789,即7.89元

Although both database storage and code operations are integers, if the front-end display is still in cents , it is not very user-friendly. So after the backend passes the value to the frontend, the frontend needs to divide the value by itself  100and display it to the user in yuan . Then when the front end passes the value to the back end, it is still passed as an agreed integer.

ending

The processing of the amount is explained. We learned two business computing scenarios:

  • Decimal type

  • fixed-length integer

In fact, business computing is not technically difficult, but if it is not handled correctly, it will cause incalculable losses. After all, money-related matters are not trivial.

Guess you like

Origin blog.csdn.net/wufaqidong1/article/details/131558942