Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80941b6f03 | ||
|
|
a02e5cb109 | ||
|
|
a9f3c0b255 | ||
|
|
2e03c0a440 | ||
|
|
6b9810b075 | ||
|
|
b11e3da453 | ||
|
|
1c66773ff4 | ||
|
|
e1bdb6c703 | ||
|
|
777a3b56e2 | ||
|
|
d5286c3600 | ||
|
|
f83d1bd6f8 | ||
|
|
6c0d675c5d |
@@ -7,6 +7,28 @@ folgen [Semantic Versioning](https://semver.org/lang/de/). Die Version steht in
|
||||
|
||||
## [Unveröffentlicht]
|
||||
|
||||
## [0.7.0] - 2026-10-12
|
||||
|
||||
### Hinzugefügt
|
||||
- `column -d Spalte ...` (und `project -d`) löscht Spalten: alle anderen bleiben in ihrer Reihenfolge. Fehler bei unbekannter Spalte oder wenn alle gelöscht würden. Dazu Abschnitt in `man/column.man`, Beispiel in `testall.cmds` und Tests.
|
||||
- Kantonstarife 2026 für `tax`: `demo/taxCH_AI2026`, `taxCH_AR2026`, `taxCH_SG2026`, `taxCH_GR2026` (Präfix `taxCH_` für die Schweiz; einfache Staatssteuer für Alleinstehende, vor Steuerfuss, Gemeinde und Kirche), erzeugt mit `tools/gen_tax_tables.rb`. Mit Tests, Beispiel in `testall.cmds` und Erklärung zum Steuerfuss im README.
|
||||
|
||||
### Behoben
|
||||
- `rdb man dashline` (und `headline`, `backup`, `dbase`, `accounting_ter`) zeigte als Überschrift eine Makrozeile (`dashline -- .PP`). Themen-Seiten ohne Beschreibung nehmen jetzt den Titel aus der `.TH`-Zeile.
|
||||
- `splittable -0` und mehr als 676 Stücke endeten mit einem Ruby-Abbruch (im zweiten Fall nach 676 geschriebenen Dateien). Jetzt eine Fehlermeldung, und es wird vorher geprüft, damit nichts halb geschrieben wird.
|
||||
|
||||
## [0.6.0] - 2026-10-11
|
||||
|
||||
### Hinzugefügt
|
||||
- Steuertabellen 2026 für `tax`: `demo/taxDE2026`, `taxAT2026`, `taxCH2026` (nur Bundessteuer), `taxLI2026` (nur Landessteuer), erzeugt mit `tools/gen_tax_tables.rb`, mit Tests.
|
||||
|
||||
### Geändert
|
||||
- `record`: 0 und 1 sind die erste Zeile, negative Zahlen zählen vom Ende (`-1` letzte, `-2` vorletzte ...). Außerhalb der Tabelle kommt eine leere Tabelle, Text und Dezimalzahlen sind ein Fehler. (Vorher lieferten 0, -1 und Text ungewollt Zeilen vom Ende.) `testall.cmds` und `man/record.man` zeigen es.
|
||||
- README und `man/row.man` erklären, warum `select` in bash/zsh/ksh einen Syntaxfehler gibt und wie man es trotzdem aufruft.
|
||||
|
||||
### Behoben
|
||||
- `rdb man` zeigte `\\select` in den Man-Pages als `\elect`: ein doppelter Backslash wird jetzt zuerst geschützt.
|
||||
|
||||
## [0.5.0] - 2026-10-11
|
||||
|
||||
### Hinzugefügt
|
||||
|
||||
@@ -40,6 +40,8 @@ Dasselbe gilt für **Listen** (ein Datensatz als `Spalte<Tab>Wert`-Zeilen, die
|
||||
Datei beginnt mit einer Leerzeile). `tabletolist` und `listtotable` wandeln um,
|
||||
die meisten Befehle lesen beides.
|
||||
|
||||
`column -d Spalte ...` löscht Spalten (alle anderen bleiben), `column Spalte ...` wählt sie aus.
|
||||
|
||||
`row`, `compute` und `validate` übergeben ihren Ausdruck an awk, nachdem sie
|
||||
Spaltennamen durch `$N` ersetzt haben. Es gilt also awk-Syntax.
|
||||
|
||||
@@ -117,6 +119,48 @@ pick -m -k 'down space down enter' < inventory Tasten aus einem String (Skrip
|
||||
Pfeile, Bild auf/ab, Pos1/Ende scrollen; `/` filtert (Enter behält den Filter, Esc löscht ihn); Enter wählt;
|
||||
Esc bricht ab (Status 1, keine Ausgabe). Ohne Markierung gilt die Zeile unter dem Cursor.
|
||||
|
||||
## Steuertabellen 2026 für `tax`
|
||||
|
||||
`demo/` enthält vier Tarife für das Steuerjahr 2026 im Format von `tax` (income, tax, percent), jeweils für
|
||||
Alleinstehende (Grundtarif). Erzeugt mit `ruby tools/gen_tax_tables.rb`.
|
||||
|
||||
| Datei | Land | Quelle | Genauigkeit |
|
||||
|---|---|---|---|
|
||||
| `taxDE2026` | Deutschland, Einkommensteuer | § 32a EStG (Grundfreibetrag 12 348 €) | Kurvenstücke durch Geraden genähert, Abweichung unter 0,15 € (die Formel rundet ab, `tax` auf) |
|
||||
| `taxAT2026` | Österreich, Einkommensteuer | § 33 EStG (0 % bis 13 539 €, dann 20/30/40/48/50/55 %) | exakt, vor Absetzbeträgen |
|
||||
| `taxCH2026` | Schweiz, **nur direkte Bundessteuer** | Art. 36 DBG, ESTV-Rundschreiben 2-215-D-2025 | Stufen exakt, das Gesetz rechnet in 100-Franken-Schritten |
|
||||
| `taxLI2026` | Liechtenstein, **nur Landessteuer** | Art. 19 SteG (LR 640.0, Fassung 01.07.2026) | exakt |
|
||||
| `taxCH_AI2026` | Kanton Appenzell I.Rh., einfache Staatssteuer | Art. 38 StG (ESTV-Kantonsblatt) | exakt |
|
||||
| `taxCH_AR2026` | Kanton Appenzell A.Rh., einfache Staatssteuer (Alleinstehendentarif) | Art. 39 StG, Beträge ab 2024 | exakt |
|
||||
| `taxCH_SG2026` | Kanton St. Gallen, einfache Staatssteuer | Art. 50 StG, Beträge ab 2026 | exakt |
|
||||
| `taxCH_GR2026` | Kanton Graubünden, einfache Kantonssteuer | Art. 39 StG, indexierte Beträge ab 2025 | exakt |
|
||||
|
||||
```
|
||||
tax 60000 < demo/taxDE2026 14233
|
||||
tax 60000 < demo/taxAT2026 15447
|
||||
tax 60000 < demo/taxCH2026 671
|
||||
tax 60000 < demo/taxLI2026 1396 (mal 1 + Gemeindezuschlag, 150 % bis 250 %)
|
||||
```
|
||||
|
||||
Der Betrag ist die Steuer auf das **zu versteuernde** Einkommen. Abzüge, Sozialversicherung, Absetzbeträge,
|
||||
Kirchensteuer und Soli sind nicht enthalten. Für die Schweiz kommen Kanton und Gemeinde dazu (sie sind meist
|
||||
höher als der Bund), für Liechtenstein der Gemeindezuschlag: gesamt = Landessteuer × (1 + Zuschlag/100).
|
||||
Ehepaar- und Alleinerziehendentarife fehlen. Das ist keine Steuerberatung.
|
||||
|
||||
**Kantone (AI, AR, SG, GR):** Die Dateien tragen den Präfix `taxCH_` (Schweiz) vor dem Kantonskürzel; `taxCH2026` ist die Bundessteuer. Die Tabellen geben die *einfache Steuer* wieder. Sie wird mit dem Steuerfuss multipliziert,
|
||||
und Gemeinde und Kirche kommen dazu. Der Kanton setzt den Steuerfuss pro Jahr fest:
|
||||
|
||||
| Kanton | Steuerfuss Kanton 2026 | Quelle |
|
||||
|---|---|---|
|
||||
| AI | 96 % | Kantonsblatt AI (Grossratsbeschluss 1.12.2025) |
|
||||
| SG | 105 % | Budgetbeschluss des Kantonsrats (Websuche, nicht im Kantonsblatt) |
|
||||
| GR | 92 % | Grosser Rat, Budget 2026 (Websuche) |
|
||||
| AR | nicht ermittelt | AR rechnet in Steuereinheiten, die Zahl für 2026 habe ich nicht gefunden |
|
||||
|
||||
Beispiel AI bei 60 000: `tax 60000 < demo/taxCH_AI2026` = 3770, mal 0,96 = 3619 (Kantonssteuer, ohne Bezirk, Gemeinde und Kirche).
|
||||
Oberhalb einer Grenze gilt bei allen vier Kantonen ein fester Satz auf das *ganze* Einkommen (AI ab 200 000 mit 8 %,
|
||||
AR ab 260 800 mit 2,6 %, SG ab 264 500 mit 8,5 %, GR ab 780 440 mit 11 %); in der Tabelle ist das die letzte Zeile.
|
||||
|
||||
## Befehle
|
||||
|
||||
| Gruppe | Befehle |
|
||||
@@ -175,6 +219,10 @@ er nicht mehr passt (außer mit `-a`). Beide halten die Indizes aktuell.
|
||||
`row`, `compute`, `column`, `sorttable`, `jointable`, `rename`, `validate` und
|
||||
`rdb` haben dazu den Abschnitt „Case of Column Names“; er ist die einzige
|
||||
Ergänzung gegenüber den Original-Seiten.
|
||||
- `select` ist in bash, zsh und ksh ein reserviertes Wort. `select 'Cost > 5' < inventory` endet dort
|
||||
mit `syntax error near unexpected token '<'`, weil die Shell das Schlüsselwort vor dem `PATH` auswertet.
|
||||
Es gibt dasselbe unter dem Namen `row`. Außerdem gehen `\select ...`, `command select ...` und
|
||||
`rdb select ...`.
|
||||
- `jointable` braucht keine vorsortierten Tabellen.
|
||||
- `ve` ist ein zeilenorientierter Editor, kein Bildschirmeditor.
|
||||
- `soundex` nutzt den Standard-Algorithmus. Die Beispielwerte im Handbuch
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
income tax percent
|
||||
------ --- -------
|
||||
0 0.00 0
|
||||
13539 0.00 20
|
||||
21992 1690.60 30
|
||||
36458 6030.40 40
|
||||
70365 19593.20 48
|
||||
104859 36150.32 50
|
||||
1000000 483720.82 55
|
||||
@@ -0,0 +1,14 @@
|
||||
income tax percent
|
||||
------ --- -------
|
||||
0 0.00 0
|
||||
15200 0.00 0.77
|
||||
33200 138.60 0.88
|
||||
43500 229.20 2.64
|
||||
58000 612.00 2.97
|
||||
76200 1152.50 5.94
|
||||
82100 1502.95 6.6
|
||||
108900 3271.75 8.8
|
||||
141500 6140.55 11
|
||||
185100 10936.55 13.2
|
||||
793900 91298.15 11.85
|
||||
794000 91310.00 11.5
|
||||
@@ -0,0 +1,16 @@
|
||||
income tax percent
|
||||
------ --- -------
|
||||
0 0.00 0
|
||||
3000 0.00 1
|
||||
6000 30.00 2
|
||||
9000 90.00 3
|
||||
12000 180.00 4
|
||||
15000 300.00 5
|
||||
18000 450.00 6
|
||||
22000 690.00 7
|
||||
26000 970.00 7.5
|
||||
30000 1270.00 8
|
||||
40000 2070.00 8.5
|
||||
74000 4960.00 9
|
||||
140000 10900.00 8.5
|
||||
200000 16000.00 8
|
||||
@@ -0,0 +1,14 @@
|
||||
income tax percent
|
||||
------ --- -------
|
||||
0 0.00 0
|
||||
8300 0.00 0.6
|
||||
9900 9.60 1
|
||||
11500 25.60 1.5
|
||||
15700 88.60 1.8
|
||||
27200 295.60 2.2
|
||||
41800 616.80 2.4
|
||||
54300 916.80 2.6
|
||||
74100 1431.60 2.7
|
||||
88700 1825.80 2.8
|
||||
125200 2847.80 2.9
|
||||
260800 6780.80 2.6
|
||||
@@ -0,0 +1,21 @@
|
||||
income tax percent
|
||||
------ --- -------
|
||||
0 0.00 0
|
||||
16895 0.00 2.5
|
||||
17985 27.25 4
|
||||
19075 70.85 5
|
||||
20165 125.35 6
|
||||
21255 190.75 6.5
|
||||
22345 261.60 7
|
||||
24525 414.20 8
|
||||
31065 937.40 8.5
|
||||
35425 1308.00 9
|
||||
39785 1700.40 9.5
|
||||
44145 2114.60 10.3
|
||||
65945 4360.00 10.6
|
||||
87745 6670.80 10.7
|
||||
109545 9003.40 11.2
|
||||
218545 21211.40 11.3
|
||||
327545 33528.40 11.4
|
||||
436545 45954.40 11.6
|
||||
780440 85848.40 11
|
||||
@@ -0,0 +1,9 @@
|
||||
income tax percent
|
||||
------ --- -------
|
||||
0 0.00 0
|
||||
11600 0.00 4
|
||||
15800 168.00 6
|
||||
33800 1248.00 8
|
||||
60300 3368.00 9.2
|
||||
98400 6873.20 9.4
|
||||
264500 22482.50 8.5
|
||||
+235
@@ -0,0 +1,235 @@
|
||||
income tax percent
|
||||
------ --- -------
|
||||
0 0.00 0
|
||||
12348 0.00 14.228628
|
||||
12598 35.57 14.685882
|
||||
12848 72.29 15.143138
|
||||
13098 110.14 15.600392
|
||||
13348 149.15 16.057648
|
||||
13598 189.29 16.514902
|
||||
13848 230.58 16.972158
|
||||
14098 273.01 17.429413
|
||||
14348 316.58 17.886668
|
||||
14598 361.30 18.343922
|
||||
14848 407.16 18.801178
|
||||
15098 454.16 19.258432
|
||||
15348 502.31 19.715688
|
||||
15598 551.60 20.172942
|
||||
15848 602.03 20.630198
|
||||
16098 653.60 21.087453
|
||||
16348 706.32 21.544707
|
||||
16598 760.18 22.001963
|
||||
16848 815.19 22.459217
|
||||
17098 871.34 22.916473
|
||||
17348 928.63 23.373727
|
||||
17598 987.06 23.786172
|
||||
17799 1034.87 24.012466
|
||||
18049 1094.90 24.099825
|
||||
18299 1155.15 24.186375
|
||||
18549 1215.62 24.272925
|
||||
18799 1276.30 24.359475
|
||||
19049 1337.20 24.446025
|
||||
19299 1398.31 24.532575
|
||||
19549 1459.65 24.619125
|
||||
19799 1521.19 24.705675
|
||||
20049 1582.96 24.792225
|
||||
20299 1644.94 24.878775
|
||||
20549 1707.14 24.965325
|
||||
20799 1769.55 25.051875
|
||||
21049 1832.18 25.138425
|
||||
21299 1895.02 25.224975
|
||||
21549 1958.09 25.311525
|
||||
21799 2021.37 25.398075
|
||||
22049 2084.86 25.484625
|
||||
22299 2148.57 25.571175
|
||||
22549 2212.50 25.657725
|
||||
22799 2276.64 25.744275
|
||||
23049 2341.01 25.830825
|
||||
23299 2405.58 25.917375
|
||||
23549 2470.38 26.003925
|
||||
23799 2535.39 26.090475
|
||||
24049 2600.61 26.177025
|
||||
24299 2666.05 26.263575
|
||||
24549 2731.71 26.350125
|
||||
24799 2797.59 26.436675
|
||||
25049 2863.68 26.523225
|
||||
25299 2929.99 26.609775
|
||||
25549 2996.51 26.696325
|
||||
25799 3063.25 26.782875
|
||||
26049 3130.21 26.869425
|
||||
26299 3197.38 26.955975
|
||||
26549 3264.77 27.042525
|
||||
26799 3332.38 27.129075
|
||||
27049 3400.20 27.215625
|
||||
27299 3468.24 27.302175
|
||||
27549 3536.50 27.388725
|
||||
27799 3604.97 27.475275
|
||||
28049 3673.66 27.561825
|
||||
28299 3742.56 27.648375
|
||||
28549 3811.68 27.734925
|
||||
28799 3881.02 27.821475
|
||||
29049 3950.57 27.908025
|
||||
29299 4020.34 27.994575
|
||||
29549 4090.33 28.081125
|
||||
29799 4160.53 28.167675
|
||||
30049 4230.95 28.254225
|
||||
30299 4301.59 28.340775
|
||||
30549 4372.44 28.427325
|
||||
30799 4443.51 28.513875
|
||||
31049 4514.79 28.600425
|
||||
31299 4586.29 28.686975
|
||||
31549 4658.01 28.773525
|
||||
31799 4729.95 28.860075
|
||||
32049 4802.10 28.946625
|
||||
32299 4874.46 29.033175
|
||||
32549 4947.05 29.119725
|
||||
32799 5019.84 29.206275
|
||||
33049 5092.86 29.292825
|
||||
33299 5166.09 29.379375
|
||||
33549 5239.54 29.465925
|
||||
33799 5313.21 29.552475
|
||||
34049 5387.09 29.639025
|
||||
34299 5461.18 29.725575
|
||||
34549 5535.50 29.812125
|
||||
34799 5610.03 29.898675
|
||||
35049 5684.78 29.985225
|
||||
35299 5759.74 30.071775
|
||||
35549 5834.92 30.158325
|
||||
35799 5910.31 30.244875
|
||||
36049 5985.93 30.331425
|
||||
36299 6061.75 30.417975
|
||||
36549 6137.80 30.504525
|
||||
36799 6214.06 30.591075
|
||||
37049 6290.54 30.677625
|
||||
37299 6367.23 30.764175
|
||||
37549 6444.14 30.850725
|
||||
37799 6521.27 30.937275
|
||||
38049 6598.61 31.023825
|
||||
38299 6676.17 31.110375
|
||||
38549 6753.95 31.196925
|
||||
38799 6831.94 31.283475
|
||||
39049 6910.15 31.370025
|
||||
39299 6988.57 31.456575
|
||||
39549 7067.22 31.543125
|
||||
39799 7146.07 31.629675
|
||||
40049 7225.15 31.716225
|
||||
40299 7304.44 31.802775
|
||||
40549 7383.95 31.889325
|
||||
40799 7463.67 31.975875
|
||||
41049 7543.61 32.062425
|
||||
41299 7623.76 32.148975
|
||||
41549 7704.14 32.235525
|
||||
41799 7784.73 32.322075
|
||||
42049 7865.53 32.408625
|
||||
42299 7946.55 32.495175
|
||||
42549 8027.79 32.581725
|
||||
42799 8109.24 32.668275
|
||||
43049 8190.92 32.754825
|
||||
43299 8272.80 32.841375
|
||||
43549 8354.91 32.927925
|
||||
43799 8437.23 33.014475
|
||||
44049 8519.76 33.101025
|
||||
44299 8602.51 33.187575
|
||||
44549 8685.48 33.274125
|
||||
44799 8768.67 33.360675
|
||||
45049 8852.07 33.447225
|
||||
45299 8935.69 33.533775
|
||||
45549 9019.52 33.620325
|
||||
45799 9103.57 33.706875
|
||||
46049 9187.84 33.793425
|
||||
46299 9272.32 33.879975
|
||||
46549 9357.02 33.966525
|
||||
46799 9441.94 34.053075
|
||||
47049 9527.07 34.139625
|
||||
47299 9612.42 34.226175
|
||||
47549 9697.99 34.312725
|
||||
47799 9783.77 34.399275
|
||||
48049 9869.77 34.485825
|
||||
48299 9955.98 34.572375
|
||||
48549 10042.41 34.658925
|
||||
48799 10129.06 34.745475
|
||||
49049 10215.92 34.832025
|
||||
49299 10303.00 34.918575
|
||||
49549 10390.30 35.005125
|
||||
49799 10477.81 35.091675
|
||||
50049 10565.54 35.178225
|
||||
50299 10653.49 35.264775
|
||||
50549 10741.65 35.351325
|
||||
50799 10830.03 35.437875
|
||||
51049 10918.62 35.524425
|
||||
51299 11007.43 35.610975
|
||||
51549 11096.46 35.697525
|
||||
51799 11185.71 35.784075
|
||||
52049 11275.17 35.870625
|
||||
52299 11364.84 35.957175
|
||||
52549 11454.74 36.043725
|
||||
52799 11544.84 36.130275
|
||||
53049 11635.17 36.216825
|
||||
53299 11725.71 36.303375
|
||||
53549 11816.47 36.389925
|
||||
53799 11907.45 36.476475
|
||||
54049 11998.64 36.563025
|
||||
54299 12090.04 36.649575
|
||||
54549 12181.67 36.736125
|
||||
54799 12273.51 36.822675
|
||||
55049 12365.57 36.909225
|
||||
55299 12457.84 36.995775
|
||||
55549 12550.33 37.082325
|
||||
55799 12643.03 37.168875
|
||||
56049 12735.96 37.255425
|
||||
56299 12829.09 37.341975
|
||||
56549 12922.45 37.428525
|
||||
56799 13016.02 37.515075
|
||||
57049 13109.81 37.601625
|
||||
57299 13203.81 37.688175
|
||||
57549 13298.03 37.774725
|
||||
57799 13392.47 37.861275
|
||||
58049 13487.12 37.947825
|
||||
58299 13581.99 38.034375
|
||||
58549 13677.08 38.120925
|
||||
58799 13772.38 38.207475
|
||||
59049 13867.90 38.294025
|
||||
59299 13963.63 38.380575
|
||||
59549 14059.59 38.467125
|
||||
59799 14155.75 38.553675
|
||||
60049 14252.14 38.640225
|
||||
60299 14348.74 38.726775
|
||||
60549 14445.56 38.813325
|
||||
60799 14542.59 38.899875
|
||||
61049 14639.84 38.986425
|
||||
61299 14737.30 39.072975
|
||||
61549 14834.99 39.159525
|
||||
61799 14932.89 39.246075
|
||||
62049 15031.00 39.332625
|
||||
62299 15129.33 39.419175
|
||||
62549 15227.88 39.505725
|
||||
62799 15326.64 39.592275
|
||||
63049 15425.63 39.678825
|
||||
63299 15524.82 39.765375
|
||||
63549 15624.24 39.851925
|
||||
63799 15723.87 39.938475
|
||||
64049 15823.71 40.025025
|
||||
64299 15923.77 40.111575
|
||||
64549 16024.05 40.198125
|
||||
64799 16124.55 40.284675
|
||||
65049 16225.26 40.371225
|
||||
65299 16326.19 40.457775
|
||||
65549 16427.33 40.544325
|
||||
65799 16528.69 40.630875
|
||||
66049 16630.27 40.717425
|
||||
66299 16732.06 40.803975
|
||||
66549 16834.07 40.890525
|
||||
66799 16936.30 40.977075
|
||||
67049 17038.74 41.063625
|
||||
67299 17141.40 41.150175
|
||||
67549 17244.28 41.236725
|
||||
67799 17347.37 41.323275
|
||||
68049 17450.68 41.409825
|
||||
68299 17554.20 41.496375
|
||||
68549 17657.94 41.582925
|
||||
68799 17761.90 41.669475
|
||||
69049 17866.07 41.756025
|
||||
69299 17970.46 41.842575
|
||||
69549 18075.07 41.963277
|
||||
69879 18213.55 42.000014
|
||||
277826 105551.32 45
|
||||
@@ -0,0 +1,11 @@
|
||||
income tax percent
|
||||
------ --- -------
|
||||
0 0.00 0
|
||||
15900 0.00 1
|
||||
21100 52.00 3
|
||||
42300 688.00 4
|
||||
74000 1956.00 5
|
||||
105700 3541.00 6
|
||||
137400 5443.00 6.5
|
||||
169200 7510.00 7
|
||||
211400 10464.00 8
|
||||
+21
-1
@@ -12,6 +12,9 @@ rdb select 'Description ~ /gloves/' < inventory
|
||||
## column (project): pick columns in any order; a name that does not exist becomes a new empty column
|
||||
column Description Cost < inventory
|
||||
project Item Notes < owners
|
||||
## column -d: keep all columns except the named ones (a deletion)
|
||||
column -d Cost Value < inventory
|
||||
subtotal Date Debit Credit < journal | column -d Account Description
|
||||
## sorttable: -n numeric, -r reverse, -f fold case
|
||||
sorttable -nr Cost < inventory
|
||||
## compute: calculate columns from other columns (the new column must exist, so make it with addcol)
|
||||
@@ -23,8 +26,14 @@ rename Owner Person < owners
|
||||
## number / Number: number the rows (number: rows only, Number: rows and the head)
|
||||
number < owners
|
||||
Number < owners
|
||||
## record: print one record (the 3rd row)
|
||||
## record: print one record. 0 and 1 are the first row, 3 the third, -1 the last, -2 the one before
|
||||
record 3 < inventory
|
||||
record 0 < owners
|
||||
record -1 < owners
|
||||
record -2 < owners
|
||||
## beyond the end: an empty table; not a number: an error
|
||||
record 9 < owners
|
||||
record abc < owners
|
||||
## rmblank: drop empty rows; headon / headoff: add or strip the head
|
||||
column Item Cost < inventory | rmblank
|
||||
headoff < colors
|
||||
@@ -223,6 +232,17 @@ act
|
||||
cashflow < cash
|
||||
## tax: look up the tax for an income in a tax table
|
||||
tax 20000 < X83
|
||||
## tax also works with the real 2026 tables of DE, AT, CH (federal tax only) and LI (Landessteuer,
|
||||
## before the municipal surcharge of 150 % to 250 %); see README. Tax on an income of 60000:
|
||||
tax 60000 < taxDE2026
|
||||
tax 60000 < taxAT2026
|
||||
tax 60000 < taxCH2026
|
||||
tax 60000 < taxLI2026
|
||||
## Swiss cantons (einfache Staatssteuer, single; still times the cantonal Steuerfuss, plus Gemeinde): AI AR SG GR
|
||||
tax 60000 < taxCH_AI2026
|
||||
tax 60000 < taxCH_AR2026
|
||||
tax 60000 < taxCH_SG2026
|
||||
tax 60000 < taxCH_GR2026
|
||||
## explode: parts list -> all subparts with counts (part, amount)
|
||||
explode 10001 2 parttable
|
||||
## chartdup: look for duplicate account numbers and short names in chart
|
||||
|
||||
+56
-2
@@ -28,6 +28,23 @@ Item Notes
|
||||
1
|
||||
3
|
||||
6
|
||||
## column -d: keep all columns except the named ones (a deletion)
|
||||
$ column -d Cost Value < inventory
|
||||
Item Amount Description
|
||||
---- ------ -----------
|
||||
1 3 rubber gloves
|
||||
2 100 test tubes
|
||||
3 5 clamps
|
||||
4 23 plates
|
||||
5 99 cleaning cloth
|
||||
6 89 bunsen burners
|
||||
7 5 scales
|
||||
$ subtotal Date Debit Credit < journal | column -d Account Description
|
||||
Date Debit Credit
|
||||
---- ----- ------
|
||||
861222 25000 25000
|
||||
861223 10000 10000
|
||||
861224 30000 30000
|
||||
## sorttable: -n numeric, -r reverse, -f fold case
|
||||
$ sorttable -nr Cost < inventory
|
||||
Item Amount Cost Value Description
|
||||
@@ -86,11 +103,29 @@ Number Item Owner
|
||||
1 1 ann
|
||||
2 3 bob
|
||||
3 6 cy
|
||||
## record: print one record (the 3rd row)
|
||||
## record: print one record. 0 and 1 are the first row, 3 the third, -1 the last, -2 the one before
|
||||
$ record 3 < inventory
|
||||
Item Amount Cost Value Description
|
||||
---- ------ ---- ----- -----------
|
||||
3 5 8.00 40.00 clamps
|
||||
$ record 0 < owners
|
||||
Item Owner
|
||||
---- -----
|
||||
1 ann
|
||||
$ record -1 < owners
|
||||
Item Owner
|
||||
---- -----
|
||||
6 cy
|
||||
$ record -2 < owners
|
||||
Item Owner
|
||||
---- -----
|
||||
3 bob
|
||||
## beyond the end: an empty table; not a number: an error
|
||||
$ record 9 < owners
|
||||
Item Owner
|
||||
---- -----
|
||||
$ record abc < owners
|
||||
record: 'abc' is not a record number (1, 2, 3 ... or -1 for the last)
|
||||
## rmblank: drop empty rows; headon / headoff: add or strip the head
|
||||
$ column Item Cost < inventory | rmblank
|
||||
Item Cost
|
||||
@@ -845,7 +880,7 @@ Item Amount Cost Value Description
|
||||
## tabletom4 / tabletosed / tabletostruct were shown above
|
||||
## ===== Help and the terminal =====
|
||||
$ rdb --version
|
||||
rdb 0.5.0
|
||||
rdb 0.7.0
|
||||
$ howmany
|
||||
136
|
||||
$ whatis row
|
||||
@@ -935,6 +970,25 @@ Date Amount Balance Description
|
||||
## tax: look up the tax for an income in a tax table
|
||||
$ tax 20000 < X83
|
||||
3369
|
||||
## tax also works with the real 2026 tables of DE, AT, CH (federal tax only) and LI (Landessteuer,
|
||||
## before the municipal surcharge of 150 % to 250 %); see README. Tax on an income of 60000:
|
||||
$ tax 60000 < taxDE2026
|
||||
14233
|
||||
$ tax 60000 < taxAT2026
|
||||
15447
|
||||
$ tax 60000 < taxCH2026
|
||||
671
|
||||
$ tax 60000 < taxLI2026
|
||||
1396
|
||||
## Swiss cantons (einfache Staatssteuer, single; still times the cantonal Steuerfuss, plus Gemeinde): AI AR SG GR
|
||||
$ tax 60000 < taxCH_AI2026
|
||||
3770
|
||||
$ tax 60000 < taxCH_AR2026
|
||||
1065
|
||||
$ tax 60000 < taxCH_SG2026
|
||||
3344
|
||||
$ tax 60000 < taxCH_GR2026
|
||||
3748
|
||||
## explode: parts list -> all subparts with counts (part, amount)
|
||||
$ explode 10001 2 parttable
|
||||
Part Amount
|
||||
|
||||
@@ -11,9 +11,20 @@ Rdb.register('row', 'Make a new table where rows match logical condition',
|
||||
end
|
||||
Rdb.alias_for('select', 'row')
|
||||
|
||||
Rdb.register('column', 'Display columns of a table in any order', 'column [ Column ... ] < tableorlist') do |args|
|
||||
Rdb.register('column', 'Display columns of a table in any order', 'column [ -d ] [ Column ... ] < tableorlist') do |args|
|
||||
drop = args.first == '-d'
|
||||
args.shift if drop
|
||||
t = table_in
|
||||
next t.write if args.empty?
|
||||
next t.write if args.empty? && !drop
|
||||
die 'usage: column -d Column ... < tableorlist' if drop && args.empty?
|
||||
|
||||
if drop # keep every column except the named ones
|
||||
gone = args.map { |a| t.index_of(a) }
|
||||
keep = t.head.each_index.reject { |i| gone.include?(i) }
|
||||
die 'column -d would delete every column' if keep.empty?
|
||||
|
||||
next t.with(head: keep.map { |i| t.head[i] }, rows: t.rows.map { |r| keep.map { |i| r[i] } }).write
|
||||
end
|
||||
|
||||
idx = args.map { |a| Rdb.column_index(t.head, a) }
|
||||
head = args.each_with_index.map { |a, n| idx[n] ? t.head[idx[n]] : a }
|
||||
@@ -72,8 +83,12 @@ end
|
||||
|
||||
Rdb.register('record', 'Find and output a record from a table', 'record number < table') do |args|
|
||||
die 'usage: record number < table' unless args.size == 1
|
||||
die "'#{args[0]}' is not a record number (1, 2, 3 ... or -1 for the last)" unless args[0].match?(/\A[+-]?\d+\z/)
|
||||
|
||||
# 0 and 1 are the first record; -1 is the last, -2 the one before it ...
|
||||
t = table_in
|
||||
row = t.rows[args[0].to_i - 1]
|
||||
n = args[0].to_i
|
||||
row = n.negative? ? (t.rows[n] if -n <= t.rows.size) : t.rows[[n, 1].max - 1]
|
||||
t.with(rows: row ? [row] : []).write
|
||||
end
|
||||
|
||||
@@ -160,12 +175,14 @@ end
|
||||
Rdb.register('splittable', 'Divide a table horizontally', 'splittable [-n] table') do |args|
|
||||
n = 50
|
||||
n = args.shift[1..].to_i if args.first&.match?(/\A-\d+\z/)
|
||||
die 'usage: splittable [-n] table' unless args.size == 1
|
||||
die 'usage: splittable [-n] table (n is at least 1)' unless args.size == 1 && n.positive?
|
||||
|
||||
t = open_table(args[0])
|
||||
chunks = t.rows.each_slice(n).to_a
|
||||
die "#{args[0]}: #{t.rows.size} rows in pieces of #{n} need #{chunks.size} files, but there are only 676 names (aa to zz)" if chunks.size > 676
|
||||
|
||||
suffix = ('aa'..'zz').each
|
||||
t.rows.each_slice(n) do |chunk|
|
||||
File.write("#{args[0]}#{suffix.next}", t.with(rows: chunk).to_s)
|
||||
end
|
||||
chunks.each { |chunk| File.write("#{args[0]}#{suffix.next}", t.with(rows: chunk).to_s) }
|
||||
end
|
||||
|
||||
# ---- aggregates ------------------------------------------------------------
|
||||
|
||||
+8
-4
@@ -81,9 +81,10 @@ end
|
||||
# ---- manual pages ---------------------------------------------------------------
|
||||
|
||||
def troff_clean(str)
|
||||
str.gsub(/\\f[BIRP]/, '').gsub('\\e', '\\').gsub('\\(em', ' -- ').gsub('\\(aa', "'")
|
||||
# a literal backslash (\\ or \e) is parked in \0 so that later rules cannot see it
|
||||
str.gsub('\\\\', "\0").gsub(/\\f[BIRP]/, '').gsub('\\e', "\0").gsub('\\(em', ' -- ').gsub('\\(aa', "'")
|
||||
.gsub('\\-', '-').gsub('\\&', '').gsub('\\*(UX', 'UNIX').gsub('``', '"').gsub("''", '"').gsub('\\ ', ' ')
|
||||
.gsub(/\\\(..|\\\*\(..|\\[a-z]/, '').gsub('\\\\', '\\')
|
||||
.gsub(/\\\(..|\\\*\(..|\\[a-z]/, '').gsub("\0", '\\')
|
||||
end
|
||||
|
||||
def render_man(src)
|
||||
@@ -147,8 +148,11 @@ Rdb.register('man', 'Display the manual page for a command', 'man [-g] command')
|
||||
args.each do |a|
|
||||
f = File.join(Rdb::ROOT, 'man', "#{a == 'Number' ? 'number.c' : a}.man")
|
||||
File.exist?(f) or die("no manual page for '#{a}'")
|
||||
desc = File.read(f)[/^\.PC\s+"?(.*?)"?\s*$/, 1]
|
||||
puts "#{a}#{desc ? " -- #{troff_clean(desc)}" : ''}", '=' * 60
|
||||
src = File.read(f)
|
||||
desc = src[/^\.PC[ \t]+"?(.*?)"?[ \t]*$/, 1]
|
||||
# topic pages (dash line, head line ...) have an empty .PC; their title is in .TH
|
||||
title = desc.to_s.empty? && src =~ /^\.TH\s+(?:"([^"]*)"|(\S+))/ ? ($1 || $2) : nil
|
||||
puts "#{title || a}#{desc.to_s.empty? ? '' : " -- #{troff_clean(desc)}"}", '=' * 60
|
||||
puts render_man(File.read(f)), ''
|
||||
end
|
||||
end
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Rdb
|
||||
VERSION = '0.5.0'
|
||||
VERSION = '0.7.0'
|
||||
end
|
||||
|
||||
@@ -129,6 +129,24 @@ Names that are also
|
||||
reserved words or are followed by
|
||||
.B (
|
||||
are never taken for columns.
|
||||
.SH "Deleting Columns"
|
||||
.I "(Ruby re-creation only.)"
|
||||
.DM
|
||||
column -d Column ... < tableorlist
|
||||
.DE
|
||||
.PP
|
||||
With the option
|
||||
.BR -d ,
|
||||
.B column
|
||||
keeps every column
|
||||
.I except
|
||||
those named.
|
||||
The order of the other columns stays as it is.
|
||||
It is an error to name a column that does not exist,
|
||||
or to delete every column.
|
||||
.DM
|
||||
subtotal Date Debit Credit < journal | column -d Account Description
|
||||
.DE
|
||||
.SH "See Also"
|
||||
.B
|
||||
project,
|
||||
|
||||
@@ -59,6 +59,17 @@ you see output in the following format:
|
||||
.DM
|
||||
Item 1 is rubber gloves
|
||||
.DE
|
||||
.SH "Record Numbers"
|
||||
.I "(Ruby re-creation only.)"
|
||||
Records are counted from 1.
|
||||
The numbers 0 and 1 both mean the first record.
|
||||
A negative number counts from the end:
|
||||
.B \-1
|
||||
is the last record,
|
||||
.B \-2
|
||||
the one before it, and so on.
|
||||
A number beyond either end gives a table without records.
|
||||
Anything that is not a whole number is an error.
|
||||
.SH "See Also"
|
||||
.B
|
||||
row,
|
||||
|
||||
+26
@@ -191,6 +191,32 @@ Names that are also
|
||||
reserved words or are followed by
|
||||
.B (
|
||||
are never taken for columns.
|
||||
.SH "Using select in a Shell"
|
||||
.I "(Ruby re-creation only.)"
|
||||
.B select
|
||||
is another name for
|
||||
.BR row ,
|
||||
but in
|
||||
.BR bash ,
|
||||
.B zsh
|
||||
and
|
||||
.B ksh
|
||||
``select'' is a reserved word of the shell.
|
||||
The shell recognizes it before it looks for a program, so
|
||||
.DM
|
||||
select 'Cost > 5' < inventory
|
||||
.DE
|
||||
.PP
|
||||
stops with a syntax error at the ``<''.
|
||||
Use the name
|
||||
.B row
|
||||
instead, or protect the word from the shell:
|
||||
.DM
|
||||
row 'Cost > 5' < inventory
|
||||
\\select 'Cost > 5' < inventory
|
||||
command select 'Cost > 5' < inventory
|
||||
rdb select 'Cost > 5' < inventory
|
||||
.DE
|
||||
.SH "See Also"
|
||||
.B
|
||||
compute,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
[tools]
|
||||
"cargo:scryer-prolog" = "0.10"
|
||||
ruby = "3.4"
|
||||
rust = "1.99"
|
||||
|
||||
[env]
|
||||
_.path = ["{{config_root}}/bin"]
|
||||
|
||||
+116
-1
@@ -5,6 +5,65 @@ require 'pty'
|
||||
require_relative '../lib/rdb'
|
||||
|
||||
class SelectionTest < RdbCase
|
||||
def test_column_d_deletes_columns
|
||||
out = rdb('column', '-d', 'Cost', 'Value', file: 'inventory')
|
||||
assert_equal %w[Item Amount Description], out.lines.first.chomp.split("\t")
|
||||
assert_equal %w[1 3 rubber\ gloves], data(out).first
|
||||
assert_equal %w[Date Debit Credit], rdb('column', '-d', 'Account', 'Description', stdin: rdb('subtotal', 'Date', 'Debit', 'Credit', file: 'journal')).lines.first.chomp.split("\t")
|
||||
end
|
||||
|
||||
def test_column_d_errors
|
||||
src = File.read(File.join(@dir, 'owners'))
|
||||
[%w[-d], %w[-d Nope], %w[-d Item Owner]].each do |args|
|
||||
out, err, st = run3('column', *args, stdin: src)
|
||||
refute st.success?, "column #{args.join(' ')} should fail"
|
||||
assert_equal '', out
|
||||
refute_empty err
|
||||
end
|
||||
end
|
||||
|
||||
def test_column_d_works_on_lists_and_with_icase
|
||||
list = rdb('tabletolist', file: 'owners')
|
||||
assert_includes rdb('column', '-d', 'Item', stdin: list), "Owner\tann"
|
||||
out, _e, st = run3('column', '-d', 'COST', stdin: File.read(File.join(@dir, 'inventory')), env: { 'RDB_ICASE' => '1' })
|
||||
assert st.success?
|
||||
refute_includes out.lines.first, 'Cost'
|
||||
end
|
||||
|
||||
def test_splittable_pieces_and_errors
|
||||
out, = run3('splittable', '-4', 'inventory')
|
||||
assert_equal 4, data(File.read(File.join(@dir, 'inventoryaa'))).size
|
||||
assert_equal 3, data(File.read(File.join(@dir, 'inventoryab'))).size
|
||||
assert_includes File.read(File.join(@dir, 'inventoryab')), "Item\tAmount"
|
||||
_o, err, st = run3('splittable', '-0', 'inventory')
|
||||
refute st.success?
|
||||
assert_match(/usage/, err)
|
||||
write('big', "N\n-\n#{(1..700).to_a.join("\n")}\n")
|
||||
_o, err, st = run3('splittable', '-1', 'big')
|
||||
refute st.success?
|
||||
assert_match(/only 676 names/, err)
|
||||
assert_empty Dir[File.join(@dir, 'bigaa')] # nothing is written when it cannot all be written
|
||||
end
|
||||
|
||||
def test_record_zero_and_one_are_the_first_and_negatives_count_from_the_end
|
||||
{ '0' => '1', '1' => '1', '2' => '3', '3' => '6', '-1' => '6', '-2' => '3', '-3' => '1', '+2' => '3' }.each do |n, item|
|
||||
assert_equal [[item]], data(rdb('record', n, file: 'owners')).map { |r| r.first(1) }, "record #{n}"
|
||||
end
|
||||
end
|
||||
|
||||
def test_record_beyond_either_end_is_an_empty_table
|
||||
%w[4 9 -4 -9].each { |n| assert_equal [], data(rdb('record', n, file: 'owners')), "record #{n}" }
|
||||
end
|
||||
|
||||
def test_record_rejects_junk
|
||||
%w[abc 2.5 1e3].each do |n|
|
||||
out, err, st = run3('record', n, stdin: File.read(File.join(@dir, 'owners')))
|
||||
refute st.success?, "record #{n} should fail"
|
||||
assert_equal '', out
|
||||
assert_match(/not a record number/, err)
|
||||
end
|
||||
end
|
||||
|
||||
def test_row_numeric_condition
|
||||
out = rdb('row', 'Cost > 5', file: 'inventory')
|
||||
assert_equal %w[3 6 7], data(out).map(&:first)
|
||||
@@ -429,7 +488,7 @@ end
|
||||
|
||||
class HelpTest < RdbCase
|
||||
def test_whatis_whatwill_commands
|
||||
assert_includes rdb('whatis', 'column'), " column - display columns of a table in any order\n column column [ Column ... ] < tableorlist\n"
|
||||
assert_includes rdb('whatis', 'column'), " column - display columns of a table in any order\n column column [ -d ] [ Column ... ] < tableorlist\n"
|
||||
assert_includes rdb('whatwill', 'total'), 'subtotal'
|
||||
assert_operator data(rdb('commands')).size, :>, 120
|
||||
assert_operator rdb('howmany').to_i, :>, 120
|
||||
@@ -447,6 +506,12 @@ class HelpTest < RdbCase
|
||||
assert_equal "\e[3;8H", rdb('cursor', '3', '8')
|
||||
end
|
||||
|
||||
def test_man_topic_pages_get_their_title
|
||||
assert_equal 'dash line', rdb('man', 'dashline').lines.first.chomp
|
||||
assert_equal 'head line', rdb('man', 'headline').lines.first.chomp
|
||||
assert_match(/\Arow -- Make a new table/, rdb('man', 'row'))
|
||||
end
|
||||
|
||||
def test_testall_mentions_every_command
|
||||
script = File.read(File.join(DEMO, 'testall.cmds'))
|
||||
names = data(rdb('commands')).map(&:first)
|
||||
@@ -723,3 +788,53 @@ class PickTest < RdbCase
|
||||
assert_equal '', table
|
||||
end
|
||||
end
|
||||
|
||||
class TaxTablesTest < RdbCase
|
||||
def tax(income, table) = rdb('tax', income.to_s, file: table).to_i
|
||||
|
||||
def test_at_2026
|
||||
assert_equal 0, tax(13_539, 'taxAT2026')
|
||||
assert_equal 11_447, tax(50_000, 'taxAT2026') # 6 030.40 + 13 542 * 40 %
|
||||
assert_equal 33_818, tax(100_000, 'taxAT2026')
|
||||
end
|
||||
|
||||
def test_ch_federal_2026
|
||||
assert_equal 0, tax(15_200, 'taxCH2026')
|
||||
assert_equal 401, tax(50_000, 'taxCH2026')
|
||||
assert_equal 2684, tax(100_000, 'taxCH2026')
|
||||
assert_equal 91_310, tax(794_000, 'taxCH2026')
|
||||
end
|
||||
|
||||
def test_li_landessteuer_2026
|
||||
assert_equal 996, tax(50_000, 'taxLI2026') # 0,04 * x - 1 004
|
||||
assert_equal 3256, tax(100_000, 'taxLI2026') # 0,05 * x - 1 744
|
||||
assert_equal 10_464, tax(211_400, 'taxLI2026')
|
||||
assert_equal 0, tax(15_855, 'taxLI2026')
|
||||
end
|
||||
|
||||
def test_swiss_cantons_2026_einfache_staatssteuer
|
||||
assert_equal 3770, tax(60_000, 'taxCH_AI2026') # 2 070 + 8,5 % of 20 000
|
||||
assert_equal 16_000, tax(200_000, 'taxCH_AI2026')
|
||||
assert_equal 24_000, tax(300_000, 'taxCH_AI2026') # 8 % of the whole income
|
||||
assert_equal 1065, tax(60_000, 'taxCH_AR2026')
|
||||
assert_equal 3344, tax(60_000, 'taxCH_SG2026') # 1 248 + 8 % + 9,2 % pieces
|
||||
assert_equal 25_500, tax(300_000, 'taxCH_SG2026') # 8,5 % of the whole income
|
||||
assert_equal 3748, tax(60_000, 'taxCH_GR2026')
|
||||
assert_equal 0, tax(16_895, 'taxCH_GR2026')
|
||||
assert_equal 20, tax(5_000, 'taxCH_AI2026') # 1 % of 2 000
|
||||
%w[AR SG GR].each { |c| assert_equal 0, tax(5_000, "taxCH_#{c}2026") }
|
||||
end
|
||||
|
||||
# § 32a EStG: the table is a straight-line approximation of the curved zones
|
||||
def test_de_2026_follows_the_formula
|
||||
[12_348, 14_000, 17_799, 20_000, 33_333, 50_000, 69_878, 100_000, 277_825, 300_000].each do |x|
|
||||
exact = if x <= 12_348 then 0
|
||||
elsif x <= 17_799 then y = (x - 12_348) / 10_000.0; ((914.51 * y) + 1400) * y
|
||||
elsif x <= 69_878 then z = (x - 17_799) / 10_000.0; (((173.10 * z) + 2397) * z) + 1034.87
|
||||
elsif x <= 277_825 then (0.42 * x) - 11_135.63
|
||||
else (0.45 * x) - 19_470.38
|
||||
end
|
||||
assert_in_delta exact.floor, tax(x, 'taxDE2026'), 1, "income #{x}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Writes the 2026 income tax tables in the format of `tax` (income, tax, percent)
|
||||
# into demo/. Sources: § 32a EStG (DE), § 33 EStG (AT), Tarif 2026 Art. 36 DBG (CH,
|
||||
# ESTV Rundschreiben 2-215-D-2025), Art. 19 SteG LR 640.0, Fassung 01.07.2026 (LI).
|
||||
# ruby tools/gen_tax_tables.rb
|
||||
|
||||
DEMO = File.expand_path('../demo', __dir__)
|
||||
|
||||
def write_table(name, points)
|
||||
rows = points.map { |i, t, p| [i.to_s, format('%.2f', t), format('%.6f', p).sub(/0+\z/, '').sub(/\.\z/, '')] }
|
||||
File.write(File.join(DEMO, name), "income\ttax\tpercent\n------\t---\t-------\n#{rows.map { |r| r.join("\t") }.join("\n")}\n")
|
||||
end
|
||||
|
||||
# --- Germany: single assessment, § 32a Abs. 1 (the zones 2 and 3 are curves, so
|
||||
# the table approximates them with short straight pieces; error below one euro)
|
||||
def de_tax(x)
|
||||
if x <= 12_348 then 0.0
|
||||
elsif x <= 17_799 then y = (x - 12_348) / 10_000.0; ((914.51 * y) + 1400) * y
|
||||
elsif x <= 69_878 then z = (x - 17_799) / 10_000.0; (((173.10 * z) + 2397) * z) + 1034.87
|
||||
elsif x <= 277_825 then (0.42 * x) - 11_135.63
|
||||
else (0.45 * x) - 19_470.38
|
||||
end
|
||||
end
|
||||
|
||||
de = [12_348] + (12_598..17_798).step(250).to_a + [17_799] + (18_049..69_549).step(250).to_a + [69_879, 277_826]
|
||||
pts = de.each_with_index.map do |i, n|
|
||||
nxt = de[n + 1]
|
||||
slope = nxt ? (de_tax(nxt) - de_tax(i)) / (nxt - i) * 100 : 45.0
|
||||
[i, de_tax(i), slope]
|
||||
end
|
||||
write_table('taxDE2026', [[0, 0.0, 0.0]] + pts)
|
||||
|
||||
# --- Austria: § 33 EStG, the tariff before the Absetzbeträge
|
||||
at = [[13_539, 20], [21_992, 30], [36_458, 40], [70_365, 48], [104_859, 50], [1_000_000, 55]]
|
||||
acc = 0.0
|
||||
prev = [0, 0]
|
||||
pts = [[0, 0.0, 0.0]]
|
||||
at.each do |limit, rate|
|
||||
acc += (limit - prev[0]) * prev[1] / 100.0
|
||||
pts << [limit, acc, rate]
|
||||
prev = [limit, rate]
|
||||
end
|
||||
write_table('taxAT2026', pts)
|
||||
|
||||
# --- Switzerland: direct federal tax, Art. 36 DBG, single persons (without canton and municipality)
|
||||
ch = [[0, 0, 0], [15_200, 0, 0.77], [33_200, 138.60, 0.88], [43_500, 229.20, 2.64], [58_000, 612.00, 2.97],
|
||||
[76_200, 1152.50, 5.94], [82_100, 1502.95, 6.60], [108_900, 3271.75, 8.80], [141_500, 6140.55, 11.00],
|
||||
[185_100, 10_936.55, 13.20], [793_900, 91_298.15, 11.85], [794_000, 91_310.00, 11.50]]
|
||||
write_table('taxCH2026', ch)
|
||||
|
||||
# --- Liechtenstein: Landessteuer, Art. 19 Abs. 1 Bst. a SteG; the municipal surcharge
|
||||
# (150 % to 250 % of it) comes on top: total = Landessteuer * (1 + surcharge / 100)
|
||||
li_rates = [[15_900, 1], [21_100, 3], [42_300, 4], [74_000, 5], [105_700, 6], [137_400, 6.5], [169_200, 7], [211_400, 8]]
|
||||
acc = 0.0
|
||||
prev = [15_855, 0]
|
||||
pts = [[0, 0.0, 0.0]]
|
||||
li_rates.each do |limit, rate|
|
||||
acc += (limit - prev[0]) * prev[1] / 100.0 if prev[1].positive?
|
||||
pts << [limit, acc, rate]
|
||||
prev = [limit, rate]
|
||||
end
|
||||
write_table('taxLI2026', pts)
|
||||
|
||||
# --- Swiss cantons: the "einfache Staatssteuer" for single persons, before the Steuerfuss and
|
||||
# without municipal and church tax. Source: ESTV Kantonsblätter (Steuerinformationen 2026).
|
||||
# Each tariff is a list of [width, rate] pieces; above the last piece one flat rate applies to
|
||||
# the whole income (the cantons call this "für das ganze Einkommen").
|
||||
def staircase(pieces, flat_rate, expect_limit)
|
||||
acc = 0.0
|
||||
start = 0
|
||||
pts = []
|
||||
pieces.each do |width, rate|
|
||||
pts << [start, acc, rate]
|
||||
acc += width * rate / 100.0
|
||||
start += width
|
||||
end
|
||||
raise "limit #{start} != #{expect_limit}" unless start == expect_limit
|
||||
|
||||
pts << [start, start * flat_rate / 100.0, flat_rate]
|
||||
end
|
||||
|
||||
# AI: Art. 38 StG
|
||||
write_table('taxCH_AI2026', staircase([[3_000, 0], [3_000, 1], [3_000, 2], [3_000, 3], [3_000, 4], [3_000, 5], [4_000, 6],
|
||||
[4_000, 7], [4_000, 7.5], [10_000, 8], [34_000, 8.5], [66_000, 9], [60_000, 8.5]], 8, 200_000))
|
||||
# AR: Art. 39 Abs. 1 lit. b StG (Alleinstehendentarif, Beträge ab Steuerperiode 2024); multiplied by the Steuereinheiten
|
||||
write_table('taxCH_AR2026', staircase([[8_300, 0], [1_600, 0.6], [1_600, 1.0], [4_200, 1.5], [11_500, 1.8], [14_600, 2.2],
|
||||
[12_500, 2.4], [19_800, 2.6], [14_600, 2.7], [36_500, 2.8], [135_600, 2.9]], 2.6, 260_800))
|
||||
# SG: Art. 50 StG (Beträge ab Steuerperiode 2026)
|
||||
write_table('taxCH_SG2026', staircase([[11_600, 0], [4_200, 4], [18_000, 6], [26_500, 8], [38_100, 9.2], [166_100, 9.4]], 8.5, 264_500))
|
||||
# GR: Art. 39 Abs. 1 StG (indexierte Beträge ab Steuerperiode 2025)
|
||||
write_table('taxCH_GR2026', staircase([[16_895, 0], [1_090, 2.5], [1_090, 4], [1_090, 5], [1_090, 6], [1_090, 6.5], [2_180, 7],
|
||||
[6_540, 8], [4_360, 8.5], [4_360, 9], [4_360, 9.5], [21_800, 10.3], [21_800, 10.6],
|
||||
[21_800, 10.7], [109_000, 11.2], [109_000, 11.3], [109_000, 11.4], [343_895, 11.6]], 11.0, 780_440))
|
||||
Reference in New Issue
Block a user