<!-- canonical: https://efficientnewlanguage.org/eml-p/projects/P151-tax-brackets/ | updated: 2026-10-18 -->

# P151 Tax brackets

Income bands written from a sentence and read four ways - closed, half-open, open, left-open - the incomes on a band's end, the slab tax the reading decides and the marginal tax it cannot change.

EML-P project `projects/tax-brackets` in the EML language repo: 2 module(s), entry `main.eml`, terminal UI. There, `eml project run projects/tax-brackets` runs it and `eml project verify projects/tax-brackets` replays every session under CPython (two hash seeds) and in the interpreter; the site build replays every session in the interpreter again and publishes a session only if its screen matches.

Built on verified corpus cases: boundary-inclusive-or-not (https://efficientnewlanguage.org/cases/233-boundary-inclusive-or-not/).

## Sessions

### bad-input - interpreter: byte-equal to the golden

Input:

```text
0
x
2

2
12345678
2
1e5
2
-5
4

4
x
4
99999
3
5
```

Screen:

```text
== Tax brackets ==
Bands written from a sentence, read four ways, and the incomes that sit on their ends.

1) bands  2) one income  3) the incomes on file  4) add an income  5) quit
choice> 0
Pick a number from 1 to 5.

1) bands  2) one income  3) the incomes on file  4) add an income  5) quit
choice> x
Pick a number from 1 to 5.

1) bands  2) one income  3) the incomes on file  4) add an income  5) quit
choice> 2
income (0 to 9999999)> 
Cancelled.

1) bands  2) one income  3) the incomes on file  4) add an income  5) quit
choice> 2
income (0 to 9999999)> 12345678
Type a whole number from 0 to 9999999.

1) bands  2) one income  3) the incomes on file  4) add an income  5) quit
choice> 2
income (0 to 9999999)> 1e5
Type a whole number from 0 to 9999999.

1) bands  2) one income  3) the incomes on file  4) add an income  5) quit
choice> 2
income (0 to 9999999)> -5
Type a whole number from 0 to 9999999.

1) bands  2) one income  3) the incomes on file  4) add an income  5) quit
choice> 4
income (0 to 9999999)> 
Cancelled.

1) bands  2) one income  3) the incomes on file  4) add an income  5) quit
choice> 4
income (0 to 9999999)> x
Type a whole number from 0 to 9999999.

1) bands  2) one income  3) the incomes on file  4) add an income  5) quit
choice> 4
income (0 to 9999999)> 99999
Income 99999 added; 10 on file.

1) bands  2) one income  3) the incomes on file  4) add an income  5) quit
choice> 3
Over the 10 incomes on file:
  reading    in no band  in one  in two   slab tax in all
  closed              0       7       3         113900.00
  half-open           0      10       0         128900.00
  open                4       6       0         134900.00
  left-open           1       9       0         113900.00
The readings part only at 0, 10000, 40000, 100000: incomes on a band's end.
Every income in exactly one band: half-open.
Marginal tax in all: 76900.00, the same under every reading.

1) bands  2) one income  3) the incomes on file  4) add an income  5) quit
choice> 5
Bye.
```

### basic - interpreter: byte-equal to the golden

Input:

```text
1
2
10000
2
25000
2
100000
2
0
3
4
40000
3
5
```

Screen:

```text
== Tax brackets ==
Bands written from a sentence, read four ways, and the incomes that sit on their ends.

1) bands  2) one income  3) the incomes on file  4) add an income  5) quit
choice> 1
The sentence: 0-10000 at 0%, 10000-40000 at 10%, 40000-100000 at 20%, 100000 and above at 30%.
  band  closed            half-open         open              left-open
  1 0%  [0, 10000]        [0, 10000)        (0, 10000)        (0, 10000]
  2 10% [10000, 40000]    [10000, 40000)    (10000, 40000)    (10000, 40000]
  3 20% [40000, 100000]   [40000, 100000)   (40000, 100000)   (40000, 100000]
  4 30% [100000, and up)  [100000, and up)  (100000, and up)  (100000, and up)

1) bands  2) one income  3) the incomes on file  4) add an income  5) quit
choice> 2
income (0 to 9999999)> 10000
Income 10000:
  reading    bands that take it  slab tax: the whole income at the band's rate
  closed     1 and 2             0.00 - band 1, the first that takes it
  half-open  2                   1000.00
  open       none                3000.00 - no band takes it, and the else branch charges the top rate
  left-open  1                   0.00
Marginal tax, each slice at its own band's rate: 0.00 under every reading: a slice's width does not depend on which band owns its end.

1) bands  2) one income  3) the incomes on file  4) add an income  5) quit
choice> 2
income (0 to 9999999)> 25000
Income 25000:
  reading    bands that take it  slab tax: the whole income at the band's rate
  closed     2                   2500.00
  half-open  2                   2500.00
  open       2                   2500.00
  left-open  2                   2500.00
Marginal tax, each slice at its own band's rate: 1500.00 under every reading: a slice's width does not depend on which band owns its end.

1) bands  2) one income  3) the incomes on file  4) add an income  5) quit
choice> 2
income (0 to 9999999)> 100000
Income 100000:
  reading    bands that take it  slab tax: the whole income at the band's rate
  closed     3 and 4             20000.00 - band 3, the first that takes it
  half-open  4                   30000.00
  open       none                30000.00 - no band takes it, and the else branch charges the top rate
  left-open  3                   20000.00
Marginal tax, each slice at its own band's rate: 15000.00 under every reading: a slice's width does not depend on which band owns its end.

1) bands  2) one income  3) the incomes on file  4) add an income  5) quit
choice> 2
income (0 to 9999999)> 0
Income 0:
  reading    bands that take it  slab tax: the whole income at the band's rate
  closed     1                   0.00
  half-open  1                   0.00
  open       none                0.00 - no band takes it, and the else branch charges the top rate
  left-open  none                0.00 - no band takes it, and the else branch charges the top rate
Marginal tax, each slice at its own band's rate: 0.00 under every reading: a slice's width does not depend on which band owns its end.

1) bands  2) one income  3) the incomes on file  4) add an income  5) quit
choice> 3
Over the 9 incomes on file:
  reading    in no band  in one  in two   slab tax in all
  closed              0       6       3          93900.20
  half-open           0       9       0         108900.20
  open                4       5       0         114900.20
  left-open           1       8       0          93900.20
The readings part only at 0, 10000, 40000, 100000: incomes on a band's end.
Every income in exactly one band: half-open.
Marginal tax in all: 61900.20, the same under every reading.

1) bands  2) one income  3) the incomes on file  4) add an income  5) quit
choice> 4
income (0 to 9999999)> 40000
Income 40000 added; 10 on file.

1) bands  2) one income  3) the incomes on file  4) add an income  5) quit
choice> 3
Over the 10 incomes on file:
  reading    in no band  in one  in two   slab tax in all
  closed              0       6       4          97900.20
  half-open           0      10       0         116900.20
  open                5       5       0         126900.20
  left-open           1       9       0          97900.20
The readings part only at 0, 10000, 40000, 100000: incomes on a band's end.
Every income in exactly one band: half-open.
Marginal tax in all: 64900.20, the same under every reading.

1) bands  2) one income  3) the incomes on file  4) add an income  5) quit
choice> 5
Bye.
```

## Modules

### main.eml

```eml
# P151 tax brackets: the bands and their four readings, one income under
# each, the partition check over the incomes on file, and adding one.
import bands

def plain(s):
    0 => i
    len(s) => j
    while i < j and s[i] == " ":
        i + 1 => i
    while j > i and s[j - 1] == " ":
        j - 1 => j
    return s[i:j]

def pad(s, n):
    s => out
    while len(out) < n:
        out + " " => out
    return out

def lpad(s, n):
    s => out
    while len(out) < n:
        " " + out => out
    return out

def money(c):
    c % 100 => p
    if p < 10:
        return str(int((c - p) / 100)) + ".0" + str(p)
    return str(int((c - p) / 100)) + "." + str(p)

def interval(reading, b):
    if reading == "closed" or reading == "half-open":
        "[" => left
    else:
        "(" => left
    if b[1] < 0:
        return left + str(b[0]) + ", and up)"
    if reading == "closed" or reading == "left-open":
        "]" => right
    else:
        ")" => right
    return left + str(b[0]) + ", " + str(b[1]) + right

def show_bands():
    "The sentence: 0-10000 at 0%, 10000-40000 at 10%, 40000-100000 at 20%, 100000 and above at 30%." ^0
    ("  " + pad("band", 6) + pad("closed", 18) + pad("half-open", 18) + pad("open", 18) + "left-open") ^0
    1 => k
    for b in bands.bands():
        ("  " + pad(str(k) + " " + str(b[2]) + "%", 6) + pad(interval("closed", b), 18) + pad(interval("half-open", b), 18) + pad(interval("open", b), 18) + interval("left-open", b)) ^0
        k + 1 => k

def names(ks):
    if len(ks) == 0:
        return "none"
    "" => out
    for k in ks:
        if out == "":
            str(k) => out
        else:
            out + " and " + str(k) => out
    return out

def one_income():
    plain(input("income (0 to 9999999)> ")) => t
    if t == "":
        "Cancelled." ^0
        return
    if len(t) > 7:
        "Type a whole number from 0 to 9999999." ^0
        return
    for c in t:
        if c < "0" or c > "9":
            "Type a whole number from 0 to 9999999." ^0
            return
    int(t) => x
    show_income(x)

def show_income(x):
    ("Income " + str(x) + ":") ^0
    ("  " + pad("reading", 11) + pad("bands that take it", 20) + "slab tax: the whole income at the band's rate") ^0
    for r in bands.readings():
        bands.taking(r, x) => ks
        money(bands.slab(r, x)) => s
        if len(ks) == 0:
            s + " - no band takes it, and the else branch charges the top rate" => s
        elif len(ks) > 1:
            s + " - band " + str(ks[0]) + ", the first that takes it" => s
        ("  " + pad(r, 11) + pad(names(ks), 20) + s) ^0
    ("Marginal tax, each slice at its own band's rate: " + money(bands.marginal(x)) + " under every reading: a slice's width does not depend on which band owns its end.") ^0

def partition(xs):
    ("Over the " + str(len(xs)) + " incomes on file:") ^0
    ("  " + pad("reading", 11) + "in no band  in one  in two   slab tax in all") ^0
    "" => exact
    for r in bands.readings():
        0 => none
        0 => one
        0 => two
        0 => total
        for x in xs:
            len(bands.taking(r, x)) => n
            if n == 0:
                none + 1 => none
            elif n == 1:
                one + 1 => one
            else:
                two + 1 => two
            total + bands.slab(r, x) => total
        ("  " + pad(r, 11) + lpad(str(none), 10) + lpad(str(one), 8) + lpad(str(two), 8) + lpad(money(total), 18)) ^0
        if one == len(xs):
            if exact == "":
                r => exact
            else:
                exact + " and " + r => exact
    "" => edge
    [] => seen
    for x in xs:
        bands.taking("closed", x) => a
        bands.taking("open", x) => b
        False => dup
        for y in seen:
            if y == x:
                True => dup
        if (len(a) != 1 or len(b) != 1) and not dup:
            seen + [x] => seen
            if edge == "":
                str(x) => edge
            else:
                edge + ", " + str(x) => edge
    if edge != "":
        ("The readings part only at " + edge + ": incomes on a band's end.") ^0
    ("Every income in exactly one band: " + exact + ".") ^0
    0 => m
    for x in xs:
        m + bands.marginal(x) => m
    ("Marginal tax in all: " + money(m) + ", the same under every reading.") ^0

def add(xs):
    plain(input("income (0 to 9999999)> ")) => t
    if t == "":
        "Cancelled." ^0
        return xs
    if len(t) > 7:
        "Type a whole number from 0 to 9999999." ^0
        return xs
    for c in t:
        if c < "0" or c > "9":
            "Type a whole number from 0 to 9999999." ^0
            return xs
    int(t) => x
    ("Income " + str(x) + " added; " + str(len(xs) + 1) + " on file.") ^0
    return xs + [x]

bands.incomes() => xs

"== Tax brackets ==" ^0
"Bands written from a sentence, read four ways, and the incomes that sit on their ends." ^0
True => running
while running:
    "" ^0
    "1) bands  2) one income  3) the incomes on file  4) add an income  5) quit" ^0
    plain(input("choice> ")) => choice
    if choice == "1":
        show_bands()
    elif choice == "2":
        one_income()
    elif choice == "3":
        partition(xs)
    elif choice == "4":
        add(xs) => xs
    elif choice == "5":
        False => running
    else:
        "Pick a number from 1 to 5." ^0
"Bye." ^0
```

Python projection of main.eml:

```python
import bands

def plain(s):
    i = 0
    j = len(s)
    while i < j and s[i] == " ":
        i = i + 1
    while j > i and s[j - 1] == " ":
        j = j - 1
    return s[i:j]

def pad(s, n):
    out = s
    while len(out) < n:
        out = out + " "
    return out

def lpad(s, n):
    out = s
    while len(out) < n:
        out = " " + out
    return out

def money(c):
    p = c % 100
    if p < 10:
        return str(int((c - p) / 100)) + ".0" + str(p)
    return str(int((c - p) / 100)) + "." + str(p)

def interval(reading, b):
    if reading == "closed" or reading == "half-open":
        left = "["
    else:
        left = "("
    if b[1] < 0:
        return left + str(b[0]) + ", and up)"
    if reading == "closed" or reading == "left-open":
        right = "]"
    else:
        right = ")"
    return left + str(b[0]) + ", " + str(b[1]) + right

def show_bands():
    print("The sentence: 0-10000 at 0%, 10000-40000 at 10%, 40000-100000 at 20%, 100000 and above at 30%.")
    print("  " + pad("band", 6) + pad("closed", 18) + pad("half-open", 18) + pad("open", 18) + "left-open")
    k = 1
    for b in bands.bands():
        print("  " + pad(str(k) + " " + str(b[2]) + "%", 6) + pad(interval("closed", b), 18) + pad(interval("half-open", b), 18) + pad(interval("open", b), 18) + interval("left-open", b))
        k = k + 1

def names(ks):
    if len(ks) == 0:
        return "none"
    out = ""
    for k in ks:
        if out == "":
            out = str(k)
        else:
            out = out + " and " + str(k)
    return out

def one_income():
    t = plain(input("income (0 to 9999999)> "))
    if t == "":
        print("Cancelled.")
        return
    if len(t) > 7:
        print("Type a whole number from 0 to 9999999.")
        return
    for c in t:
        if c < "0" or c > "9":
            print("Type a whole number from 0 to 9999999.")
            return
    x = int(t)
    show_income(x)

def show_income(x):
    print("Income " + str(x) + ":")
    print("  " + pad("reading", 11) + pad("bands that take it", 20) + "slab tax: the whole income at the band's rate")
    for r in bands.readings():
        ks = bands.taking(r, x)
        s = money(bands.slab(r, x))
        if len(ks) == 0:
            s = s + " - no band takes it, and the else branch charges the top rate"
        elif len(ks) > 1:
            s = s + " - band " + str(ks[0]) + ", the first that takes it"
        print("  " + pad(r, 11) + pad(names(ks), 20) + s)
    print("Marginal tax, each slice at its own band's rate: " + money(bands.marginal(x)) + " under every reading: a slice's width does not depend on which band owns its end.")

def partition(xs):
    print("Over the " + str(len(xs)) + " incomes on file:")
    print("  " + pad("reading", 11) + "in no band  in one  in two   slab tax in all")
    exact = ""
    for r in bands.readings():
        none = 0
        one = 0
        two = 0
        total = 0
        for x in xs:
            n = len(bands.taking(r, x))
            if n == 0:
                none = none + 1
            elif n == 1:
                one = one + 1
            else:
                two = two + 1
            total = total + bands.slab(r, x)
        print("  " + pad(r, 11) + lpad(str(none), 10) + lpad(str(one), 8) + lpad(str(two), 8) + lpad(money(total), 18))
        if one == len(xs):
            if exact == "":
                exact = r
            else:
                exact = exact + " and " + r
    edge = ""
    seen = []
    for x in xs:
        a = bands.taking("closed", x)
        b = bands.taking("open", x)
        dup = False
        for y in seen:
            if y == x:
                dup = True
        if (len(a) != 1 or len(b) != 1) and not dup:
            seen = seen + [x]
            if edge == "":
                edge = str(x)
            else:
                edge = edge + ", " + str(x)
    if edge != "":
        print("The readings part only at " + edge + ": incomes on a band's end.")
    print("Every income in exactly one band: " + exact + ".")
    m = 0
    for x in xs:
        m = m + bands.marginal(x)
    print("Marginal tax in all: " + money(m) + ", the same under every reading.")

def add(xs):
    t = plain(input("income (0 to 9999999)> "))
    if t == "":
        print("Cancelled.")
        return xs
    if len(t) > 7:
        print("Type a whole number from 0 to 9999999.")
        return xs
    for c in t:
        if c < "0" or c > "9":
            print("Type a whole number from 0 to 9999999.")
            return xs
    x = int(t)
    print("Income " + str(x) + " added; " + str(len(xs) + 1) + " on file.")
    return xs + [x]

xs = bands.incomes()
print("== Tax brackets ==")
print("Bands written from a sentence, read four ways, and the incomes that sit on their ends.")
running = True
while running:
    print("")
    print("1) bands  2) one income  3) the incomes on file  4) add an income  5) quit")
    choice = plain(input("choice> "))
    if choice == "1":
        show_bands()
    elif choice == "2":
        one_income()
    elif choice == "3":
        partition(xs)
    elif choice == "4":
        xs = add(xs)
    elif choice == "5":
        running = False
    else:
        print("Pick a number from 1 to 5.")
print("Bye.")
```

### bands.eml

```eml
# P151 tax brackets - income bands written from a sentence, "0-10000,
# 10000-40000, 40000-100000, 100000 and above", read four ways. As in the
# corpus case boundary-inclusive-or-not, "between" is four questions -
# closed, half-open, open, left-open - and they differ only at the end
# points, which is why the difference survives every test in the middle.
# Consecutive bands read closed take a boundary twice and read open take it
# not at all; only half-open makes the bands a partition. A band is [low,
# high, rate in percent]; the last has no high.

def bands():
    return [[0, 10000, 0], [10000, 40000, 10], [40000, 100000, 20], [100000, 0 - 1, 30]]

def readings():
    return ["closed", "half-open", "open", "left-open"]

def incomes():
    return [0, 8500, 10000, 25000, 40000, 40001, 72000, 100000, 150000]

def takes(reading, b, x):
    # does band b take income x under the reading
    if reading == "closed" or reading == "half-open":
        x >= b[0] => low_ok
    else:
        x > b[0] => low_ok
    if b[1] < 0:
        return low_ok
    if reading == "closed" or reading == "left-open":
        x <= b[1] => high_ok
    else:
        x < b[1] => high_ok
    return low_ok and high_ok

def taking(reading, x):
    # the numbers of the bands that take x, from 1
    [] => out
    1 => k
    for b in bands():
        if takes(reading, b, x):
            out + [k] => out
        k + 1 => k
    return out

def slab(reading, x):
    # the whole income at the rate of the first band that takes it, in
    # cents; with no band the else branch charges the top rate
    for b in bands():
        if takes(reading, b, x):
            return x * b[2]
    return x * bands()[len(bands()) - 1][2]

def marginal(x):
    # each slice of the income at its own band's rate, in cents
    0 => t
    for b in bands():
        x => top
        if b[1] >= 0 and b[1] < top:
            b[1] => top
        if top > b[0]:
            t + (top - b[0]) * b[2] => t
    return t
```

Python projection of bands.eml:

```python
def bands():
    return [[0, 10000, 0], [10000, 40000, 10], [40000, 100000, 20], [100000, 0 - 1, 30]]

def readings():
    return ["closed", "half-open", "open", "left-open"]

def incomes():
    return [0, 8500, 10000, 25000, 40000, 40001, 72000, 100000, 150000]

def takes(reading, b, x):
    if reading == "closed" or reading == "half-open":
        low_ok = x >= b[0]
    else:
        low_ok = x > b[0]
    if b[1] < 0:
        return low_ok
    if reading == "closed" or reading == "left-open":
        high_ok = x <= b[1]
    else:
        high_ok = x < b[1]
    return low_ok and high_ok

def taking(reading, x):
    out = []
    k = 1
    for b in bands():
        if takes(reading, b, x):
            out = out + [k]
        k = k + 1
    return out

def slab(reading, x):
    for b in bands():
        if takes(reading, b, x):
            return x * b[2]
    return x * bands()[len(bands()) - 1][2]

def marginal(x):
    t = 0
    for b in bands():
        top = x
        if b[1] >= 0 and b[1] < top:
            top = b[1]
        if top > b[0]:
            t = t + (top - b[0]) * b[2]
    return t
```

## README

# P151 - Tax brackets

Income bands written from a sentence - 0-10000, 10000-40000, 40000-100000,
100000 and above - read four ways, and the incomes that sit on a band's
end.

- `main.eml` - the menu, the bands under each reading, one income under
  each, the partition check over the incomes on file, and adding one
- `bands.eml` - the bands, the readings, the incomes on file, whether a
  band takes an income, the bands that take it, the slab tax and the
  marginal tax

How each part works:

- As in the corpus case `boundary-inclusive-or-not`, "between" is four
  questions - closed, half-open, open, left-open - and they part only at
  the ends: 0, 10000, 40000 and 100000. Every income in the middle lands in
  the same band under all four, which is why the difference survives every
  test there.
- Consecutive bands read closed take the three inner ends twice; read open
  they take the four ends not at all; left-open misses only 0. Half-open
  alone puts every income in exactly one band: the bands are a partition.
- The slab tax charges the whole income at its band's rate, so the reading
  decides it at an end: 10000 pays 0.00, 1000.00 or 3000.00 - read open no
  band takes it, and the else branch charges the top rate.
- The marginal tax charges each slice at its own band's rate. It is the
  same under every reading, a slice's width not depending on which band
  owns its end, and it does not jump.

What is checked: menu choices 1 to 5; an income from 0 to 9999999. An
empty answer cancels.

Sessions: `sessions/basic.in` gives:
- the bands; incomes 10000, 25000, 100000 and 0;
- the incomes on file; 40000 added, and the incomes on file again.

`sessions/bad-input.in` gives:
- menu choices 0 and x;
- incomes: a cancel, 12345678, 1e5 and -5;
- adding: a cancel and x, then 99999; the incomes on file.

Built on the verified corpus case `boundary-inclusive-or-not` (four
readings of a range, consecutive buckets that double-count or drop a
boundary, and a partition check by counting).
