Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80941b6f03 | ||
|
|
a02e5cb109 | ||
|
|
a9f3c0b255 | ||
|
|
2e03c0a440 | ||
|
|
6b9810b075 | ||
|
|
b11e3da453 | ||
|
|
1c66773ff4 | ||
|
|
e1bdb6c703 | ||
|
|
777a3b56e2 | ||
|
|
d5286c3600 | ||
|
|
f83d1bd6f8 | ||
|
|
6c0d675c5d | ||
|
|
d2c4343136 | ||
|
|
e7a7577900 | ||
|
|
837a73366d |
@@ -7,6 +7,39 @@ folgen [Semantic Versioning](https://semver.org/lang/de/). Die Version steht in
|
|||||||
|
|
||||||
## [Unveröffentlicht]
|
## [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
|
||||||
|
- `mask`: Eingabemaske mit eigener Oberfläche (Felder mit Breite, Schaltflächen, Rahmen `-b`, `-s` zeigt das Bild ohne Terminal). Schreibt die Eingabe als Tabelle auf die Standardausgabe. Die Vorlage kommt wie bei `screen` von der Standardeingabe (`mask < entry`). Dazu `man/mask.man`, `demo/maskform` und Tests (auch mit Pseudo-Terminal).
|
||||||
|
- `demo/entry`: beschriftete Eingabemaske für `screen` (README erklärt den Unterschied zu `form`).
|
||||||
|
|
||||||
## [0.3.0] - 2026-10-11
|
## [0.3.0] - 2026-10-11
|
||||||
|
|
||||||
### Hinzugefügt
|
### 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,
|
Datei beginnt mit einer Leerzeile). `tabletolist` und `listtotable` wandeln um,
|
||||||
die meisten Befehle lesen beides.
|
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
|
`row`, `compute` und `validate` übergeben ihren Ausdruck an awk, nachdem sie
|
||||||
Spaltennamen durch `$N` ersetzt haben. Es gilt also awk-Syntax.
|
Spaltennamen durch `$N` ersetzt haben. Es gilt also awk-Syntax.
|
||||||
|
|
||||||
@@ -77,6 +79,88 @@ Eine Zeile mit `##` erscheint als Überschrift in der Ausgabe, eine mit `#` ist
|
|||||||
ein stiller Kommentar. Neue Befehle brauchen hier ein Beispiel, sonst schlägt der Test
|
ein stiller Kommentar. Neue Befehle brauchen hier ein Beispiel, sonst schlägt der Test
|
||||||
`test_testall_mentions_every_command` fehl.
|
`test_testall_mentions_every_command` fehl.
|
||||||
|
|
||||||
|
## Vorlagen: `form` und `entry`
|
||||||
|
|
||||||
|
- `demo/form` ist eine Vorlage für `report` und `letter`: sie setzt Spaltenwerte in einen Text ein.
|
||||||
|
Sie hat keine Beschriftungen und nennt `Name` zweimal.
|
||||||
|
- `demo/entry` ist eine Eingabemaske für `screen`: jedes Feld kommt einmal vor und hat eine
|
||||||
|
Beschriftung. `screen < entry > maske.sh` erzeugt das Skript. Starten:
|
||||||
|
`CLEAR=$(clear.rdb) sh maske.sh > tabelle 2>&1`. Die Tabelle geht auf stderr (wie im Original),
|
||||||
|
daher das `2>&1`. Mit `form` würde die Maske leer und doppelt aussehen.
|
||||||
|
|
||||||
|
## Eingabemasken: `mask`
|
||||||
|
|
||||||
|
`mask` ist die Variante von `screen` mit eigener Oberfläche (nur Ruby, kein ncurses). Es zeichnet die
|
||||||
|
Vorlage (von der Standardeingabe, wie `screen`; ein Dateiname geht auch) selbst und schreibt die Eingabe als Tabelle auf die Standardausgabe, also pipefähig:
|
||||||
|
|
||||||
|
```
|
||||||
|
mask -b < demo/maskform Rahmen (-b), Felder, Schaltflächen
|
||||||
|
mask < demo/maskform > neu Tabelle in eine Datei
|
||||||
|
mask < demo/maskform | append -h kunden oder direkt in eine Tabelle
|
||||||
|
mask -s < demo/maskform nur das Bild und die Feldpositionen, ohne Terminal
|
||||||
|
```
|
||||||
|
|
||||||
|
In der Vorlage: `<Spalte>` oder `<Spalte:20>` ist ein Feld (mit Breite), `[ Speichern ]` eine Schaltfläche,
|
||||||
|
`[ 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
|
## Befehle
|
||||||
|
|
||||||
| Gruppe | Befehle |
|
| Gruppe | Befehle |
|
||||||
@@ -91,7 +175,7 @@ ein stiller Kommentar. Neue Befehle brauchen hier ein Beispiel, sonst schlägt d
|
|||||||
| Text | `cap` `uppercase` `lowercase` `substitute` `trimblank` `padstring` `length` `filesize` `ascii` `chr` `word` `prompt` `not` `path` |
|
| 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` |
|
| Umwandeln | `tabletolist` `listtotable` `listtosh` `tabletofix` `fixtotable` `tabletostruct` `tabletotbl` `tabletofact` `tabletorule` `tabletom4` `tabletosed` `translate` `uniondict` |
|
||||||
| Datum, Namen | `julian` `gregorian` `computedate` `todaysdate` `soundex` `like` `hashkey` |
|
| Datum, Namen | `julian` `gregorian` `computedate` `todaysdate` `soundex` `like` `hashkey` |
|
||||||
| Berichte | `report` `reportwriter` `screen` `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` |
|
| Terminal, Hilfe | `termput` `tset` `clear.rdb` `cursor` `menu` `rdb` `commands` `helpme` `whatis` `whatwill` `howmany` `man` |
|
||||||
| Betrieb | `testall` `testsearch` `timesearch` `rmcore` `cpdir.rdb` |
|
| 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` |
|
| Buchhaltung (/act) | `act` `chartdup` `foot` `adjust` `balance` `close` `consolidate` `getjournal` `fillform` `calculate` `tax` `cashflow` `calcpay` `w2` `bom` `explode` `sale` `invoice` `cstate` `update.inv` |
|
||||||
@@ -135,6 +219,10 @@ er nicht mehr passt (außer mit `-a`). Beide halten die Indizes aktuell.
|
|||||||
`row`, `compute`, `column`, `sorttable`, `jointable`, `rename`, `validate` und
|
`row`, `compute`, `column`, `sorttable`, `jointable`, `rename`, `validate` und
|
||||||
`rdb` haben dazu den Abschnitt „Case of Column Names“; er ist die einzige
|
`rdb` haben dazu den Abschnitt „Case of Column Names“; er ist die einzige
|
||||||
Ergänzung gegenüber den Original-Seiten.
|
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.
|
- `jointable` braucht keine vorsortierten Tabellen.
|
||||||
- `ve` ist ein zeilenorientierter Editor, kein Bildschirmeditor.
|
- `ve` ist ein zeilenorientierter Editor, kein Bildschirmeditor.
|
||||||
- `soundex` nutzt den Standard-Algorithmus. Die Beispielwerte im Handbuch
|
- `soundex` nutzt den Standard-Algorithmus. Die Beispielwerte im Handbuch
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
Mail List Entry Form
|
||||||
|
|
||||||
|
Name : <Name> Company: <Company>
|
||||||
|
Street: <Street>
|
||||||
|
City : <City> State : <State> ZIP: <ZIP>
|
||||||
|
Phone : <Phone>
|
||||||
|
|
||||||
|
[Speichern] [Abbrechen]
|
||||||
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
: paint crt screen
|
||||||
|
exec 1>/dev/tty
|
||||||
|
cat <<SCREEN
|
||||||
|
${CLEAR} Mail List Entry Form
|
||||||
|
|
||||||
|
Name : Company:
|
||||||
|
Street:
|
||||||
|
City : State : ZIP:
|
||||||
|
Phone :
|
||||||
|
SCREEN
|
||||||
|
: read user input
|
||||||
|
cursor 3 9 ; read Name;
|
||||||
|
cursor 3 37 ; read Company;
|
||||||
|
cursor 4 9 ; read Street;
|
||||||
|
cursor 5 9 ; read City;
|
||||||
|
cursor 5 37 ; read State;
|
||||||
|
cursor 5 56 ; read ZIP;
|
||||||
|
cursor 6 9 ; read Phone;
|
||||||
|
: output table head
|
||||||
|
exec 1>&2
|
||||||
|
echo "Name Company Street City State ZIP Phone"
|
||||||
|
echo "---- ------- ------ ---- ----- --- -----"
|
||||||
|
: append row
|
||||||
|
echo "$Name $Company $Street $City $State $ZIP $Phone"
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
Mail List Entry
|
||||||
|
|
||||||
|
Name : <Name:24>
|
||||||
|
Company: <Company:24>
|
||||||
|
Street : <Street:24>
|
||||||
|
City : <City:16> State: <State:10> ZIP: <ZIP:9>
|
||||||
|
Phone : <Phone:16>
|
||||||
|
|
||||||
|
[ Save ] [ Cancel ]
|
||||||
@@ -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
|
||||||
+44
-3
@@ -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 (project): pick columns in any order; a name that does not exist becomes a new empty column
|
||||||
column Description Cost < inventory
|
column Description Cost < inventory
|
||||||
project Item Notes < owners
|
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: -n numeric, -r reverse, -f fold case
|
||||||
sorttable -nr Cost < inventory
|
sorttable -nr Cost < inventory
|
||||||
## compute: calculate columns from other columns (the new column must exist, so make it with addcol)
|
## 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 / Number: number the rows (number: rows only, Number: rows and the head)
|
||||||
number < owners
|
number < owners
|
||||||
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 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
|
## rmblank: drop empty rows; headon / headoff: add or strip the head
|
||||||
column Item Cost < inventory | rmblank
|
column Item Cost < inventory | rmblank
|
||||||
headoff < colors
|
headoff < colors
|
||||||
@@ -182,8 +191,16 @@ report form < maillist
|
|||||||
## label prints mailing labels, letter prints form letters
|
## label prints mailing labels, letter prints form letters
|
||||||
label < maillist
|
label < maillist
|
||||||
letter form < maillist | head -5
|
letter form < maillist | head -5
|
||||||
## screen turns a form into a shell script that asks for the fields
|
## screen turns a form into a shell script that asks for the fields. Use a form with labels
|
||||||
screen < form | head -6
|
## and every column once (entry); the report template `form` would give a blank, doubled screen.
|
||||||
|
## Run the script with: CLEAR=$(clear.rdb) sh script > table 2>&1 (the table goes to stderr)
|
||||||
|
cat entry
|
||||||
|
screen < entry
|
||||||
|
## mask draws the form itself (fields, buttons, frame) and writes the row as a table on stdout.
|
||||||
|
## It needs a terminal, so here only the picture of the form and its fields (-s) is shown.
|
||||||
|
## Try it: mask -b < maskform mask < maskform | append -h t
|
||||||
|
cat maskform
|
||||||
|
mask -s -b < maskform
|
||||||
## reportwriter: a sample report with page headings (the date is cut out)
|
## reportwriter: a sample report with page headings (the date is cut out)
|
||||||
reportwriter -3 Prices < inventory | grep -v 'Report Date'
|
reportwriter -3 Prices < inventory | grep -v 'Report Date'
|
||||||
## tabletom4 / tabletosed / tabletostruct were shown above
|
## tabletom4 / tabletosed / tabletostruct were shown above
|
||||||
@@ -215,6 +232,17 @@ act
|
|||||||
cashflow < cash
|
cashflow < cash
|
||||||
## tax: look up the tax for an income in a tax table
|
## tax: look up the tax for an income in a tax table
|
||||||
tax 20000 < X83
|
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: parts list -> all subparts with counts (part, amount)
|
||||||
explode 10001 2 parttable
|
explode 10001 2 parttable
|
||||||
## chartdup: look for duplicate account numbers and short names in chart
|
## chartdup: look for duplicate account numbers and short names in chart
|
||||||
@@ -257,3 +285,16 @@ bom ; cat bom
|
|||||||
## update.inv: change the stock of one item, with record locking (INV names the table)
|
## 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
|
cp inventory inv ; printf '7\n9\n\n' | INV=inv update.inv | tail -n +2
|
||||||
grep '^7' inv
|
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 $?"
|
||||||
|
|||||||
+154
-8
@@ -28,6 +28,23 @@ Item Notes
|
|||||||
1
|
1
|
||||||
3
|
3
|
||||||
6
|
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: -n numeric, -r reverse, -f fold case
|
||||||
$ sorttable -nr Cost < inventory
|
$ sorttable -nr Cost < inventory
|
||||||
Item Amount Cost Value Description
|
Item Amount Cost Value Description
|
||||||
@@ -86,11 +103,29 @@ Number Item Owner
|
|||||||
1 1 ann
|
1 1 ann
|
||||||
2 3 bob
|
2 3 bob
|
||||||
3 6 cy
|
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
|
$ record 3 < inventory
|
||||||
Item Amount Cost Value Description
|
Item Amount Cost Value Description
|
||||||
---- ------ ---- ----- -----------
|
---- ------ ---- ----- -----------
|
||||||
3 5 8.00 40.00 clamps
|
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
|
## rmblank: drop empty rows; headon / headoff: add or strip the head
|
||||||
$ column Item Cost < inventory | rmblank
|
$ column Item Cost < inventory | rmblank
|
||||||
Item Cost
|
Item Cost
|
||||||
@@ -735,14 +770,82 @@ McDonalds
|
|||||||
123 Mac Attack
|
123 Mac Attack
|
||||||
Memphis, TENN 30000
|
Memphis, TENN 30000
|
||||||
|
|
||||||
## screen turns a form into a shell script that asks for the fields
|
## screen turns a form into a shell script that asks for the fields. Use a form with labels
|
||||||
$ screen < form | head -6
|
## and every column once (entry); the report template `form` would give a blank, doubled screen.
|
||||||
|
## Run the script with: CLEAR=$(clear.rdb) sh script > table 2>&1 (the table goes to stderr)
|
||||||
|
$ cat entry
|
||||||
|
Mail List Entry Form
|
||||||
|
|
||||||
|
Name : <Name> Company: <Company>
|
||||||
|
Street: <Street>
|
||||||
|
City : <City> State : <State> ZIP: <ZIP>
|
||||||
|
Phone : <Phone>
|
||||||
|
|
||||||
|
[Speichern] [Abbrechen]
|
||||||
|
|
||||||
|
$ screen < entry
|
||||||
: paint crt screen
|
: paint crt screen
|
||||||
exec 1>/dev/tty
|
exec 1>/dev/tty
|
||||||
cat <<SCREEN
|
cat <<SCREEN
|
||||||
${CLEAR}
|
${CLEAR} Mail List Entry Form
|
||||||
|
|
||||||
|
Name : Company:
|
||||||
|
Street:
|
||||||
|
City : State : ZIP:
|
||||||
|
Phone :
|
||||||
|
|
||||||
|
[Speichern] [Abbrechen]
|
||||||
|
SCREEN
|
||||||
|
: read user input
|
||||||
|
cursor 3 9 ; read Name;
|
||||||
|
cursor 3 37 ; read Company;
|
||||||
|
cursor 4 9 ; read Street;
|
||||||
|
cursor 5 9 ; read City;
|
||||||
|
cursor 5 37 ; read State;
|
||||||
|
cursor 5 56 ; read ZIP;
|
||||||
|
cursor 6 9 ; read Phone;
|
||||||
|
: output table head
|
||||||
|
exec 1>&2
|
||||||
|
echo "Name Company Street City State ZIP Phone"
|
||||||
|
echo "---- ------- ------ ---- ----- --- -----"
|
||||||
|
: append row
|
||||||
|
echo "$Name $Company $Street $City $State $ZIP $Phone"
|
||||||
|
## mask draws the form itself (fields, buttons, frame) and writes the row as a table on stdout.
|
||||||
|
## It needs a terminal, so here only the picture of the form and its fields (-s) is shown.
|
||||||
|
## Try it: mask -b < maskform mask < maskform | append -h t
|
||||||
|
$ cat maskform
|
||||||
|
Mail List Entry
|
||||||
|
|
||||||
|
Name : <Name:24>
|
||||||
|
Company: <Company:24>
|
||||||
|
Street : <Street:24>
|
||||||
|
City : <City:16> State: <State:10> ZIP: <ZIP:9>
|
||||||
|
Phone : <Phone:16>
|
||||||
|
|
||||||
|
[ Save ] [ Cancel ]
|
||||||
|
$ mask -s -b < maskform
|
||||||
|
┌──────────────────────────────────────────────────────────────┐
|
||||||
|
│ Mail List Entry │
|
||||||
|
│ │
|
||||||
|
│ Name : ________________________ │
|
||||||
|
│ Company: ________________________ │
|
||||||
|
│ Street : ________________________ │
|
||||||
|
│ City : ________________ State: __________ ZIP: _________ │
|
||||||
|
│ Phone : ________________ │
|
||||||
|
│ │
|
||||||
|
│ [ Save ] [ Cancel ] │
|
||||||
|
└──────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
Fields:
|
||||||
|
Name row 4 col 12 width 24
|
||||||
|
Company row 5 col 12 width 24
|
||||||
|
Street row 6 col 12 width 24
|
||||||
|
City row 7 col 12 width 16
|
||||||
|
State row 7 col 37 width 10
|
||||||
|
ZIP row 7 col 54 width 9
|
||||||
|
Phone row 8 col 12 width 16
|
||||||
|
[Save] row 10 col 6
|
||||||
|
[Cancel] row 10 col 17 (cancels)
|
||||||
## reportwriter: a sample report with page headings (the date is cut out)
|
## reportwriter: a sample report with page headings (the date is cut out)
|
||||||
$ reportwriter -3 Prices < inventory | grep -v 'Report Date'
|
$ reportwriter -3 Prices < inventory | grep -v 'Report Date'
|
||||||
Prices
|
Prices
|
||||||
@@ -777,9 +880,9 @@ Item Amount Cost Value Description
|
|||||||
## tabletom4 / tabletosed / tabletostruct were shown above
|
## tabletom4 / tabletosed / tabletostruct were shown above
|
||||||
## ===== Help and the terminal =====
|
## ===== Help and the terminal =====
|
||||||
$ rdb --version
|
$ rdb --version
|
||||||
rdb 0.2.0
|
rdb 0.7.0
|
||||||
$ howmany
|
$ howmany
|
||||||
134
|
136
|
||||||
$ whatis row
|
$ whatis row
|
||||||
row - make a new table where rows match logical condition
|
row - make a new table where rows match logical condition
|
||||||
row row 'column condition column-or-value ..' < table
|
row row 'column condition column-or-value ..' < table
|
||||||
@@ -867,6 +970,25 @@ Date Amount Balance Description
|
|||||||
## tax: look up the tax for an income in a tax table
|
## tax: look up the tax for an income in a tax table
|
||||||
$ tax 20000 < X83
|
$ tax 20000 < X83
|
||||||
3369
|
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: parts list -> all subparts with counts (part, amount)
|
||||||
$ explode 10001 2 parttable
|
$ explode 10001 2 parttable
|
||||||
Part Amount
|
Part Amount
|
||||||
@@ -1039,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.)
|
Amount Amount: Item Number: (or hit Return key to exit.)
|
||||||
$ grep '^7' inv
|
$ grep '^7' inv
|
||||||
7 9 175.00 1575 scales
|
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,275 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'io/console'
|
||||||
|
require 'open3'
|
||||||
|
|
||||||
|
# mask -- a full-screen input form. It reads the same kind of form as `screen`
|
||||||
|
# (text with <Column> fields) but draws it itself and writes the row typed in as
|
||||||
|
# a table on the standard output, so it works in a pipe: mask < entry | append t
|
||||||
|
#
|
||||||
|
# Besides <Column> the form may contain
|
||||||
|
# <Column:20> a field 20 characters wide (otherwise as wide as the <...>)
|
||||||
|
# [ Save ] a button; Cancel, Abort, Quit or Abbrechen cancel, any other accepts
|
||||||
|
# <!command!> replaced by the first line of the command's output
|
||||||
|
# Frames are plain text in the form, or drawn around everything with -b.
|
||||||
|
module Rdb
|
||||||
|
module Mask
|
||||||
|
Slot = Struct.new(:row, :col, :width, :name)
|
||||||
|
Button = Struct.new(:row, :col, :text, :label)
|
||||||
|
CANCEL = /\A(cancel|abort|quit|abbrechen)\z/i
|
||||||
|
TOKEN = /<!(.*?)!>|<([^<>!:\n]+?)(?::(\d+))?>|\[ *([^\[\]<>\n]*[^\[\]<>\s]) *\]/
|
||||||
|
|
||||||
|
class Form
|
||||||
|
attr_reader :lines, :slots, :buttons
|
||||||
|
|
||||||
|
def self.parse(text, frame: false)
|
||||||
|
lines = []
|
||||||
|
slots = []
|
||||||
|
buttons = []
|
||||||
|
text.each_line(chomp: true).with_index do |line, row|
|
||||||
|
out = +''
|
||||||
|
pos = 0
|
||||||
|
while (m = TOKEN.match(line, pos))
|
||||||
|
out << line[pos...m.begin(0)]
|
||||||
|
if m[1]
|
||||||
|
out << Open3.capture2('sh', '-c', m[1]).first.lines.first.to_s.chomp
|
||||||
|
elsif m[2]
|
||||||
|
width = m[3] ? m[3].to_i : m[0].size
|
||||||
|
slots << Slot.new(row, out.size, width, m[2].strip)
|
||||||
|
out << ' ' * width
|
||||||
|
else
|
||||||
|
buttons << Button.new(row, out.size, m[0], m[4].strip)
|
||||||
|
out << m[0]
|
||||||
|
end
|
||||||
|
pos = m.end(0)
|
||||||
|
end
|
||||||
|
lines << (out << line[pos..].to_s)
|
||||||
|
end
|
||||||
|
new(lines, slots, buttons).then { |f| frame ? f.framed : f }
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(lines, slots, buttons)
|
||||||
|
@lines = lines
|
||||||
|
@slots = slots
|
||||||
|
@buttons = buttons
|
||||||
|
end
|
||||||
|
|
||||||
|
def names
|
||||||
|
@slots.map(&:name).uniq
|
||||||
|
end
|
||||||
|
|
||||||
|
# Fields (first slot of each column) and buttons in reading order.
|
||||||
|
def items
|
||||||
|
fields = @slots.uniq(&:name).map { |s| [:field, s] }
|
||||||
|
(fields + @buttons.map { |b| [:button, b] }).sort_by { |_, o| [o.row, o.col] }
|
||||||
|
end
|
||||||
|
|
||||||
|
def framed
|
||||||
|
w = @lines.map(&:size).max.to_i
|
||||||
|
lines = ["┌#{'─' * (w + 2)}┐"] + @lines.map { |l| "│ #{l.ljust(w)} │" } + ["└#{'─' * (w + 2)}┘"]
|
||||||
|
self.class.new(lines,
|
||||||
|
@slots.map { |s| Slot.new(s.row + 1, s.col + 2, s.width, s.name) },
|
||||||
|
@buttons.map { |b| Button.new(b.row + 1, b.col + 2, b.text, b.label) })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# The state of one input session: values, which item has the focus, and the keys.
|
||||||
|
class Session
|
||||||
|
attr_reader :values, :focus, :pos
|
||||||
|
|
||||||
|
def initialize(form)
|
||||||
|
@form = form
|
||||||
|
@items = form.items
|
||||||
|
@values = form.names.to_h { |n| [n, +''] }
|
||||||
|
@focus = 0
|
||||||
|
reset_pos
|
||||||
|
end
|
||||||
|
|
||||||
|
def kind = @items[@focus]&.first
|
||||||
|
|
||||||
|
def current = @items[@focus]&.last
|
||||||
|
|
||||||
|
def field? = kind == :field
|
||||||
|
|
||||||
|
# Feed one key; returns :accept or :cancel when the session ends.
|
||||||
|
def feed(key)
|
||||||
|
case key
|
||||||
|
when "\t", "\e[B", "\e[6~" then move(1)
|
||||||
|
when "\e[Z", "\e[A", "\e[5~" then move(-1)
|
||||||
|
when "\r", "\n" then return enter
|
||||||
|
when "\x03", "\e", "\x04" then return :cancel
|
||||||
|
when "\e[C" then @pos = [@pos + 1, text.size].min if field?
|
||||||
|
when "\e[D" then @pos = [@pos - 1, 0].max if field?
|
||||||
|
when "\e[H", "\e[1~", "\eOH", "\x01" then @pos = 0
|
||||||
|
when "\e[F", "\e[4~", "\eOF", "\x05" then @pos = text.size if field?
|
||||||
|
when "\x7f", "\b" then backspace
|
||||||
|
when "\e[3~" then delete
|
||||||
|
when "\x15" then clear_field
|
||||||
|
when ' '
|
||||||
|
return press unless field?
|
||||||
|
|
||||||
|
insert(key)
|
||||||
|
else insert(key) if key.size == 1 && key.ord >= 32
|
||||||
|
end
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
# Plain-text picture of the form (no escapes), for `mask -s` and the tests.
|
||||||
|
def plain
|
||||||
|
grid = @form.lines.map(&:dup)
|
||||||
|
@form.slots.each { |s| grid[s.row][s.col, s.width] = '_' * s.width }
|
||||||
|
grid.join("\n") + "\n"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Escape sequences that paint the whole screen.
|
||||||
|
def render
|
||||||
|
out = +"\e[?25l\e[H\e[2J"
|
||||||
|
@form.lines.each_with_index { |l, r| out << "\e[#{r + 1};1H#{l}" }
|
||||||
|
cursor = nil
|
||||||
|
@form.slots.each do |s|
|
||||||
|
focused = field? && current.name == s.name
|
||||||
|
start = focused ? view_start(s.width) : 0
|
||||||
|
shown = @values[s.name][start, s.width].to_s.ljust(s.width)
|
||||||
|
out << "\e[#{s.row + 1};#{s.col + 1}H\e[#{focused ? 7 : 4}m#{shown}\e[0m"
|
||||||
|
cursor = [s.row + 1, s.col + 1 + @pos - start] if focused && current.equal?(s)
|
||||||
|
end
|
||||||
|
@form.buttons.each do |b|
|
||||||
|
focused = kind == :button && current.equal?(b)
|
||||||
|
out << "\e[#{b.row + 1};#{b.col + 1}H\e[#{focused ? 7 : 1}m#{b.text}\e[0m"
|
||||||
|
cursor = [b.row + 1, b.col + 1] if focused
|
||||||
|
end
|
||||||
|
out << "\e[#{cursor[0]};#{cursor[1]}H" if cursor
|
||||||
|
out << "\e[?25h" if field?
|
||||||
|
out
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def text = field? ? @values[current.name] : +''
|
||||||
|
|
||||||
|
def reset_pos
|
||||||
|
@pos = field? ? text.size : 0
|
||||||
|
end
|
||||||
|
|
||||||
|
def move(delta)
|
||||||
|
@focus = (@focus + delta) % [@items.size, 1].max
|
||||||
|
reset_pos
|
||||||
|
end
|
||||||
|
|
||||||
|
def enter
|
||||||
|
return press unless field?
|
||||||
|
|
||||||
|
last = @focus == @items.size - 1
|
||||||
|
return :accept if last && @form.buttons.empty?
|
||||||
|
|
||||||
|
move(1)
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def press
|
||||||
|
current.label.match?(CANCEL) ? :cancel : :accept
|
||||||
|
end
|
||||||
|
|
||||||
|
def insert(key)
|
||||||
|
return unless field?
|
||||||
|
|
||||||
|
text.insert(@pos, key)
|
||||||
|
@pos += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
def backspace
|
||||||
|
return unless field? && @pos.positive?
|
||||||
|
|
||||||
|
text.slice!(@pos - 1)
|
||||||
|
@pos -= 1
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete
|
||||||
|
text.slice!(@pos) if field?
|
||||||
|
end
|
||||||
|
|
||||||
|
def clear_field
|
||||||
|
return unless field?
|
||||||
|
|
||||||
|
text.clear
|
||||||
|
@pos = 0
|
||||||
|
end
|
||||||
|
|
||||||
|
def view_start(width)
|
||||||
|
[@pos - width + 1, 0].max
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.read_key(io)
|
||||||
|
c = io.getch
|
||||||
|
return c unless c == "\e" && io.wait_readable(0.05)
|
||||||
|
|
||||||
|
seq = +"\e#{io.getch}"
|
||||||
|
if seq == "\e["
|
||||||
|
loop do
|
||||||
|
c = io.getch
|
||||||
|
seq << c
|
||||||
|
break if c.ord.between?(0x40, 0x7e)
|
||||||
|
end
|
||||||
|
elsif seq == "\eO"
|
||||||
|
seq << io.getch
|
||||||
|
end
|
||||||
|
seq
|
||||||
|
end
|
||||||
|
|
||||||
|
# Run the form on a terminal; returns the values, or nil when cancelled.
|
||||||
|
def self.run(form, tty)
|
||||||
|
session = Session.new(form)
|
||||||
|
tty.raw do
|
||||||
|
tty.write "\e[?1049h"
|
||||||
|
begin
|
||||||
|
loop do
|
||||||
|
tty.write session.render
|
||||||
|
key = begin
|
||||||
|
read_key(tty)
|
||||||
|
rescue EOFError
|
||||||
|
"\x03"
|
||||||
|
end
|
||||||
|
result = session.feed(key)
|
||||||
|
return (result == :accept ? session.values : nil) if result
|
||||||
|
end
|
||||||
|
ensure
|
||||||
|
tty.write "\e[?25h\e[?1049l"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Rdb.register('mask', 'Full-screen input form that writes a table row', 'mask [-b] [-s] [form] < form > table') do |args|
|
||||||
|
frame = !args.delete('-b').nil?
|
||||||
|
show = !args.delete('-s').nil?
|
||||||
|
die 'usage: mask [-b] [-s] [form] < form > table' if args.size > 1
|
||||||
|
file = args.first
|
||||||
|
file.nil? || file == '-' || File.exist?(file) or die("#{file}: no such form")
|
||||||
|
text = file && file != '-' ? File.read(file) : $stdin.read
|
||||||
|
form = Rdb::Mask::Form.parse(text, frame: frame)
|
||||||
|
die "the form has no <field>" if form.names.empty?
|
||||||
|
|
||||||
|
session = Rdb::Mask::Session.new(form)
|
||||||
|
if show
|
||||||
|
print session.plain
|
||||||
|
puts '', 'Fields:'
|
||||||
|
form.slots.each { |s| puts " #{s.name}\trow #{s.row + 1} col #{s.col + 1} width #{s.width}" }
|
||||||
|
form.buttons.each { |b| puts " [#{b.label}]\trow #{b.row + 1} col #{b.col + 1}#{b.label.match?(Rdb::Mask::CANCEL) ? ' (cancels)' : ''}" }
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
|
tty = begin
|
||||||
|
File.open('/dev/tty', 'r+')
|
||||||
|
rescue SystemCallError
|
||||||
|
die 'no terminal available (mask draws on /dev/tty)'
|
||||||
|
end
|
||||||
|
values = Rdb::Mask.run(form, tty)
|
||||||
|
tty.close
|
||||||
|
if values.nil?
|
||||||
|
warn 'mask: cancelled'
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
Rdb::Table.new(form.names, [form.names.map { |n| values[n].tr("\t\r\n", ' ') }]).write
|
||||||
|
end
|
||||||
@@ -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
|
end
|
||||||
Rdb.alias_for('select', 'row')
|
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
|
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) }
|
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 }
|
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|
|
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 '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
|
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
|
t.with(rows: row ? [row] : []).write
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -160,12 +175,14 @@ end
|
|||||||
Rdb.register('splittable', 'Divide a table horizontally', 'splittable [-n] table') do |args|
|
Rdb.register('splittable', 'Divide a table horizontally', 'splittable [-n] table') do |args|
|
||||||
n = 50
|
n = 50
|
||||||
n = args.shift[1..].to_i if args.first&.match?(/\A-\d+\z/)
|
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])
|
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
|
suffix = ('aa'..'zz').each
|
||||||
t.rows.each_slice(n) do |chunk|
|
chunks.each { |chunk| File.write("#{args[0]}#{suffix.next}", t.with(rows: chunk).to_s) }
|
||||||
File.write("#{args[0]}#{suffix.next}", t.with(rows: chunk).to_s)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# ---- aggregates ------------------------------------------------------------
|
# ---- aggregates ------------------------------------------------------------
|
||||||
|
|||||||
+8
-4
@@ -81,9 +81,10 @@ end
|
|||||||
# ---- manual pages ---------------------------------------------------------------
|
# ---- manual pages ---------------------------------------------------------------
|
||||||
|
|
||||||
def troff_clean(str)
|
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('\\-', '-').gsub('\\&', '').gsub('\\*(UX', 'UNIX').gsub('``', '"').gsub("''", '"').gsub('\\ ', ' ')
|
||||||
.gsub(/\\\(..|\\\*\(..|\\[a-z]/, '').gsub('\\\\', '\\')
|
.gsub(/\\\(..|\\\*\(..|\\[a-z]/, '').gsub("\0", '\\')
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_man(src)
|
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|
|
args.each do |a|
|
||||||
f = File.join(Rdb::ROOT, 'man', "#{a == 'Number' ? 'number.c' : a}.man")
|
f = File.join(Rdb::ROOT, 'man', "#{a == 'Number' ? 'number.c' : a}.man")
|
||||||
File.exist?(f) or die("no manual page for '#{a}'")
|
File.exist?(f) or die("no manual page for '#{a}'")
|
||||||
desc = File.read(f)[/^\.PC\s+"?(.*?)"?\s*$/, 1]
|
src = File.read(f)
|
||||||
puts "#{a}#{desc ? " -- #{troff_clean(desc)}" : ''}", '=' * 60
|
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)), ''
|
puts render_man(File.read(f)), ''
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Rdb
|
module Rdb
|
||||||
VERSION = '0.3.0'
|
VERSION = '0.7.0'
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -129,6 +129,24 @@ Names that are also
|
|||||||
reserved words or are followed by
|
reserved words or are followed by
|
||||||
.B (
|
.B (
|
||||||
are never taken for columns.
|
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"
|
.SH "See Also"
|
||||||
.B
|
.B
|
||||||
project,
|
project,
|
||||||
|
|||||||
+116
@@ -0,0 +1,116 @@
|
|||||||
|
.TH mask "" "" "/rdb Command"
|
||||||
|
.PC "Full-screen input form that writes a table row"
|
||||||
|
\fBmask \fB[ -b ] [ -s ] [ \fIform\fB ] < \fIform \fB> \fItable\fR
|
||||||
|
.PP
|
||||||
|
.II "mask^input form"
|
||||||
|
.II "form^full-screen input"
|
||||||
|
.I "(Ruby re-creation only; the original /rdb has no such command.)"
|
||||||
|
.PP
|
||||||
|
The command
|
||||||
|
.B mask
|
||||||
|
draws
|
||||||
|
.I form
|
||||||
|
on the terminal, lets the user fill in its fields, and writes the result as
|
||||||
|
a one-row table on the standard output.
|
||||||
|
Unlike
|
||||||
|
.BR screen ,
|
||||||
|
it does not write a shell script: it runs the form itself.
|
||||||
|
The screen is drawn on
|
||||||
|
.BR /dev/tty ,
|
||||||
|
so the table can be redirected or piped:
|
||||||
|
.DM
|
||||||
|
mask < maskform > newrow
|
||||||
|
mask < maskform | append -h customer
|
||||||
|
.DE
|
||||||
|
.PP
|
||||||
|
.I form
|
||||||
|
is ordinary text.
|
||||||
|
Everything is shown as it stands, except:
|
||||||
|
.IP "\fB<Column>\fR" 1.2i
|
||||||
|
An input field for column
|
||||||
|
.IR Column .
|
||||||
|
It is as wide as the text between the angle brackets, including the brackets.
|
||||||
|
Blanks around the name are ignored, so \fB< City >\fR is column \fBCity\fR.
|
||||||
|
.IP "\fB<Column:20>\fR"
|
||||||
|
The same, but 20 characters wide.
|
||||||
|
Longer input scrolls inside the field.
|
||||||
|
.IP "\fB[ Save ]\fR"
|
||||||
|
A button.
|
||||||
|
A button whose label is
|
||||||
|
.BR Cancel ,
|
||||||
|
.BR Abort ,
|
||||||
|
.B Quit
|
||||||
|
or
|
||||||
|
.B Abbrechen
|
||||||
|
cancels; any other button accepts.
|
||||||
|
.IP "\fB<!command!>\fR"
|
||||||
|
Replaced by the first line the shell
|
||||||
|
.I command
|
||||||
|
writes.
|
||||||
|
.PP
|
||||||
|
If a column occurs more than once in the form, it is one field,
|
||||||
|
and every place shows what was typed.
|
||||||
|
The table has one column per field, in the order of first appearance.
|
||||||
|
Frames are made of characters in the form, for example \fB+---+\fR or
|
||||||
|
box-drawing characters.
|
||||||
|
.SH Keys
|
||||||
|
.nf
|
||||||
|
Tab, Down, Enter next field or button
|
||||||
|
Shift-Tab, Up previous field or button
|
||||||
|
Left, Right, Home, End move inside a field
|
||||||
|
Backspace, Delete erase; Ctrl-U clears the field
|
||||||
|
Enter, Space on a button: press it
|
||||||
|
Enter on the last field of a form without buttons: accept
|
||||||
|
Esc, Ctrl-C cancel
|
||||||
|
.fi
|
||||||
|
.PP
|
||||||
|
When the user cancels,
|
||||||
|
.B mask
|
||||||
|
writes nothing and exits with status 1.
|
||||||
|
.SH Options
|
||||||
|
.IP \fB-b\fR 0.5i
|
||||||
|
Draw a frame around the whole form.
|
||||||
|
.IP \fB-s\fR
|
||||||
|
Show the form as a plain picture (fields as underscores) with the position of
|
||||||
|
each field and button, and exit.
|
||||||
|
This needs no terminal and is useful to check a form.
|
||||||
|
.PP
|
||||||
|
Like
|
||||||
|
.BR screen ,
|
||||||
|
.B mask
|
||||||
|
reads the form from the standard input.
|
||||||
|
A file name argument is also accepted, and
|
||||||
|
.B -
|
||||||
|
means the standard input.
|
||||||
|
.SH Example
|
||||||
|
Form
|
||||||
|
.BR maskform :
|
||||||
|
.DM
|
||||||
|
Mail List Entry
|
||||||
|
|
||||||
|
Name : <Name:24>
|
||||||
|
City : <City:16> State: <State:10>
|
||||||
|
|
||||||
|
[ Save ] [ Cancel ]
|
||||||
|
.DE
|
||||||
|
.PP
|
||||||
|
.DM
|
||||||
|
mask -b < maskform > newrow
|
||||||
|
.DE
|
||||||
|
.PP
|
||||||
|
After typing ``Ann'' and ``Rome'', pressing Tab to the buttons and Enter on
|
||||||
|
.BR Save ,
|
||||||
|
.B newrow
|
||||||
|
contains:
|
||||||
|
.DM
|
||||||
|
Name City State
|
||||||
|
---- ---- -----
|
||||||
|
Ann Rome
|
||||||
|
.DE
|
||||||
|
.SH "See Also"
|
||||||
|
.B
|
||||||
|
cursor,
|
||||||
|
report,
|
||||||
|
screen
|
||||||
|
.R
|
||||||
|
.SO rdb
|
||||||
@@ -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
|
.DM
|
||||||
Item 1 is rubber gloves
|
Item 1 is rubber gloves
|
||||||
.DE
|
.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"
|
.SH "See Also"
|
||||||
.B
|
.B
|
||||||
row,
|
row,
|
||||||
|
|||||||
+26
@@ -191,6 +191,32 @@ Names that are also
|
|||||||
reserved words or are followed by
|
reserved words or are followed by
|
||||||
.B (
|
.B (
|
||||||
are never taken for columns.
|
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"
|
.SH "See Also"
|
||||||
.B
|
.B
|
||||||
compute,
|
compute,
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
[tools]
|
[tools]
|
||||||
|
"cargo:scryer-prolog" = "0.10"
|
||||||
ruby = "3.4"
|
ruby = "3.4"
|
||||||
|
rust = "1.99"
|
||||||
|
|
||||||
[env]
|
[env]
|
||||||
_.path = ["{{config_root}}/bin"]
|
_.path = ["{{config_root}}/bin"]
|
||||||
|
|||||||
+316
-1
@@ -1,8 +1,69 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require_relative 'test_helper'
|
require_relative 'test_helper'
|
||||||
|
require 'pty'
|
||||||
|
require_relative '../lib/rdb'
|
||||||
|
|
||||||
class SelectionTest < RdbCase
|
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
|
def test_row_numeric_condition
|
||||||
out = rdb('row', 'Cost > 5', file: 'inventory')
|
out = rdb('row', 'Cost > 5', file: 'inventory')
|
||||||
assert_equal %w[3 6 7], data(out).map(&:first)
|
assert_equal %w[3 6 7], data(out).map(&:first)
|
||||||
@@ -427,7 +488,7 @@ end
|
|||||||
|
|
||||||
class HelpTest < RdbCase
|
class HelpTest < RdbCase
|
||||||
def test_whatis_whatwill_commands
|
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_includes rdb('whatwill', 'total'), 'subtotal'
|
||||||
assert_operator data(rdb('commands')).size, :>, 120
|
assert_operator data(rdb('commands')).size, :>, 120
|
||||||
assert_operator rdb('howmany').to_i, :>, 120
|
assert_operator rdb('howmany').to_i, :>, 120
|
||||||
@@ -445,6 +506,12 @@ class HelpTest < RdbCase
|
|||||||
assert_equal "\e[3;8H", rdb('cursor', '3', '8')
|
assert_equal "\e[3;8H", rdb('cursor', '3', '8')
|
||||||
end
|
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
|
def test_testall_mentions_every_command
|
||||||
script = File.read(File.join(DEMO, 'testall.cmds'))
|
script = File.read(File.join(DEMO, 'testall.cmds'))
|
||||||
names = data(rdb('commands')).map(&:first)
|
names = data(rdb('commands')).map(&:first)
|
||||||
@@ -523,3 +590,251 @@ class ActTest < RdbCase
|
|||||||
assert_equal [%w[b]], data(out)
|
assert_equal [%w[b]], data(out)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
class MaskTest < RdbCase
|
||||||
|
def form(text, frame: false) = Rdb::Mask::Form.parse(text, frame: frame)
|
||||||
|
|
||||||
|
def type(session, *keys)
|
||||||
|
keys.each { |k| k.start_with?("\e") || k.size == 1 ? session.feed(k) : k.each_char { |c| session.feed(c) } }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_parse_fields_buttons_and_width
|
||||||
|
f = form("Name: <Name:10> < City >\n[ Save ] [Cancel]\n")
|
||||||
|
assert_equal %w[Name City], f.names
|
||||||
|
assert_equal [0, 6, 10], f.slots[0].to_a.first(3)
|
||||||
|
assert_equal 9, f.slots[1].width # the width of "< City >"
|
||||||
|
assert_equal %w[Save Cancel], f.buttons.map(&:label)
|
||||||
|
assert_equal ["Name: #{' ' * 10} #{' ' * 9}", '[ Save ] [Cancel]'], f.lines
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_command_in_form_and_frame
|
||||||
|
f = form("<!echo hi!> <A>\n", frame: true)
|
||||||
|
assert_equal '│ hi │', f.lines[1]
|
||||||
|
assert_equal [1, 5], f.slots.first.to_a.first(2)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_typing_and_navigation
|
||||||
|
s = Rdb::Mask::Session.new(form("<A:5> <B:5> [ OK ] [ Cancel ]\n"))
|
||||||
|
type(s, 'one', "\t", 'two', "\x7f", "\r")
|
||||||
|
assert_equal({ 'A' => 'one', 'B' => 'tw' }, s.values)
|
||||||
|
assert_equal :button, s.kind
|
||||||
|
assert_equal :accept, s.feed("\r")
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_cancel_by_button_and_escape
|
||||||
|
s = Rdb::Mask::Session.new(form("<A:5> [ Save ] [ Abbrechen ]\n"))
|
||||||
|
s.feed("\e[Z")
|
||||||
|
assert_equal :cancel, s.feed(' ')
|
||||||
|
assert_equal :cancel, Rdb::Mask::Session.new(form("<A>\n")).feed("\e")
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_enter_on_last_field_accepts_without_buttons
|
||||||
|
s = Rdb::Mask::Session.new(form("<A:3> <B:3>\n"))
|
||||||
|
assert_nil s.feed("\r")
|
||||||
|
assert_equal :accept, s.feed("\r")
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_same_column_twice_is_one_field
|
||||||
|
s = Rdb::Mask::Session.new(form("<A:3> <A:3>\n"))
|
||||||
|
type(s, 'xy')
|
||||||
|
assert_equal({ 'A' => 'xy' }, s.values)
|
||||||
|
assert_equal %w[A], form("<A:3> <A:3>\n").names
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_show_prints_picture_without_terminal
|
||||||
|
write('f', "Name: <Name:6>\n[ Save ] [ Cancel ]\n")
|
||||||
|
out = rdb('mask', '-s', file: 'f')
|
||||||
|
assert_includes out, "Name: ______\n[ Save ] [ Cancel ]"
|
||||||
|
assert_includes out, " Name\trow 1 col 7 width 6"
|
||||||
|
assert_includes out, " [Cancel]\trow 2 col 10 (cancels)"
|
||||||
|
assert_equal out, rdb('mask', '-s', 'f') # a file name works too
|
||||||
|
end
|
||||||
|
|
||||||
|
# Drive mask on a pseudo terminal; its table goes to a file, like in a pipe.
|
||||||
|
def run_on_pty(*keys)
|
||||||
|
out = File.join(@dir, 'mask.out')
|
||||||
|
result = nil
|
||||||
|
PTY.spawn({ 'PATH' => "#{BIN}:#{ENV['PATH']}" }, 'sh', '-c', "cd #{@dir} && mask < f > mask.out") do |r, w, pid|
|
||||||
|
sleep 0.5
|
||||||
|
keys.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_mask_on_a_terminal_writes_a_table
|
||||||
|
write('f', "Name: <Name:10>\nCity: <City:10>\n[ Save ] [ Cancel ]\n")
|
||||||
|
table, status = run_on_pty("Ann\r", "Rome\r", "\r")
|
||||||
|
assert_equal 0, status
|
||||||
|
assert_equal "Name\tCity\n----\t----\nAnn\tRome\n", table
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_mask_cancel_writes_nothing
|
||||||
|
write('f', "Name: <Name:10>\n[ Save ] [ Cancel ]\n")
|
||||||
|
table, status = run_on_pty("Ann", "\e[Z", "\r")
|
||||||
|
assert_equal 1, status
|
||||||
|
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