Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80941b6f03 | ||
|
|
a02e5cb109 | ||
|
|
a9f3c0b255 | ||
|
|
2e03c0a440 | ||
|
|
6b9810b075 | ||
|
|
b11e3da453 | ||
|
|
1c66773ff4 | ||
|
|
e1bdb6c703 | ||
|
|
777a3b56e2 | ||
|
|
d5286c3600 | ||
|
|
f83d1bd6f8 | ||
|
|
6c0d675c5d | ||
|
|
d2c4343136 |
@@ -7,6 +7,33 @@ 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
|
||||
- `pick`: Tabelle auf dem Terminal durchblättern, filtern und eine oder mehrere Zeilen (`-m`) auswählen; die Auswahl geht als Tabelle auf die Standardausgabe. `-k` nimmt die Tasten aus einem String. Dazu `man/pick.man`, Beispiele in `testall.cmds` und Tests (auch mit Pseudo-Terminal).
|
||||
|
||||
## [0.4.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.
|
||||
|
||||
@@ -102,6 +104,63 @@ In der Vorlage: `<Spalte>` oder `<Spalte:20>` ist ein Feld (mit Breite), `[ Spei
|
||||
`[ Abbrechen ]` (oder Cancel, Abort, Quit) bricht ab, `<!date!>` fügt Befehlsausgabe ein. Kommt eine Spalte mehrfach
|
||||
vor, ist es ein Feld. Tab/Pfeile wechseln, Enter bestätigt, Esc bricht ab (Status 1, keine Ausgabe).
|
||||
|
||||
## Zeilen auswählen: `pick`
|
||||
|
||||
`pick` zeigt eine Tabelle auf dem Terminal, man scrollt, filtert und wählt eine oder mehrere Zeilen. Die Tabelle
|
||||
kommt von der Standardeingabe, die gewählten Zeilen gehen als Tabelle auf die Standardausgabe, also mitten in der Pipe:
|
||||
|
||||
```
|
||||
pick < inventory eine Zeile wählen
|
||||
pick -m < inventory | total Amount Value mehrere (Leertaste markiert, a alle)
|
||||
column Item Description < inventory | pick -m | jointable - owners
|
||||
pick -m -k 'down space down enter' < inventory Tasten aus einem String (Skripte, Tests)
|
||||
```
|
||||
|
||||
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 |
|
||||
@@ -116,7 +175,7 @@ vor, ist es ein Feld. Tab/Pfeile wechseln, Enter bestätigt, Esc bricht ab (Stat
|
||||
| Text | `cap` `uppercase` `lowercase` `substitute` `trimblank` `padstring` `length` `filesize` `ascii` `chr` `word` `prompt` `not` `path` |
|
||||
| Umwandeln | `tabletolist` `listtotable` `listtosh` `tabletofix` `fixtotable` `tabletostruct` `tabletotbl` `tabletofact` `tabletorule` `tabletom4` `tabletosed` `translate` `uniondict` |
|
||||
| Datum, Namen | `julian` `gregorian` `computedate` `todaysdate` `soundex` `like` `hashkey` |
|
||||
| Berichte | `report` `reportwriter` `screen` `mask` `label` `letter` |
|
||||
| Berichte | `report` `reportwriter` `screen` `mask` `pick` `label` `letter` |
|
||||
| Terminal, Hilfe | `termput` `tset` `clear.rdb` `cursor` `menu` `rdb` `commands` `helpme` `whatis` `whatwill` `howmany` `man` |
|
||||
| Betrieb | `testall` `testsearch` `timesearch` `rmcore` `cpdir.rdb` |
|
||||
| Buchhaltung (/act) | `act` `chartdup` `foot` `adjust` `balance` `close` `consolidate` `getjournal` `fillform` `calculate` `tax` `cashflow` `calcpay` `w2` `bom` `explode` `sale` `invoice` `cstate` `update.inv` |
|
||||
@@ -160,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
|
||||
+34
-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
|
||||
@@ -265,3 +285,16 @@ bom ; cat bom
|
||||
## update.inv: change the stock of one item, with record locking (INV names the table)
|
||||
cp inventory inv ; printf '7\n9\n\n' | INV=inv update.inv | tail -n +2
|
||||
grep '^7' inv
|
||||
|
||||
## ===== Choosing rows on the terminal =====
|
||||
## pick shows a table, lets you scroll, filter (/) and mark rows (Space, -m), and writes the
|
||||
## chosen rows as a table, so it can sit in a pipe. It needs a terminal; -k feeds the keys instead.
|
||||
## Try it: pick -m < inventory | total Amount Value
|
||||
## mark the 2nd and 5th row (down, space, down, down, space) and finish with enter
|
||||
pick -m -k 'down space down down space enter' < inventory
|
||||
## the same chosen rows go on into total
|
||||
pick -m -k 'down space down down space enter' < inventory | total Amount Value
|
||||
## filter with / (type, enter), then choose the row that is left
|
||||
pick -k '/clamps enter enter' < inventory
|
||||
## Esc cancels: nothing on stdout, exit status 1
|
||||
pick -k esc < inventory ; echo "status $?"
|
||||
|
||||
+81
-3
@@ -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,9 +880,9 @@ Item Amount Cost Value Description
|
||||
## tabletom4 / tabletosed / tabletostruct were shown above
|
||||
## ===== Help and the terminal =====
|
||||
$ rdb --version
|
||||
rdb 0.4.0
|
||||
rdb 0.7.0
|
||||
$ howmany
|
||||
135
|
||||
136
|
||||
$ whatis row
|
||||
row - make a new table where rows match logical condition
|
||||
row row 'column condition column-or-value ..' < table
|
||||
@@ -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
|
||||
@@ -1107,3 +1161,27 @@ Item Number: (or hit Return key to exit.) Item=7 Amount=5 Cost=175.00 Value=8
|
||||
Amount Amount: Item Number: (or hit Return key to exit.)
|
||||
$ grep '^7' inv
|
||||
7 9 175.00 1575 scales
|
||||
## ===== Choosing rows on the terminal =====
|
||||
## pick shows a table, lets you scroll, filter (/) and mark rows (Space, -m), and writes the
|
||||
## chosen rows as a table, so it can sit in a pipe. It needs a terminal; -k feeds the keys instead.
|
||||
## Try it: pick -m < inventory | total Amount Value
|
||||
## mark the 2nd and 5th row (down, space, down, down, space) and finish with enter
|
||||
$ pick -m -k 'down space down down space enter' < inventory
|
||||
Item Amount Cost Value Description
|
||||
---- ------ ---- ----- -----------
|
||||
2 100 0.50 50.00 test tubes
|
||||
5 99 2.45 242.55 cleaning cloth
|
||||
## the same chosen rows go on into total
|
||||
$ pick -m -k 'down space down down space enter' < inventory | total Amount Value
|
||||
Item Amount Cost Value Description
|
||||
---- ------ ---- ----- -----------
|
||||
199 292.55
|
||||
## filter with / (type, enter), then choose the row that is left
|
||||
$ pick -k '/clamps enter enter' < inventory
|
||||
Item Amount Cost Value Description
|
||||
---- ------ ---- ----- -----------
|
||||
3 6 8.00 48.00 clamps
|
||||
## Esc cancels: nothing on stdout, exit status 1
|
||||
$ pick -k esc < inventory ; echo "status $?"
|
||||
pick: nothing chosen
|
||||
status 1
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'io/console'
|
||||
|
||||
# pick -- look at a table on the terminal, scroll through it and choose one or
|
||||
# several rows. The table comes in on the standard input, the chosen rows go
|
||||
# out on the standard output as a table, so it sits in the middle of a pipe:
|
||||
# column Item Description < inventory | pick -m | total Item
|
||||
# The keys are read from /dev/tty (or, for scripts and tests, from -k).
|
||||
module Rdb
|
||||
module Pick
|
||||
KEYS = {
|
||||
'up' => "\e[A", 'down' => "\e[B", 'pgup' => "\e[5~", 'pgdn' => "\e[6~",
|
||||
'home' => "\e[H", 'end' => "\e[F", 'enter' => "\r", 'space' => ' ',
|
||||
'esc' => "\e", 'backspace' => "\x7f"
|
||||
}.freeze
|
||||
|
||||
# The state of one session: which rows are shown, where the cursor is,
|
||||
# which rows are marked. Knows nothing about the terminal.
|
||||
class Session
|
||||
attr_reader :head, :rows, :cursor, :top, :filter, :marked
|
||||
|
||||
def initialize(head, rows, multi: false)
|
||||
@head = head
|
||||
@rows = rows
|
||||
@multi = multi
|
||||
@marked = []
|
||||
@filter = +''
|
||||
@typing = false
|
||||
@cursor = 0
|
||||
@top = 0
|
||||
@height = 20
|
||||
@width = 80
|
||||
refilter
|
||||
end
|
||||
|
||||
def resize(height, width)
|
||||
@height = [height, 1].max
|
||||
@width = [width, 20].max
|
||||
scroll
|
||||
end
|
||||
|
||||
def typing? = @typing
|
||||
|
||||
# Indices (into rows) of the rows that pass the filter.
|
||||
def visible = @visible
|
||||
|
||||
def current = @visible[@cursor]
|
||||
|
||||
# Rows to hand on: the marked ones, else the row under the cursor.
|
||||
def selected
|
||||
idx = @marked.empty? ? [current].compact : @marked.sort
|
||||
idx.map { |i| @rows[i] }
|
||||
end
|
||||
|
||||
# Returns :accept or :cancel when the session is over.
|
||||
def feed(key)
|
||||
@typing ? type(key) : browse(key)
|
||||
end
|
||||
|
||||
def render
|
||||
widths = @head.each_index.map { |i| ([@head[i]] + @rows.map { |r| r[i].to_s }).map(&:size).max }
|
||||
line = ->(cells) { cells.each_with_index.map { |c, i| c.to_s.ljust(widths[i]) }.join(' ').rstrip }
|
||||
out = +"\e[?25l\e[H"
|
||||
out << "\e[K \e[1m#{fit(line.(@head))}\e[0m\r\n"
|
||||
out << "\e[K #{fit(widths.map { |w| '-' * w }.join(' '))}\r\n"
|
||||
@height.times do |n|
|
||||
pos = @top + n
|
||||
i = @visible[pos]
|
||||
out << "\e[K"
|
||||
if i
|
||||
text = "#{@marked.include?(i) ? '*' : ' '} #{fit(line.(@rows[i]))}"
|
||||
out << (pos == @cursor ? "\e[7m#{text}\e[0m" : text)
|
||||
end
|
||||
out << "\r\n"
|
||||
end
|
||||
out << "\e[K#{status}"
|
||||
out
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fit(text) = text[0, @width - 2]
|
||||
|
||||
def status
|
||||
keys = @typing ? 'type to filter Enter keep Esc clear' : "Enter ok#{' Space mark a all' if @multi} / filter Esc cancel"
|
||||
filter = @filter.empty? && !@typing ? '' : " /#{@filter}#{'_' if @typing}"
|
||||
"\e[7m #{@visible.empty? ? 0 : @cursor + 1}/#{@visible.size} of #{@rows.size}" \
|
||||
"#{" marked #{@marked.size}" if @multi}#{filter} #{keys} \e[0m"
|
||||
end
|
||||
|
||||
def browse(key)
|
||||
case key
|
||||
when "\e[A" then move(-1)
|
||||
when "\e[B" then move(1)
|
||||
when "\e[5~" then move(-@height)
|
||||
when "\e[6~" then move(@height)
|
||||
when "\e[H", "\e[1~", "\eOH" then move(-@visible.size)
|
||||
when "\e[F", "\e[4~", "\eOF" then move(@visible.size)
|
||||
when "\r", "\n" then return selected.empty? ? :cancel : :accept
|
||||
when ' ' then toggle
|
||||
when 'a' then mark_all if @multi
|
||||
when '/' then @typing = true
|
||||
when "\x03", "\e", 'q' then return :cancel
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
def type(key)
|
||||
case key
|
||||
when "\r", "\n" then @typing = false
|
||||
when "\e" then @filter.clear; @typing = false; refilter
|
||||
when "\x03" then return :cancel
|
||||
when "\x7f", "\b" then @filter.chop!; refilter
|
||||
else
|
||||
if key.size == 1 && key.ord >= 32
|
||||
@filter << key
|
||||
refilter
|
||||
end
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
def refilter
|
||||
needle = @filter.downcase
|
||||
@visible = @rows.each_index.select { |i| needle.empty? || @rows[i].join("\t").downcase.include?(needle) }
|
||||
@cursor = [@cursor, [@visible.size - 1, 0].max].min
|
||||
scroll
|
||||
end
|
||||
|
||||
def move(delta)
|
||||
@cursor = (@cursor + delta).clamp(0, [@visible.size - 1, 0].max)
|
||||
scroll
|
||||
end
|
||||
|
||||
def scroll
|
||||
@top = @cursor if @cursor < @top
|
||||
@top = @cursor - @height + 1 if @cursor >= @top + @height
|
||||
@top = 0 if @top.negative?
|
||||
end
|
||||
|
||||
def toggle
|
||||
return unless @multi && current
|
||||
|
||||
@marked.include?(current) ? @marked.delete(current) : @marked << current
|
||||
move(1)
|
||||
end
|
||||
|
||||
def mark_all
|
||||
all = @visible.all? { |i| @marked.include?(i) }
|
||||
all ? @marked -= @visible : @marked |= @visible
|
||||
end
|
||||
end
|
||||
|
||||
def self.run(session, tty)
|
||||
tty.raw do
|
||||
tty.write "\e[?1049h"
|
||||
begin
|
||||
loop do
|
||||
rows, cols = tty.winsize
|
||||
session.resize((rows.zero? ? 24 : rows) - 3, cols.zero? ? 80 : cols)
|
||||
tty.write session.render
|
||||
key = begin
|
||||
Rdb::Mask.read_key(tty)
|
||||
rescue EOFError
|
||||
"\x03"
|
||||
end
|
||||
result = session.feed(key)
|
||||
return result if result
|
||||
end
|
||||
ensure
|
||||
tty.write "\e[?25h\e[?1049l"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Words of a -k script: a name from KEYS, or else every character is one key.
|
||||
def self.script(words)
|
||||
words.split.flat_map { |w| KEYS.key?(w) ? [KEYS[w]] : w.chars }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Rdb.register('pick', 'Scroll through a table and choose one or more rows', 'pick [-m] [-k keys] < table > chosenrows') do |args|
|
||||
multi = !args.delete('-m').nil?
|
||||
keys = nil
|
||||
if (i = args.index('-k'))
|
||||
keys = args[i + 1] or die 'usage: pick [-m] [-k keys] < table'
|
||||
args.slice!(i, 2)
|
||||
end
|
||||
die 'usage: pick [-m] [-k "down space enter"] < table > chosenrows' unless args.empty?
|
||||
|
||||
t = table_in
|
||||
die 'pick: the table is empty' if t.rows.empty?
|
||||
session = Rdb::Pick::Session.new(t.head, t.rows, multi: multi)
|
||||
|
||||
result = nil
|
||||
if keys
|
||||
session.resize(21, 80)
|
||||
Rdb::Pick.script(keys).each do |k|
|
||||
result = session.feed(k)
|
||||
break if result
|
||||
end
|
||||
else
|
||||
tty = begin
|
||||
File.open('/dev/tty', 'r+')
|
||||
rescue SystemCallError
|
||||
die 'no terminal available (pick takes its keys from /dev/tty; see -k)'
|
||||
end
|
||||
result = Rdb::Pick.run(session, tty)
|
||||
tty.close
|
||||
end
|
||||
unless result == :accept
|
||||
warn 'pick: nothing chosen'
|
||||
exit 1
|
||||
end
|
||||
Rdb::Table.new(t.head, session.selected).write
|
||||
end
|
||||
@@ -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.4.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,
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
.TH pick "" "" "/rdb Command"
|
||||
.PC "Scroll through a table and choose one or more rows"
|
||||
\fBpick \fB[ -m ] [ -k \fIkeys\fB ] < \fItable \fB> \fIchosenrows\fR
|
||||
.PP
|
||||
.II "pick^choose rows"
|
||||
.II "table^scroll"
|
||||
.I "(Ruby re-creation only; the original /rdb has no such command.)"
|
||||
.PP
|
||||
The command
|
||||
.B pick
|
||||
shows
|
||||
.I table
|
||||
on the terminal, lets the user scroll through it, and writes the rows
|
||||
that were chosen as a table on the standard output.
|
||||
It therefore fits into the middle of a pipe:
|
||||
.DM
|
||||
column Item Description < inventory | pick -m | jointable - owners
|
||||
pick -m < inventory | total Amount Value
|
||||
.DE
|
||||
.PP
|
||||
The table is read from the standard input;
|
||||
the keys are read from the terminal (\fB/dev/tty\fR).
|
||||
A list is read like a table, and the output is always a table.
|
||||
The head and the dash line stay at the top while the rows scroll.
|
||||
.SH Keys
|
||||
.nf
|
||||
Up, Down move the cursor one row
|
||||
PgUp, PgDn move a page
|
||||
Home, End first or last row
|
||||
Enter choose and finish
|
||||
Space mark the row, move down (with -m)
|
||||
a mark all shown rows; again, unmark them (with -m)
|
||||
/ filter: type text, shown rows must contain it
|
||||
(Enter keeps the filter, Esc clears it)
|
||||
Esc, q, Ctrl-C cancel
|
||||
.fi
|
||||
.PP
|
||||
.B pick
|
||||
writes the marked rows in table order.
|
||||
If no row is marked, it writes the row under the cursor.
|
||||
Marks stay when a filter hides the row.
|
||||
If the user cancels, or the filter leaves no row to choose,
|
||||
.B pick
|
||||
writes nothing and exits with status 1.
|
||||
.SH Options
|
||||
.IP \fB-m\fR 0.5i
|
||||
Allow several rows: Space marks, \fBa\fR marks all.
|
||||
Without it, Enter chooses the row under the cursor.
|
||||
.IP "\fB-k\fR \fIkeys\fR"
|
||||
Take the keys from this string instead of the terminal.
|
||||
Words are key names:
|
||||
.BR up ,
|
||||
.BR down ,
|
||||
.BR pgup ,
|
||||
.BR pgdn ,
|
||||
.BR home ,
|
||||
.BR end ,
|
||||
.BR enter ,
|
||||
.BR space ,
|
||||
.BR esc ,
|
||||
.BR backspace ;
|
||||
any other word is typed character by character.
|
||||
This is for scripts, demonstrations and tests.
|
||||
.SH Example
|
||||
.DM
|
||||
pick -m -k 'down space down down space enter' < inventory
|
||||
.DE
|
||||
.PP
|
||||
marks the second and the fifth row and prints:
|
||||
.DM
|
||||
Item Amount Cost Value Description
|
||||
---- ------ ---- ------ --------------
|
||||
2 100 0.50 50.00 test tubes
|
||||
5 99 2.45 242.55 cleaning cloth
|
||||
.DE
|
||||
.SH "See Also"
|
||||
.B
|
||||
display,
|
||||
mask,
|
||||
row,
|
||||
search
|
||||
.R
|
||||
.SO rdb
|
||||
@@ -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"]
|
||||
|
||||
+221
-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)
|
||||
@@ -618,3 +683,158 @@ class MaskTest < RdbCase
|
||||
assert_equal '', table
|
||||
end
|
||||
end
|
||||
|
||||
class PickTest < RdbCase
|
||||
def session(multi: true, height: 3)
|
||||
t = Rdb::Table.read(File.open(File.join(DEMO, 'inventory')))
|
||||
Rdb::Pick::Session.new(t.head, t.rows, multi: multi).tap { |x| x.resize(height, 80) }
|
||||
end
|
||||
|
||||
def keys(sess, words) = Rdb::Pick.script(words).map { |k| sess.feed(k) }.compact.last
|
||||
|
||||
def test_mark_and_select
|
||||
s = session
|
||||
assert_equal :accept, keys(s, 'down space down down space enter')
|
||||
assert_equal %w[2 5], s.selected.map(&:first)
|
||||
end
|
||||
|
||||
def test_single_mode_takes_the_cursor_row_and_ignores_space
|
||||
s = session(multi: false)
|
||||
assert_equal :accept, keys(s, 'down down space enter')
|
||||
assert_equal %w[3], s.selected.map(&:first)
|
||||
end
|
||||
|
||||
def test_scrolling_keeps_the_cursor_in_view
|
||||
s = session(height: 3)
|
||||
keys(s, 'down down down down')
|
||||
assert_equal [4, 2], [s.cursor, s.top]
|
||||
keys(s, 'end')
|
||||
assert_equal [6, 4], [s.cursor, s.top]
|
||||
keys(s, 'pgup')
|
||||
assert_equal 3, s.cursor
|
||||
keys(s, 'home')
|
||||
assert_equal [0, 0], [s.cursor, s.top]
|
||||
end
|
||||
|
||||
def test_filter_and_clear
|
||||
s = session
|
||||
keys(s, '/clam')
|
||||
assert_predicate s, :typing?
|
||||
keys(s, 'enter')
|
||||
assert_equal [2], s.visible
|
||||
keys(s, '/ esc')
|
||||
assert_equal 7, s.visible.size
|
||||
end
|
||||
|
||||
def test_marks_survive_a_filter_and_a_all_toggles
|
||||
s = session
|
||||
keys(s, 'space /scales enter')
|
||||
assert_equal [0], s.marked
|
||||
keys(s, 'a')
|
||||
assert_equal [0, 6], s.marked.sort
|
||||
keys(s, 'a')
|
||||
assert_equal [0], s.marked
|
||||
end
|
||||
|
||||
def test_cancel_and_empty_choice
|
||||
assert_equal :cancel, keys(session, 'esc')
|
||||
assert_equal :cancel, keys(session, '/zzz enter enter')
|
||||
end
|
||||
|
||||
def test_command_with_keys_script_is_a_filter
|
||||
out = rdb('pick', '-m', '-k', 'down space down down space enter', file: 'inventory')
|
||||
assert_equal %w[2 5], data(out).map(&:first)
|
||||
assert_equal '292.55', data(rdb('total', 'Value', stdin: out)).first[3]
|
||||
end
|
||||
|
||||
def test_command_cancelled_exits_1_with_no_output
|
||||
out, _err, st = run3('pick', '-k', 'esc', stdin: File.read(File.join(@dir, 'inventory')))
|
||||
refute st.success?
|
||||
assert_equal '', out
|
||||
end
|
||||
|
||||
def test_command_reads_a_list
|
||||
out = rdb('pick', '-k', 'down enter', stdin: rdb('tabletolist', file: 'owners'))
|
||||
assert_equal [%w[3 bob]], data(out)
|
||||
end
|
||||
|
||||
def run_on_pty(*key_strokes)
|
||||
out = File.join(@dir, 'pick.out')
|
||||
result = nil
|
||||
PTY.spawn({ 'PATH' => "#{BIN}:#{ENV['PATH']}" }, 'sh', '-c', "cd #{@dir} && pick -m < inventory > pick.out") do |r, w, pid|
|
||||
sleep 0.5
|
||||
key_strokes.each do |k|
|
||||
w.write k
|
||||
sleep 0.1
|
||||
r.read_nonblock(65_536) rescue nil
|
||||
end
|
||||
sleep 0.3
|
||||
r.read_nonblock(65_536) rescue nil
|
||||
_, st = Process.wait2(pid)
|
||||
result = [File.exist?(out) ? File.read(out) : '', st.exitstatus]
|
||||
end
|
||||
result
|
||||
end
|
||||
|
||||
def test_pick_on_a_terminal
|
||||
table, status = run_on_pty("\e[B", ' ', "\e[B", ' ', "\r")
|
||||
assert_equal 0, status
|
||||
assert_equal %w[2 4], data(table).map(&:first)
|
||||
end
|
||||
|
||||
def test_pick_on_a_terminal_cancel
|
||||
table, status = run_on_pty("\e[B", "\e")
|
||||
assert_equal 1, status
|
||||
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