## Really? Tax Tables?

This isn’t a topic most people would find interesting, until they are subjected to implementing graduated or step tables. And I’ve found that many people aren’t aware of how a *progressive* tax model works even when seeing the tables.

I bring this up because when implementing a tax calculator using those tax tables if the tables are followed “as written” from most sources the code can generate wrong values and can even claim there are no brackets!

Credit to the Internal Revenue Service — its tables are defined properly!

How can so many be so wrong and what’s the actual error? And is the error in more than tax tables? Well…

## 2022 US Federal Income Tax Brackets

To show the point, here’s an example of a site that gets it wrong (it’s simply the first site I happened to hit that listed the table when searching online for “2022 tax brackets us federal income”): Bank Rate Tax Brackets and scroll down to 2022.

The rates are fine, that’s not the error. Read the column for Single and it shows the following:

10% | $0 | $10,275 |

12% | $10,276 | $41,775 |

22% | $41,776 | $89,075 |

24% | $89,076 | $170,050 |

32% | $170,051 | $215,950 |

35% | $215,951 | $539,900 |

37% | $539,901 | MORE |

Looks reasonable. That is easy to code, could be tossed into Excel and using VLOOKUP would work great.

Except it’s obviously got an error. Where?

**What is the bracket for $10,275.50?**

It can’t be 10%, because it’s above the high.

It can’t be 12%, because it’s below the low.

## Everyone* Knows* It’s 12%

A human might…but if a software developer were asked to implement code and *obey that table*, they would get it wrong unless they happened to test for fencepost errors and noticed it. Importantly, I’ve worked with people on projects involving graduated tables and they didn’t notice it — until (in some cases) I had them feed in the “value between the high and low” and get the error in their code.

The cause of this error is the *understandable* desire to show a clean table with lower and upper bounds. If currency did not have fractional parts this wouldn’t be an error. Unfortunately, currencies *absolutely* have fractional parts!

Feel free to search for US income tax rates on other sites. There are some that get it right, but most don’t. One that does (thankfully) is the Internal Revenue’s own site and instructions. They list the table as Worksheet 1-4 in their Publication 505 (2022), Tax Withholding and Estimated Tax.

They properly list their brackets as having a lower bound that *matches the prior upper bound*.

## The Proper Way To Represent Graduated Table Bounds

Since a graduated or step table is meant to be used with a *continuous* lookup amount (meaning it has both integral and fractional portions, continuous in the sense of the real number system) mathematically the brackets are defined as a range as follows:

**[lower-bound, upper-bound)**

That is not a typo … the opening square bracket means “inclusive of” and the closing parenthesis means “up to *but not including*” and is “exclusive of.” The bracket bounds are a half-open interval.

To show this in a table, such that an obedient developer following a requirement **could** use, the table should be shown like this:

10% | $0 | $10,275 |

12% | $10,275 | $41,775 |

22% | $41,775 | $89,075 |

24% | $89,075 | $170,050 |

32% | $170,050 | $215,950 |

35% | $215,950 | $539,900 |

37% | $539,900 | MORE |

Here, because of the interval being properly represented, there *are no gaps* and no fencepost error.

## Is It Just the United States?

Actually, it’s not. Here’s a link to the Republic of South Africa Revenue Service (SARS is their IRS) and they provide a table for the 2023 Tax year at the Rates of Tax for Individuals page (only showing the first three rows, as it’s enough to make the point):

1 | 226 000 | 18% |

226 001 | 353 100 | 26% |

353 101 | 488 700 | 31% |

The use of 1, 226 001, and 353 101 have the same error as the +1 in the Bankrate table had.

What’s interesting, their instructions with each bracket *are correct* and contradict the lower bound number! Here’s an example of their actual rate for “226 001 – 353 100”:

**40 680 + 26% of taxable income above 226 000**

The “40 680” is 226000 * 18% (it’s the portion of the first bracket) and is discussed in the next section.

Notice the text “+26% of taxable income **above** 226 000″ (bolding mine). That is *the proper interval rule* for [226 000 , 353 100) as expected. Their text is correct, starting at the upper of the prior row. Any developer who *ignores the lower bound* and works only with upper bounds and percentages would follow that guidance would get the right behavior.

But, what’s that 40680 number for?

Actually, if the IRS worksheet 1-4 is reviewed, there’s a column (d) called subtraction amount that is non-zero for all the rows after the first. It’s exactly the same purpose as that 40680.

## Interpreting and Using Graduated or Step Tables

This is the other part of the implementation issue when coding a table such as this.

The brackets are traversed in order, each providing a portion of the final amount.

This is crucial to the idea of *how progressive taxes are calculated* and it’s done wrong so often that IRS and SARS both *do the math* trying to help people understand it.

