Is Cobol holding you hostage with Math?
medium.com
medium.com
The problem is that fixed point is ambiguous: there are multiple ways to do rounding (unlike floating point which has been largely standardised since the early 90s). In fact, the COBOL rounding rules are rather complicated: https://stackoverflow.com/a/30215718/392585
This has some interesting consequences:
I know of an insurance company where there were difficulties trying to replicate the exact premium calculation (which was done on a mainframe in COBOL), part of which involved taking 1.01 to the power of a positive integer (which depended on various risk factors of the policy).
COBOL was not really intended for numerical work, and so doesn't have a "pow" function (or at least this version didn't): instead, it turns out that the programmer had used a simple loop which would iteratively multiply a variable by 1.01, incurring round-off at each iteration. So the only way to emulate it exactly was to use the same arithmetic _and_ the same ugly hacks used in the original software.
Insurance policies (the contracts) just have final premiums (sometimes they might split it out by risk), but they don't typically explain how the insurer arrived at that number.
That was a knowledge I wish I already had back then, since these sorts of projects provided a lot of frustration, insecurities and expenses that you never could imagine working as software half-pm/half-coder. At some point you start to doubt whether you’re skilled for this job or just a loser with a keyboard.
Minor/simple processes lack that cohesion and can be fixed or left as-is without much trouble though.
Can you imagine if, suddenly, an insurer had a 1% shift in premiums overnight because someone fixed the rounding algorithm to comply with the 1983 spec that had been mis-implemented in COBOL?
(So it's not true that floating-point has been standardized many years ago, recent latest IEEE 754 includes decimal floating-point as it was implemented on IBM mainframe.)
ie, when doing "total = 1.3 + 2 * 10%" all this is in decimal. To make float "total = 1.3f + 2f * 10%".
The reason is that most, if not all, the calculations in my life are around money. I only use floats because that is what every language make me do.
However, I wish to know how do this robustly. How not rely on hacks? How do this ok? I'm working in rust for this.
This is a software implementation of the standardized IEEE decimal format.
But, apart to just have a decimal datatype + overload the operators and provide some functions, exist something else I need to be aware?
* The COBOL knowledge shortage is a myth. I got pretty good at reading their code and giving the COBOL guys small bugfixes while most of the time not even having the compiler at my disposal. Learning COBOL the language is pretty easy, even if actually doing something with it is slow, verbose and bureaucratic. Training new devs will never be the problem if a company and a person were found willing.
* But training is the problem, politically. Why would a dev learn a language that is a bad mark on the resume while being paid less? Why would management want to risk their career by pushing forward an evolutionary dead end?
* And the language is a dead end. NLS in Cobol is really weak, which means it's a no-go for anything global. Fixed width everywhere causes massive technical dept as there is a tendency to 'cheap out on bytes', after which you're completely locked in by the required data migration everywhere. Cobol culture is rife with obsolete practices. One program equals one file (and some libs managed by other cobol devs) most of the time, so methods of a few thousand lines are the norm.
* The real value of the COBOL devs I know is that they are business people first, technical second. They know their business, deep. They've seen everything and smell if an idea is good or bad. Because of this, they run rings around younger devs and analists when you look at business value, especially if said devs/analists are outside consultants that knknow basically nothing and just code what's been fed to them. While training for Cobol is easy, training for the actual business is close to impossible.
* Another Cobol upside is being a technical dinosaur: The Enterprise Architecture Astronauts or bungee-boss CEO won't touch it with a 10 feet pole, so there is no new great architectural vision every other year. Lava flow architecture is not that huge a problem. Besides, your code base lived more than 40 years and is already ugly as hell. The technological investment and training cost have been made long ago. Cobol devs can just convert problem to program without weird technological surprises, integration hell, or architectural distractions.
Don't get me wrong. I'm very glad I don't do COBOL. But its not burn-it-down-and-start-over-now bad either.
Hmm!
I worked on the periphery of a mainframe, COBOL modernization project. Basically an attempted rewrite. Utter failure.
My hunch at the time was the better technical strategy would have been to run all that stuff within an emulator (to get off the nearly dead mainframe) and to focus on process improvements (eg source control, repeatable builds, schema management). But otherwise leave the legacy code in place.
Money values are rationals, not reals.
Languages need to support the full number stack, including the rationals!
"Floating point decimal" is wrong and the worst of both worlds. Unless you live in the USA, you need to do currency conversions anyways, and those aren't decimal.
So what I see is a conclusion that floating point is "emulated reals" as opposed to rationals, despite all floating point numbers being rational numbers and that decimal floating point is wrong and "the worst of both worlds", but I don't see a valid argument for it.
I don't get the argument about libraries and performance. We're talking about companies using emulators of ancient hardware to run decades old software. Pulling in some library is a complete non issue from a performance point of view.
Speaking of hardware, that is magnitudes faster than anything imaginable when most cobol was first written. Performance is not the key concern here.
bigDecimal.doubleValue() + bigDecimal2.doubleValue()
just because Java does not have operator overloading and using methods instead of familiar operators was ugly.
Anyway, BigDecimal has an add() method (overloaded for rounding behaviour)
bdResult = bd1.add( bd2 )
Let us compare:
> bigDecimal.doubleValue() + bigDecimal2.doubleValue()
vs.
> bdResult = bd1.add( bd2 )
The first code is longer (two methods calls, which each do a conversion, one addition, and probably omitted some conversion back to BigDecimal). The second one is just one simple method call. I would strongly argue that the second, right way is much easier than the wrong way.
The primitive types in Java and their operators have always been a hack for performance in the object-oriented type system of Java. So if such code exists in the program, there can be very good reason (in particular performance), but it always has "code smell". So it always should be commented properly.
> if the developers are unaware of the implications
You do not do such a conversation if you have not read into the implications.
bdP = bdA / pow((1 + bdr / bdm), (bdm - bdt));
bdP = bdA.divide((ONE.add(bdr.divide(bdm))).pow(bdm.subtract(bdt)));
Obviously the first is more transparent than the second and this isn't a particularly complicated equation. Mind you that this can be mostly mitigated by splitting up the equation but that can sometimes make solutions quite opaque comparatively.
Using it for formulas makes for wonderful code.
Try
y = (5x^3 - 3x^2 + 7x + 1)/(3x^2 - 7x)
See how readable that is using methods like .add()Side note: the notation we have is not that old; but as soon as people actually started working with polynomials, they invented notation that makes it sane.
That's to say, Java's .add() is so 1400's.
There are languages that allow for a cleaner way to express mathematical statements, this is valid Julia:
julia> p(x) = (5x^3 - 3x^2 + 7x + 1)/(3x^2 - 7x)
p (generic function with 1 method)
julia> p(1 + 2im)
2.4 + 0.2imYou're selling the upside short. Having a pool of software devs to draw from who are familiar with the language and typical deployments is a big deal.
The next y2k crisis for this industry will be in a decade or three when there's few left who have experience here.
Well, in a world where everyone is self-taught from what’s trending at the moment. In the old days if companies needed someone with particular skills they would train them and make an effort to retain them. Now the cost and the risk has all been pushed onto the employee.
It should be a simple equation: cost of training vs cost of rewriting every 2-3 years...
The data type used is "Packed Decimal" where each nybble in a string of bytes represents a digit, except the last nybble. The last nybble describes the sign of the overall number. It's similar to BCD with a sign-nybble at the end.
Here's a list of the Packed Decimal instructions with a description of each.
The main reason is that business and domain logic exists only as COBOL logic. COBOL devs are ~60 on average, and many people who wrote system requirements are probably already dead. Existing code runs millions or billions of dollars worth of transactions often based on arcane financial rules and internal regulations. Good luck untangling that without stopping the business and without losing that functionality.
Relevant links:
- https://uk.reuters.com/article/uk-usa-banks-cobol-idUKKBN17C...
- Reverse engineering a factory: <if someone can find a link to this fascinating story, please help me :)>
PIC 9(3)V9(15).
is really shorthand for PICTURE 999V999999999999999.
Fixed point numbers are declared like that.COBOL is a decent language for business logic. Especially when money amounts are involved. Certainly better than PHP.
Also, performance wise COBOL might be faster on a single machine, but good like trying to scale it out. Plus, a distributed architecture can make it easier to deploy software and hardware upgrades since you can generally take out a few nodes without bringing down the whole system.
I’m not saying all this code should be rewritten. If you have some code out in the wild that works, you should always weigh carefully the risks in terms of cost and potential new bugs before doing a rewrite. However, lack of fixed point is a lousy excuse to not upgrade.
Actually though, I believe that modern Ada would probably be the best upgrade path in these cases. It has a strong static type system, it has first class support for fixed types, with Contracts and SPARK you can get strong guarantees of correctness, and as a compiled language is decently fast.
Contracts and such do automatically insert runtime checks which can effect performance however if they are causing a legitimate issue can be reduced via compiler flags (not super safe) or by verifying that they are unnecessary in code. (demonstrating correctness to the compiler via SPARK)
In systems such as Finance, languages such as Ada (in its modern form) have a real chance to shine given their speed, safety guarantees, and remarkable stability.
COBOL demonstrably scales sufficiently to run the entire global economy, so no luck required.
Of course, that comment's definition of "single machine" ignores the intrinsically distributed nature of mainframe architectures, on which on might reasonably suppose COBOL would run.
[1] e.g. a "single machine" only needs a single replica for those benefits. Sure, even that's a form of distributed architecture, potentially subject to at least some of the Fallacies Of Distributed Computing, but I'm confident that's not what was meant.
But I won’t deny that Sysplex has been proven to work in the field.
It's not a builtin. If it's like the one place I've seen COBOL in, there's a lot of batch processing. When she talks about performance of the "import" statement, she's not kidding. Of course yes, that's a terrible architecture by modern standards, but now we're talking about more than replacing just COBOL.
It's very similar to using an external library (at least if you're in a language with decent package management). Yes there is a "standard" decimal library, but it's not a natural path to take when working with numbers in Python/Java/... : there's no literal support, and in Java's case not even arithmetic operators. These languages naturally drive you towards using IEEE 754 binary floating point, and by the time you've realised that wasn't what you wanted it's often too late to migrate.
I think that’s a massive exaggeration. Most people these days think JavaScript without a framework is “bare metal”.
Gatekeeping is fun.
It would be mayhem! Projects would just NEVER FINISH.
Paranoia is fun!
Programs in COBOL mostly don't need pointers and offer better type safety than C. They often used nested structures of records, which are a pain to represent in C. Language is also stricter when it comes to modularity.
Rewriting in C would make most COBOL programs worse.
Also, floating-point is really not the issue. What you in fact want, on the IBM Mainframe at least, is to move to hardware decimal floating-point (latest IEEE 754), which is way faster than IBM's packed decimal (fixed-point) system commonly used in COBOL.
Finally, legacy COBOL systems either have or not have bugs which cause certain edge-case behavior and this is really hard to reproduce in another system.
https://gist.github.com/divs1210/b2076eeaf533d5e49262bd96843...
(defun r1 (y z)
(- 108 (/ (- 815 (/ 1500 z)) y)))
(defun r (n &aux (l '(4 425/100)))
(loop for i from 2 upto (1+ n)
do (setf l (append l
(list (r1 (elt l (- i 1))
(elt l (- i 2)))))))
l)
CL-USER 68 > (loop for e in (r 20) and i below 20
do (format t
"~%~3a~20,15f"
i
(float e 1.0d0)))
0 4.000000000000000
1 4.250000000000000
2 4.470588235294118
3 4.644736842105263
4 4.770538243626063
5 4.855700712589074
6 4.910847499082793
7 4.945537404123916
8 4.966962581762701
9 4.980045701355631
10 4.987979448478392
11 4.992770288062068
12 4.995655891506634
13 4.997391268381344
14 4.998433943944817
15 4.999060071970894
16 4.999435937146839
17 4.999661524103767
18 4.999796900713418
19 4.999878135477931 | rec results |
rec := [ :y :z | 108 - (815 - (1500 / z) / y) ].
results := { 4. 425/100. } asOrderedCollection.
3 to: 30 do: [ :i |
results add: (rec value: (results at: i - 1) value: (results at: i - 2)).
].
results doWithIndex: [ :each :i |
Transcript show: i; tab; tab; show: each asFloat; cr.
].
Output: 1 4.0
2 4.25
3 4.470588235294118
4 4.644736842105263
5 4.770538243626063
6 4.855700712589074
7 4.910847499082793
8 4.945537404123916
9 4.966962581762701
10 4.980045701355631
11 4.987979448478392
12 4.992770288062068
13 4.995655891506634
14 4.997391268381344
15 4.998433943944817
16 4.999060071970894
17 4.999435937146839
18 4.999661524103767
19 4.9997969007134175
20 4.999878135477931
21 4.9999268795046
22 4.999956127061158
23 4.9999736760057125
24 4.999984205520272
25 4.999990523282228
26 4.99999431395856
27 4.999996588371256
28 4.9999979530213565
29 4.999998771812312
30 4.999999263087206Any Cobol free-lancers hanging out here? Whats you're job perspectives like? Are you filthy rich?
[1] https://qz.com/email/quartz-obsession/1316505/
[2] https://www.reuters.com/article/us-usa-banks-cobol-idUSKBN17...
So what's interesting instead is where are the rumours of a COBOL dev shortage coming from?
A lot of commenters here are missing the point about decimal library support. The point is not that language x does or doesn't have some sort of support for decimal math, the point is it's not native support. Even if language x supports a decimal type, back by a byte[] (for example) there is still function/method call overhead for basic operations (+-/*). For high volume stuff she's talking about, it adds up, fast.
I think there are a few languges with similar native decimal support; ada and pl/1 iirc.
Cobol is an albatross that's going to be with us for a loooong time to come.
With the right style of C++ programming (trait/templated focused instead of inheritance) or Rust's static-dispatch-by-default it should be no more of a performance impact than 'native' Cobol calls after the compiler optimizes it.
COBOL can be used for online system and performance wise a 16 MB (M not G) even for a small mainframe (PC on P5!) can support 1000 uses easily. Whilst it is totally dated, no one support and IBM charge a lot - None is related to cobol, or cics or ims ...
For the floating point part ... not using those. Most of the attention is to handle and agree upon how to do exact calc inclding reminder. No rounding per sec in the system. Nothing lose. Not even one cent. Hence, floating point ...
And change it to c, ada etc. It is English programming language. The hard part is to translate and test. And explain to use decmical as exact number.
... lots of project has successfully migrated. But unless you get that right - cobol is not slow and it is an exact number computation with in depth business know-how, good luck.
The Muller’s Recurrence is a mathematical problem that will converge to 5 only with precise arithmetics. This has nothing to do with COBOL and nothing to do with floating and fixed point arithmetics as such. The more precision your arithmetics has, the closer you get to 5 before departing and eventually converging to 100. Python's fixed point package has 23 decimal points of precision by default, whereas normal 64bit floating point has about 16 decimal points. If you increase your precision, you can linger longer near 5, but eventually you will diverge and then converge to 100.
# Long version
What's going on here is that someone has tried to solve the roots of the polynomial
x^3 - 108 x^2 + 815 x - 1500,
which is equal to (x - 3)(x - 5)(x - 100).
So the roots are 3, 5 and 100. We can derive a two-point iteration method by x^3 = 108 x^2 - 815 x + 1500
x^2 = 108 x - 815 + 1500/z
x = 108 - (815 - 1500/z)/y
where y = x_{n-1} and z = x_{n-2}. But at this point, we don't know yet whether this method will converge, and if yes, to which roots.This iteration method can be seen as a map F from R^2 to R^2:
F(y,z) = (108 - (815 - 1500/z)/y, y).
The roots of the polynomial are 3,5 and 100, so we know that this map F has fixed points (3,3), (5,5) and (100,100). Looking at the derivative of F (meaning the Jacobian matrix) we can see that the eigenvalues of the Jacobian at the fixed points are 100/3 and 5/3, 20 and 3/5, 1/20 and 3/100.So (3,3) is a repulsive fixed point (both eigenvalues > 1), any small deviation from this fixed point will be amplified when the map F is applied iteratively. (100,100) is an attracting fixed point (both eigenvalues < 1). And (5,5) has one eigenvalue much larger than 1, and one slightly less than 1. So this fixed point is attracting only when approached from a specific direction.
Kahan [1, page 3] outlines a method to find sequences that converge to 5. We can choose beta and gamma freely in his method (Kahan has different values for the coefficients of the polynomial, though) and with lots of algebra (took me 2 pages with pen and paper) we can eliminate the beta and gamma and get to the bottom of it. What it comes down to, is that for any 3 < z < 5, choose y = 8 - 15/z, and this pair z,y will start a sequence that converges to 5. But only if you have precise arithmetics with no rounding errors.
For the big picture, we have this map F, you can try to plot a 2D vector field of F or rather F(x,y) - (x,y) to see the steps. Almost any point in the space will start a trajectory that will converge to (100,100), except (3,3) and (5,5) are stable points themselves, and then there is this peculiar small segment of a curve from (3,3) to (5,5), if we start exactly on that curve and use exact arithmetics, we converge to (5,5).
Now that we understand the mathematics, we can conclude:
Any iteration with only finite precision will, at every step, accrue rounding errors and step by step end up further and further away from the mathematical curve, inevitably leading to finally converging to (100,100). Using higher precision arithmetics, we can initially get the semblance of closing in to (5,5), but eventually we will reach the limit of our precision, and further steps will take us away from (5,5) and then converge to (100,100).
The blog post is maybe a little misleading. This has nothing to do with COBOL and nothing to do with fixed point arithmetics. It just happens that by default Python's Decimal package has more precision (28 decimal places) than 64bit floating point (53 binary places, so around 16 decimals). Any iteration, any finite precision no matter how much, run it long enough and it will eventually diverge away from 5 and then converge to 100.
Specifically, if you were to choose floating point arithmetic that uses higher precision than the fixed point arithmetic, then the floating point would "outperform" the fixed point, in the sense of closing in nearer to 5 before going astray.
[1] https://people.eecs.berkeley.edu/~wkahan/Math128/M128Bsoln09...
So we could just say that arithmetics with less precision is better, here, because it reaches the global optimum quicker? Whereas higher precision arithmetic is "lingers" in the "orbit" of 5 a little longer.
I suppose it might end up looking a bit funny in their books?
In the Netherlands we disliked the 1 and 2 euro-cent coins, so all shops round to the nearest 5 cent when you pay cash. By shopping 'smart' you could save yourself multiple cents every purchase (1, 2, 6, and 7 round down, 3, 4, 8, and 9 round up). E.g. if you buy one item for 99 cents, you pay 1 euro.. When you buy three of those items, you pay only 2.95, a 1.66666% discount. I've never heard of anyone doing this, because nobody cares :-)
Internally your bank is almost certainly doing daily, and across many accounts rounding errors do need to be controlled, even if the retail customers never see that side
Trading is actually done in “basis points” or “pips” which are 1/100 of a cent
Update: quick calc on my phone: log10(2 63) gives 14+4 places. A quick google gives an estimate of less than 100 trillion dollar on this planet, which is als 14 digits. So unless your needs are really extreme, int64 should serve you well.
Let R = the tax rate x 10000, in a jurisdiction where the tax rate is an integral multiple of 0.0001. Note that R is an integer.
Let A = the sale amount x 100, in a jurisdiction where prices are an integral multiple of 0.01. I.e., in the US, A is the sale amount in cents. Note that A is an integer.
Let T = the tax * 100, or in US terms, the tax in cents. Note that T is an integer.
If you can arrange to keep your amounts in cents and your rates in the x 10000 form (or whatever is appropriate for the jurisdiction), then you only need integers and things are simple:
def tax(A, R):
T = (A * R + 5000)//10000
return T
You probably have to go from cents to dollars somewhere, such as when informing the user of prices, taxes, and totals. I believe that integer/100, rounded to 2 places in all cases and printed in all of the above languages will be correct, but I kind of cheated and for display results I treated it as a string manipulation problem, not a number problem (which also takes care of making sure amounts less than $1 have a leading 0, and that multiples of 0.1 have a trailing zero) [1].If you don't have the amount and rate in the nice integer forms above, but rather have them in floating point such as you get from parsing a string like 12.34 (for a price of $12.34) or 0.086 (for a tax of 8.6%), here are three functions to return the tax in cents that might seem reasonable, and you might think are properly handling rounding:
def tax_f1(amt, rate):
tax = round(amt * rate,2)
return round(tax * 100)
def tax_f2(amt, rate):
return round(amt*rate*100)
def tax_f3(amt, rate):
return round(amt*rate*100+.5)
Alas, they are all flawed. input f1 f2 f3
------------- --- --- ---
1% of $21.50 21 22 22 ( 22 is right)
3% of $21.50 65 64 65 ( 65 is right)
6% of $21.50 129 129 130 (129 is right)
10% of $21.15 211 211 212 (212 is right)
It does work to convert from floating point to the x 100 and x 10000 integer form, and then use the integer function given earlier: def tax_f4(amt, rate):
amt = round(amt * 100)
rate = round(rate * 10000)
tax = (amt * rate + 5000)//10000
return tax
def tax_f5(amt, rate):
amt = int(amt * 100 + .5)
rate = int(rate * 10000 + .5)
tax = (amt * rate + 5000)//10000
return tax
Both of those are right in the test cases above, and I believe in all other cases (well, all other cases where everything is positive...). For Python I've done brute force testing of all combinations of amount = 0.01 to 25.00 in steps of 0.01 and rate = 0.0001 to 1.0000 in steps of 0.0001 to verify that.I've also done a brute force C test that involved sscanf(..., "%lf",...) of strings of the form "0.ddd...ddd" where the 'd' are decimal digits, and there are up to 9 of them. In all cases multiplying the resulting double by 10^k, where k is the number of digits after the decimal point and called round() on that gave the correct integer. Assuming that Python, PHP, etc., are using IEEE 754 when they do floating point, the results should be the same in all of those, which is why I believe that tax_f4 and tax_f5 should work for all cases, not just the ones I actually tested in the Python brute force test.
I did another C test, over the same range as the sscanf test, to verify that given an integer I, in the range [0, 10^k] for positive k up to 9, if you computer (double)I/10^k, then multiply that by 10^k and round(), you get back I.
My conclusions (assuming IEEE 754 or something that behaves similarly):
1. It is OK to store money values and tax rates in floating point, at least as long as you have 9 or fewer digits after the decimal point. Just avoid doing calculations in this form.
2. Converting from a floating point representation to an integer x 10^k representation by doing a floating point multiply by 10^k and a round to nearest integer works, at least as long as k <= 9.
3. sscanf '%lf', and I expect most reasonable string to float parsers, if applied to a floating point number of the form 0.ddd... with up to 9 digits after the decimal point, will work as expected, in the sense that they will give you a floating point number that when converted to integer x 10^k representation as described in #2 will give you the integer you expect and want.
4. I did not do any tests of floating point amounts that had large integer parts. With a large enough integer part, the places where I mention k <= 9 above might need to have that 9 lowered.
[1] e.g., in PHP:
function cents_to_dollars($cents)
{
$cents = strval($cents);
while (strlen($cents) < 3)
$cents = '0' . $cents;
return substr($cents,0,strlen($cents)-2) . '.' . substr($cents,-2);
}What solid here means? I find some repos that implement it or support floating point decimals.