Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80941b6f03 | ||
|
|
a02e5cb109 | ||
|
|
a9f3c0b255 | ||
|
|
2e03c0a440 | ||
|
|
6b9810b075 | ||
|
|
b11e3da453 |
@@ -7,6 +7,16 @@ 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
|
## [0.6.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.
|
||||||
|
|
||||||
@@ -128,6 +130,10 @@ Alleinstehende (Grundtarif). Erzeugt mit `ruby tools/gen_tax_tables.rb`.
|
|||||||
| `taxAT2026` | Österreich, Einkommensteuer | § 33 EStG (0 % bis 13 539 €, dann 20/30/40/48/50/55 %) | exakt, vor Absetzbeträgen |
|
| `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 |
|
| `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 |
|
| `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/taxDE2026 14233
|
||||||
@@ -141,6 +147,20 @@ Kirchensteuer und Soli sind nicht enthalten. Für die Schweiz kommen Kanton und
|
|||||||
höher als der Bund), für Liechtenstein der Gemeindezuschlag: gesamt = Landessteuer × (1 + Zuschlag/100).
|
höher als der Bund), für Liechtenstein der Gemeindezuschlag: gesamt = Landessteuer × (1 + Zuschlag/100).
|
||||||
Ehepaar- und Alleinerziehendentarife fehlen. Das ist keine Steuerberatung.
|
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 |
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -235,6 +238,11 @@ tax 60000 < taxDE2026
|
|||||||
tax 60000 < taxAT2026
|
tax 60000 < taxAT2026
|
||||||
tax 60000 < taxCH2026
|
tax 60000 < taxCH2026
|
||||||
tax 60000 < taxLI2026
|
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
|
||||||
|
|||||||
+27
-1
@@ -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
|
||||||
@@ -863,7 +880,7 @@ 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.6.0
|
rdb 0.7.0
|
||||||
$ howmany
|
$ howmany
|
||||||
136
|
136
|
||||||
$ whatis row
|
$ whatis row
|
||||||
@@ -963,6 +980,15 @@ $ tax 60000 < taxCH2026
|
|||||||
671
|
671
|
||||||
$ tax 60000 < taxLI2026
|
$ tax 60000 < taxLI2026
|
||||||
1396
|
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
|
||||||
|
|||||||
@@ -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 }
|
||||||
@@ -164,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 ------------------------------------------------------------
|
||||||
|
|||||||
+5
-2
@@ -148,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.6.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,
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
+60
-1
@@ -5,6 +5,46 @@ require 'pty'
|
|||||||
require_relative '../lib/rdb'
|
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
|
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|
|
{ '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}"
|
assert_equal [[item]], data(rdb('record', n, file: 'owners')).map { |r| r.first(1) }, "record #{n}"
|
||||||
@@ -448,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
|
||||||
@@ -466,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)
|
||||||
@@ -766,6 +812,19 @@ class TaxTablesTest < RdbCase
|
|||||||
assert_equal 0, tax(15_855, 'taxLI2026')
|
assert_equal 0, tax(15_855, 'taxLI2026')
|
||||||
end
|
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
|
# § 32a EStG: the table is a straight-line approximation of the curved zones
|
||||||
def test_de_2026_follows_the_formula
|
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|
|
[12_348, 14_000, 17_799, 20_000, 33_333, 50_000, 69_878, 100_000, 277_825, 300_000].each do |x|
|
||||||
|
|||||||
@@ -61,3 +61,34 @@ li_rates.each do |limit, rate|
|
|||||||
prev = [limit, rate]
|
prev = [limit, rate]
|
||||||
end
|
end
|
||||||
write_table('taxLI2026', pts)
|
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