Using the US tables (because they’re listed in full above) let’s calculate tax on a taxable amount of $50,000. (NOTE: have the full Worksheet 1-4 link opened in another window to *see* how this is working.)

Searching the intervals, I find that $50,000 falls in the third row, between [$41775 , $89075) with a rate of 22%.

If I follow their calculation rules, I get this equation: 50000 * .22 = 11000.

Then subtract $4383 giving $6,617.

That works, because they “did the math” mostly in advance for us. What math did they do?

To reduce the data entry, I represent that table with these two columns:

$10,275 | 10% |

$41,775 | 12% |

$89,075 | 22% |

$170,050 | 24% |

$215,950 | 32% |

$539,900 | 35% |

(blank) | 37% |

Now, that table is a lot less data entry and a lot easier to understand (and can be used immediately with the upper bounds of ANY of the various tax tables, because their errors are always in the lower bounds).

How to use it?

Using the $50,000 example:

The portion up to $10,275 is taxed at 10% which is $1,027.50

The portion ABOVE $10,275 through $41,775 is (41775-10275) = $31,500 @ 12% = $3,780

The portion ABOVE $41,775 is (50000-41775) = $8225 @ 22% = $1,809.50

TOTAL TAX = $1,027.50 + $3,780 + $1,809.50 = $6,617

Notice that is far less than calculating $50,000 * 22% = $11,000

Only the portion in the bracket is charged the rate of the bracket. This is why taxes increase in rate as the income goes higher. Some people think if they get a tiny raise that boosts them to the next bracket they’ll owe that rate even against what they already paid. They don’t! Raises are great! Only the *portion in the higher bracket* is at the higher rate.

Now, the IRS table did *not make you do all the intermediate steps*. How did they get their value in (d) Subtraction Amount?

They got it the same way that the SARS table got the addition amount!!

## Sparing the Human the Intermediate Calculations

The lower tax brackets (the brackets for amounts less than the amount being searched) are *constants*. Only the current bracket subtracts the amount given from the prior upper bound. For this reason, the max tax possible per all brackets (except the highest, which is unbounded) *can be calculated in advance*.

If that calculation is done, then tax owed can be accumulated by simply adding the max tax per lower brackets and then calculating just the (rate * (amount searched – prior upper bound)). That’s why the SARS table reports it as an addition.

The IRS does it via subtraction, but it’s the same logic. For instance, the subtraction amount at $50,000 was $4,383. How did they get it?

To recap: $50,000 * .22 = $11,000, subtraction amount was $4,383.

What they did was walk EACH bracket except the last and do:

Range = Upper – Lower

Bracket Portion = Range * Rate @ bracket

Total Portion for Bracket = Total Portion for Last Bracket + Bracket Portion (accumulation)

Transitory Total = Upper * Rate @ bracket (note, NOT range, but the MAX for this bracket)

Subtraction Amount = Transitory Total – Total Portion for Bracket

It works for the last bracket too, but since the last bracket has no upper bound, it’s JUST treating the UPPER as the prior UPPER, which results in the accumulation from the prior coming through. The math works if you do it (a spreadsheet helps here).

## Why Bother When the Tables are Available?

Why not just create the tables with the addition or subtraction as appropriate, why go through the effort of working the numbers to find out how the tables work?

The challenge for any multinational is that the tables for each country are setup with different assumptions, but the *algorithm for processing graduated or step tables* is exactly the same in all cases. Whether addition or subtraction, the approach of accumulating as iterating always works.

If using the given raw tables (complete with lower and upper bounds and subtraction or addition amounts) were mandated, there would need to be per-country *implementation code* to handle that country’s logic.

By working with the fundamentals, the only data needed to represent the table is the upper bounds (with ONE that is “the rest” or “above” or “blank” etc.) and the rate.

On a side note, no developer should blindly follow an algorithm without understanding it. The algorithmic meaning of graduated tables is clearly *not understood well* or so many of the websites that show lower and upper bounds wouldn’t *get the bounds wrong* which would cascade through errors in application!

## Minor Last Point

The tax calculated is *generally not what is owed*.

In South Africa, for instance, a rebate is then subtracted (based on age).

In the US, there are “standard deductions” the serve the same purpose as the SA rebate.

There’s more to calculating tax than a table lookup! In this sense, every country has *very different* steps and some countries can be much harder or easier than others.

## Take Away

The obvious and easy is often wrong. Something as simple as lower and upper bounded lookup tables are typically presented incorrectly by even experienced authorities.

Always take the time to understand not just the intention of whatever needs to be implemented, but how it’s presented and used. Dig in, make sure that there are no numbers which are “confusing.”

Amateurs can get away with blind implementation — until they can’t. Don’t choose to be ignorant!

Remember: seek first to understand, *then to be understood*.

Not just a Habit for Highly Effective People (thank you, Covey!) but … a critical point for developing software that works (you want the *computer* to understand).

Keep the Light!