Obsah
1. Číselné hodnoty s neomezeným rozsahem a přesností v programovacím jazyku Go: typ Decimal
2. Interní reprezentace numerických hodnot typu Decimal
3. Instalace balíčku shopspring/decimal
4. Nulová hodnota typu Decimal
5. Konstrukce hodnot typu Decimal s využitím inicializační celočíselné hodnoty
6. Konstrukce hodnot typu Decimal s využitím inicializační hodnoty s plovoucí řádovou čárkou
7. Explicitní specifikace hodnoty exponentu při konstrukci hodnot typu Decimal
8. Převod obsahu řetězce na numerickou hodnotu typu Decimal
9. Automatická kontrola, zda naparsování hodnoty z řetězce bylo úspěšné
10. Naparsování hodnoty z řetězce se specifikací nadbytečných znaků v řetězci
11. Základní aritmetické operace s hodnotami typu Decimal
12. Podíl se specifikací způsobu zaokrouhlení výsledku
13. Výpočet faktoriálu s hodnotami typu Decimal
14. Agregační funkce s hodnotami typu Decimal
15. Interní struktura hodnot typu Decimal
16. Výpočet hodnoty π s typem Decimal
17. Rychlost výpočtů s hodnotami typu Decimal
19. Repositář s demonstračními příklady
1. Číselné hodnoty s neomezeným rozsahem a přesností v programovacím jazyku Go: typ Decimal
V seriálu o programovacím jazyce Go jsme se již zabývali způsoby, jakými je možné v tomto programovacím jazyce pracovat s hodnotami, které mají neomezený rozsah a/nebo i přesnost [1] [2]. Připomeňme si, že se pro tento účel používají datové struktury definované ve standardním balíčku math/big; konkrétně se jedná o celá čísla s neomezeným rozsahem (big.Int), zlomky, jejichž čitatele i jmenovatele mají neomezený rozsah (big.Rat) a konečně hodnoty s plovoucí řádovou čárkou (big.Float). Existují ovšem situace, které vyžadují použití odlišných způsobů reprezentace rozsáhlých numerických hodnot. A jedna z možných reprezentací je nabízena v balíčku shopspring/decimal. Jak již název tohoto balíčku naznačuje, nejedná se o součást standardní knihovny, takže je nutná jeho (triviální) instalace.
2. Interní reprezentace numerických hodnot typu Decimal
Hodnoty typu Decimal jsou interně reprezentovány velmi jednoduchou datovou strukturou, která obsahuje pouze dva prvky. Prvním prvkem je hodnota typu big.Int, což je, jak již dobře víme, celočíselná hodnota s neomezeným rozsahem. Druhým prvkem je desítkový exponent reprezentovaný 32bitovou celočíselnou hodnotou int32. Důvod, proč je exponent takto „omezený“ pouze na 32 bitů, je uveden v poznámce:
type Decimal struct { value *big.Int // NOTE(vadim): this must be an int32, because we cast it to float64 during // calculations. If exp is 64 bit, we might lose precision. // If we cared about being able to represent every possible decimal, we // could make exp a *big.Int but it would hurt performance and numbers // like that are unrealistic. exp int32 }
3. Instalace balíčku shopspring/decimal
Samotná instalace balíčku shopspring/decimal je při použití některé novější verze základních nástrojů programovacího jazyka Go jednoduchá. Nejprve si vytvořte prázdný projekt příkazem:
$ go mod init decimal-1
Dále je nutné upravit soubor go.mod, který vznikne předchozím příkazem, do podoby:
module decimal-1 go 1.20 require github.com/shopspring/decimal v1.3.1
Nyní je již možné ve zdrojových souborech projektu provést import balíčku shopspring/decimal a začít používat zde definovaný datový typ Decimal:
package main import ( "fmt" "github.com/shopspring/decimal" ) ... ... ...
4. Nulová hodnota typu Decimal
Již ve standardním balíčku math.big je zajištěno, že explicitně neinicializovaná struktura daného typu big.Int, big.Rat nebo big.Float je považována za nulovou hodnotu, což může zjednodušovat zápis programů. Ostatně si to můžeme velmi snadno ověřit tímto triviálním příkladem, v němž pouze definujeme proměnné daného typu, ovšem bez jejich explicitní inicializace:
package main import ( "fmt" "math/big" ) func main() { var x big.Int var y big.Rat var z big.Float fmt.Println("big.Int ", x.Text(10)) fmt.Println("big.Rat ", y.String()) fmt.Println("big.Float", z.Text('f', 10)) }
S těmito výsledky:
big.Int 0 big.Rat 0/1 big.Float 0.0000000000
Podobně je tomu i u datového typu Decimal, jehož neinicializované proměnné jsou automaticky považovány za nulové hodnoty:
package main import ( "fmt" "github.com/shopspring/decimal" ) func main() { var d decimal.Decimal fmt.Println(d) }
Po překladu a spuštění tohoto demonstračního příkladu se skutečně vypíše nula:
0
5. Konstrukce hodnot typu Decimal s využitím inicializační celočíselné hodnoty
Hodnoty typu Decimal je pochopitelně možné vytvářet a současně i inicializovat (na hodnotu odlišnou od nuly). Pro inicializaci lze použít i celočíselné hodnoty, konkrétně hodnoty typu int32 či int (což je v současnosti na většině architektur vlastně int64). Podívejme se nejdříve na ukázku použití konstruktoru, který akceptuje hodnoty typu int32:
package main import ( "fmt" "github.com/shopspring/decimal" ) func main() { d0 := decimal.NewFromInt32(0) fmt.Println(d0) d1 := decimal.NewFromInt32(1234) fmt.Println(d1) d2 := decimal.NewFromInt32(12345) fmt.Println(d2) d3 := decimal.NewFromInt32(123456) fmt.Println(d3) d4 := decimal.NewFromInt32(1234567) fmt.Println(d4) d5 := decimal.NewFromInt32(12345678) fmt.Println(d5) }
Výsledky vypsané po překladu a spuštění tohoto demonstračního příkladu pravděpodobně nebudou nijak překvapivé:
0 1234 12345 123456 1234567 12345678
Prakticky stejným způsobem, pouze s využitím odlišného konstruktoru, můžeme pro inicializaci datového typu Decimal použít i hodnoty typu int (tedy většinou int64):
package main import ( "fmt" "github.com/shopspring/decimal" ) func main() { d0 := decimal.NewFromInt(0) fmt.Println(d0) d1 := decimal.NewFromInt(123456) fmt.Println(d1) d2 := decimal.NewFromInt(123456789) fmt.Println(d2) d3 := decimal.NewFromInt(12345678900000) fmt.Println(d3) d4 := decimal.NewFromInt(1234567890000000000) fmt.Println(d4) }
S výsledky:
0 123456 123456789 12345678900000 1234567890000000000
6. Konstrukce hodnot typu Decimal s využitím inicializační hodnoty s plovoucí řádovou čárkou
K inicializaci hodnot typu Decimal můžeme pochopitelně použít i hodnotu s plovoucí řádovou čárkou, což v programovacím jazyku Go znamená typy float32 a float64. Na tomto místě je ale dobré si uvědomit, že typ Decimal nedokáže vyjádřit kladné a záporné nekonečno, takže při pokusu o převod těchto hodnot dojde k pádu (panic) celého programu (ovšem víme již, že tento pád lze zachytit).
Nejdříve si ukažme inicializaci s využitím vstupní hodnoty typu float32:
package main import ( "fmt" "math" "github.com/shopspring/decimal" ) func main() { d0 := decimal.NewFromFloat32(0) fmt.Println(d0) d1 := decimal.NewFromFloat32(1e10) fmt.Println(d1) d2 := decimal.NewFromFloat32(1e-10) fmt.Println(d2) // maximální podporovaný exponent d3 := decimal.NewFromFloat32(1.2e38) fmt.Println(d3) // minimální podporovaný exponent d4 := decimal.NewFromFloat32(1.2e-38) fmt.Println(d4) // de facto nula z pohledu float32 d5 := decimal.NewFromFloat32(1.1e-100) fmt.Println(d5) // kladné nekonečno d6 := decimal.NewFromFloat32(float32(math.Inf(1))) fmt.Println(d6) // záporné nekonečno d7 := decimal.NewFromFloat32(float32(math.Inf(-1))) fmt.Println(d7) }
Prvních pět hodnot se převede bez problémů:
0 10000000000 0.0000000001 120000000000000000000000000000000000000 0.000000000000000000000000000000000000012
Šestá hodnota je pro float32 tak malá, že se jedná o nulu:
0
A kladné či záporné nekonečno převést nelze – právě zde dojde k pádu aplikace:
panic: Cannot create a Decimal from +Inf goroutine 1 [running]: github.com/shopspring/decimal.newFromFloat(0xc00010a270?, 0x0?, 0x0?) /home/ptisnovs/go/pkg/mod/github.com/shopspring/decimal@v1.3.1/decimal.go:286 +0x3d4 github.com/shopspring/decimal.NewFromFloat32(0x4d49f8?) /home/ptisnovs/go/pkg/mod/github.com/shopspring/decimal@v1.3.1/decimal.go:281 +0x65 main.main() /home/ptisnovs/src/go-root/article_AA/04_construct_from_float_32/construct_from_float32.go:29 +0x2c5 exit status 2
Podobně můžeme postupovat při použití numerických hodnot reprezentovaných typem float64:
package main import ( "fmt" "math" "github.com/shopspring/decimal" ) func main() { d0 := decimal.NewFromFloat(0) fmt.Println(d0) d1 := decimal.NewFromFloat(1e10) fmt.Println(d1) d2 := decimal.NewFromFloat(1e-10) fmt.Println(d2) // maximální exponent d3 := decimal.NewFromFloat(1.2e308) fmt.Println(d3) // minimální exponent d4 := decimal.NewFromFloat(1.2e-308) fmt.Println(d4) // tato hodnota je již de facto nulová d5 := decimal.NewFromFloat(1.1e-1000) fmt.Println(d5) // kladné nekonečno d6 := decimal.NewFromFloat(math.Inf(1)) fmt.Println(d6) // záporné nekonečno d7 := decimal.NewFromFloat(math.Inf(-1)) fmt.Println(d7) }
Podívejme se nyní na zobrazené výsledky, a to včetně pádu aplikace při pokusu o převedení nekonečné hodnoty na typ Decimal:
0 10000000000 0.0000000001 120000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0.000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000012 0 panic: Cannot create a Decimal from +Inf goroutine 1 [running]: github.com/shopspring/decimal.newFromFloat(0xc00010a270?, 0x0?, 0x0?) /home/ptisnovs/go/pkg/mod/github.com/shopspring/decimal@v1.3.1/decimal.go:286 +0x3d4 github.com/shopspring/decimal.NewFromFloat(0x4d49f8?) /home/ptisnovs/go/pkg/mod/github.com/shopspring/decimal@v1.3.1/decimal.go:262 +0x56 main.main() /home/ptisnovs/src/go-root/article_AA/05_construct_from_float/construct_from_float.go:29 +0x2c5 exit status 2
7. Explicitní specifikace hodnoty exponentu při konstrukci hodnot typu Decimal
Ve druhé kapitole jsme si ukázali, jakým způsobem jsou uloženy hodnoty typu Decimal. Víme tedy, že vlastní celočíselná hodnota je oddělena od exponentu. Zajímavé je, že při konstrukci hodnoty Decimal je možné hodnotu desítkového exponentu explicitně nastavit, konkrétně použitím konstruktoru NewFromFloatWithExponent. Podívejme se nyní na důsledky:
package main import ( "fmt" "github.com/shopspring/decimal" ) func main() { d0 := decimal.NewFromFloatWithExponent(123.456, 0) fmt.Println(d0) d1 := decimal.NewFromFloatWithExponent(123.456, 1) fmt.Println(d1) d2 := decimal.NewFromFloatWithExponent(123.456, 2) fmt.Println(d2) d3 := decimal.NewFromFloatWithExponent(123.456, 3) fmt.Println(d3) d4 := decimal.NewFromFloatWithExponent(123.456, -1) fmt.Println(d4) d5 := decimal.NewFromFloatWithExponent(123.456, -2) fmt.Println(d5) d6 := decimal.NewFromFloatWithExponent(123.456, -3) fmt.Println(d6) d7 := decimal.NewFromFloatWithExponent(123.456, -4) fmt.Println(d7) }
Zajímavé bude sledovat, jak ovlivní předaná hodnota exponentu hodnotu, která převodem na typ Decimal vznikne:
123 120 100 0 123.5 123.46 123.456 123.456
Kladné hodnoty znamenají, že se obecně ztrácí informace o cifrách před desetinnou čárkou/tečkou a záporné hodnoty mohou ovlivnit přesnost za desetinnou čárkou/tečkou. Je to vlastně logické, když si uvědomíme, že hodnota je reprezentována jako celé_číslo×10exponent. Tj. když například budeme vyžadovat použití exponentu==1, bude muset být základ nastaven na 12 a exponent na jedničku (tj. poslední cifra se ztratí).
8. Převod obsahu řetězce na numerickou hodnotu typu Decimal
Velmi často má programátor k dispozici numerickou hodnotu v textové podobě, tj. reprezentovanou řetězcem. Takovou hodnotu lze převést (naparsovat) na typ Decimal přímo, tj. bez mezipřevodu na celá čísla nebo čísla s plovoucí řádovou čárkou. V tom nejjednodušším případě můžeme pro parsing textu na Decimal použít konstruktor nazvaný NewFromString, který ovšem vrací dvě hodnoty – převedené číslo a rozhraní reprezentující chybu při převodu. Pochopitelně pokud k chybě nenastane, bude hodnota chyby nastavena na nil:
package main import ( "fmt" "github.com/shopspring/decimal" ) func main() { d0, err := decimal.NewFromString("-0") fmt.Println(d0, err) fmt.Println() d1, err := decimal.NewFromString("-1234567890.123456789") fmt.Println(d1, err) fmt.Println() d2, err := decimal.NewFromString("1e1000") fmt.Println(d2, err) fmt.Println() d3, err := decimal.NewFromString("1e-1000") fmt.Println(d3, err) fmt.Println() d4, err := decimal.NewFromString("-1234567890e123456") fmt.Println(d4, err) fmt.Println() }
A takto vypadají výsledky (povšimněte si, že chyba je v těchto případech vždy nil):
0 <nil> -1234567890.123456789 <nil> 10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 <nil> 0.0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001 <nil> ... ... ...
Otestovat si můžeme i pokusy o převod řetězců, které číselné hodnoty (v textové podobě) ve skutečnosti neobsahují:
package main import ( "fmt" "github.com/shopspring/decimal" ) func main() { d0, err := decimal.NewFromString("-a") fmt.Println(d0, err) fmt.Println() d1, err := decimal.NewFromString("-x.123456789") fmt.Println(d1, err) fmt.Println() d2, err := decimal.NewFromString("1e100a") fmt.Println(d2, err) fmt.Println() }
V těchto případech získáme chyby s přesnější informací o provedené operaci a problémech, které nastaly:
0 can't convert -a to decimal 0 can't convert -x.123456789 to decimal 0 can't convert 1e100a to decimal: exponent is not numeric
9. Automatická kontrola, zda naparsování hodnoty z řetězce bylo úspěšné
Explicitní kontrola chyby, která může nastat při parsování řetězce, je v jazyce Go (bez podpory syntaxe pro zachytávání výjimek) problematická, resp. přesněji řečeno poněkud nečitelná. Proto se ustálil idiom, že konstruktory popř. jiné funkce existují i ve variantě typicky začínající prefixem Must. Taková varianta nevrací chybovou hodnotu, ale v případě chyby přímo havaruje, tj. vyvolá panic. V knihovně decimal lze při parsingu řetězců použít konstruktor nazvaný RequireFromString (zde není onen idiom dodržen), který se používá následovně:
package main import ( "fmt" "github.com/shopspring/decimal" ) func main() { d0 := decimal.RequireFromString("-a") fmt.Println(d0) fmt.Println() d1 := decimal.RequireFromString("-x.123456789") fmt.Println(d1) fmt.Println() d2 := decimal.RequireFromString("1e100a") fmt.Println(d2) fmt.Println() }
Tento program po svém překladu a spuštění zhavaruje při pokusu o parsing prvního řetězce, takže se další konverze již nebudou provádět:
panic: can't convert -a to decimal goroutine 1 [running]: github.com/shopspring/decimal.RequireFromString(...) /home/ptisnovs/go/pkg/mod/github.com/shopspring/decimal@v1.3.1/decimal.go:243 main.main() /home/ptisnovs/src/go-root/article_AA/12_require_from_string/require_from_string_err.go:10 +0x208 exit status 2
10. Naparsování hodnoty z řetězce se specifikací nadbytečných znaků v řetězci
Mnohdy se taktéž setkáme se situací, kdy vstupní řetězce obsahují nějaké nadbytečné znaky, například oddělovače milionů a tisíců (v tuzemsku mezery, v US pak čárky), symboly měn atd. Tyto znaky je samozřejmě možné explicitně odstranit ještě před konverzí (parsingem) řetězce na hodnotu typu Decimal. Ovšem díky existenci konstruktoru NewFromFormattedString je možné jak odstranění, tak i konverzi provést v jediném kroku. Tomuto konstruktoru se předává jak řetězec, který se má parsovat, tak i regulární výraz (a ten je mimochodem konstruován právě konstruktorem, jehož jméno začíná na Must).
Nejprve si ukažme parsing řetězců obsahujících přímo textovou reprezentaci čísla, z níž se žádné znaky nemusí odstraňovat:
package main import ( "fmt" "regexp" "github.com/shopspring/decimal" ) func main() { r := regexp.MustCompile("neco") d0, err := decimal.NewFromFormattedString("-0", r) fmt.Println(d0, err) fmt.Println() d1, err := decimal.NewFromFormattedString("-1234567890.123456789", r) fmt.Println(d1, err) fmt.Println() d2, err := decimal.NewFromFormattedString("1e1000", r) fmt.Println(d2, err) fmt.Println() d3, err := decimal.NewFromFormattedString("1e-1000", r) fmt.Println(d3, err) fmt.Println() d4, err := decimal.NewFromFormattedString("-1234567890e123", r) fmt.Println(d4, err) fmt.Println() }
Výsledky:
0 <nil> -1234567890.123456789 <nil> 10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 <nil> 0.0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001 <nil> -1234567890000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 <nil>
V dalším příkladu pak parsujeme řetězce, které obsahují „nechtěné“ znaky, které jsou odstraněny:
package main import ( "fmt" "regexp" "github.com/shopspring/decimal" ) func main() { r := regexp.MustCompile(",") d0, err := decimal.NewFromFormattedString("1,000", r) fmt.Println(d0, err) fmt.Println() d1, err := decimal.NewFromFormattedString("1,000,000", r) fmt.Println(d1, err) fmt.Println() d2, err := decimal.NewFromFormattedString("1.000", r) fmt.Println(d2, err) fmt.Println() d3, err := decimal.NewFromFormattedString("1,000.000", r) fmt.Println(d3, err) fmt.Println() }
Výsledky by opět měly odpovídat očekávání:
1000 <nil> 1000000 <nil> 1 <nil> 1000 <nil>
Odstranění symbolů měny:
package main import ( "fmt" "regexp" "github.com/shopspring/decimal" ) func main() { r := regexp.MustCompile("\\$") d0, err := decimal.NewFromFormattedString("$1000", r) fmt.Println(d0, err) fmt.Println() d1, err := decimal.NewFromFormattedString("20.25$", r) fmt.Println(d1, err) fmt.Println() r = regexp.MustCompile("Kč") d2, err := decimal.NewFromFormattedString("1000Kč", r) fmt.Println(d2, err) fmt.Println() }
Vypočtené a zobrazené numerické hodnoty:
1000 <nil> 20.25 <nil> 1000 <nil>
11. Základní aritmetické operace s hodnotami typu Decimal
Datový typ Decimal pochopitelně obsahuje i podporu pro provádění všech pěti základních aritmetických operací (součet, rozdíl, součin, podíl a modulo). Tyto operace se zapisují odlišným způsobem, než je tomu u typů ze základní knihovny big, protože příjemcem je první hodnota a jediným parametrem operace druhá numerická hodnota:
package main import ( "fmt" "github.com/shopspring/decimal" ) func main() { x := decimal.NewFromInt32(2) y := decimal.NewFromInt32(3) r1 := x.Add(y) r2 := x.Sub(y) r3 := x.Mul(y) r4 := x.Div(y) r5 := x.Mod(y) fmt.Println("x ", x) fmt.Println("y ", y) fmt.Println("x+y ", r1) fmt.Println("x-y ", r2) fmt.Println("x*y ", r3) fmt.Println("x/y ", r4) fmt.Println("x%y ", r5) }
Výsledky získané po překladu a spuštění tohoto demonstračního příkladu by neměly být příliš překvapující (snad až na zdánlivě omezenou přesnost hodnoty 2/3):
x 2 y 3 x+y 5 x-y -1 x*y 6 x/y 0.6666666666666667 x%y 2
12. Podíl se specifikací způsobu zaokrouhlení výsledku
Již v předchozí kapitole jsme mohli vidět, že u operace podílu nebyl (a mnohdy ani nemůže být) výsledek zcela přesný. Zajímavé je, že u datového typu Decimal lze přesnost této aritmetické operace zvolit, pokud se namísto metody Div použije metoda nazvaná DivRound, které se navíc předá i další hodnota (typu int32 – viz druhou kapitolu) s požadovanou přesností:
package main import ( "fmt" "github.com/shopspring/decimal" ) func main() { x := decimal.NewFromInt32(1) y := decimal.NewFromInt32(7) for precision := 30; precision > 0; precision-- { r := x.DivRound(y, int32(precision)) fmt.Println(r) } }
Výsledky zobrazené tímto demonstračním příkladem ukazují výpočty podílu s přesností klesající od 30 do 1:
0.142857142857142857142857142857 0.14285714285714285714285714286 0.1428571428571428571428571429 0.142857142857142857142857143 0.14285714285714285714285714 0.1428571428571428571428571 0.142857142857142857142857 0.14285714285714285714286 0.1428571428571428571429 0.142857142857142857143 0.14285714285714285714 0.1428571428571428571 0.142857142857142857 0.14285714285714286 0.1428571428571429 0.142857142857143 0.14285714285714 0.1428571428571 0.142857142857 0.14285714286 0.1428571429 0.142857143 0.14285714 0.1428571 0.142857 0.14286 0.1429 0.143 0.14 0.1
13. Výpočet faktoriálu s hodnotami typu Decimal
V předchozích dvou článcích s popisem datových typů nabízených standardní knihovnou jazyka Go big jsme si ukazovali některé základní numerické algoritmy, například výpočet konstanty π či výpočet faktoriálu. A právě výpočet faktoriálu nad hodnotami typu Decimal je ukázán v této kapitole. I když je opět nutné nahradit zápis aritmetických operací (i operace relační) za volání metod, je výsledek nepatrně čitelnější, než je tomu v případě použití big.Int, a to kvůli odlišné sémantice volání binárních operací:
package main import ( "fmt" "github.com/shopspring/decimal" ) func factorial(n decimal.Decimal) decimal.Decimal { one := decimal.NewFromInt32(1) var zero decimal.Decimal if n.Cmp(zero) <= 0 { return one } return n.Mul(factorial(n.Sub(one))) } func main() { for n := int64(1); n < 80; n++ { f := factorial(decimal.NewFromInt(n)) fmt.Printf("%3d! = %s\n", n, f) } }
Hodnoty faktoriálu vypočtené tímto demonstračním příkladem:
1! = 1 2! = 2 3! = 6 4! = 24 5! = 120 6! = 720 7! = 5040 8! = 40320 9! = 362880 10! = 3628800 ... ... ... 70! = 11978571669969891796072783721689098736458938142546425857555362864628009582789845319680000000000000000 71! = 850478588567862317521167644239926010288584608120796235886430763388588680378079017697280000000000000000 72! = 61234458376886086861524070385274672740778091784697328983823014963978384987221689274204160000000000000000 73! = 4470115461512684340891257138125051110076800700282905015819080092370422104067183317016903680000000000000000 74! = 330788544151938641225953028221253782145683251820934971170611926835411235700971565459250872320000000000000000 75! = 24809140811395398091946477116594033660926243886570122837795894512655842677572867409443815424000000000000000000 76! = 1885494701666050254987932260861146558230394535379329335672487982961844043495537923117729972224000000000000000000 77! = 145183092028285869634070784086308284983740379224208358846781574688061991349156420080065207861248000000000000000000 78! = 11324281178206297831457521158732046228731749579488251990048962825668835325234200766245086213177344000000000000000000 79! = 894618213078297528685144171539831652069808216779571907213868063227837990693501860533361810841010176000000000000000000
14. Agregační funkce s hodnotami typu Decimal
Velmi užitečné mohou být i agregační funkce nazvané Min, Max, Avg a Sum, jejichž jména dostatečně popisují prováděný výpočet. Těmto funkcím je možné předat libovolný (nenulový) počet hodnot typu Decimal a výsledkem bude nová hodnota stejného typu s vypočteným výsledkem:
package main import ( "fmt" "github.com/shopspring/decimal" ) func main() { x := decimal.NewFromInt32(2) y := decimal.NewFromInt32(3) z := decimal.NewFromInt32(1) w := decimal.NewFromInt32(0) min := decimal.Min(x, y, z, w) max := decimal.Max(x, y, z, w) avg := decimal.Avg(x, y, z, w) sum := decimal.Sum(x, y, z, w) fmt.Println("min ", min) fmt.Println("max ", max) fmt.Println("avg ", avg) fmt.Println("sum ", sum) }
Podívejme se nyní na vypočtené výsledky:
min 0 max 3 avg 1.5 sum 6
Pokus o předání nulového počtu parametrů skončí chybou při překladu:
package main import ( "fmt" "github.com/shopspring/decimal" ) func main() { min := decimal.Min() max := decimal.Max() avg := decimal.Avg() sum := decimal.Sum() fmt.Println("min ", min) fmt.Println("max ", max) fmt.Println("avg ", avg) fmt.Println("sum ", sum) }
./g.go:10:9: not enough arguments in call to decimal.Min have () want (decimal.Decimal, ...decimal.Decimal) ./g.go:11:9: not enough arguments in call to decimal.Max have () want (decimal.Decimal, ...decimal.Decimal) ./g.go:12:9: not enough arguments in call to decimal.Avg have () want (decimal.Decimal, ...decimal.Decimal) ./g.go:13:9: not enough arguments in call to decimal.Sum have () want (decimal.Decimal, ...decimal.Decimal)
15. Interní struktura hodnot typu Decimal
Ve druhé kapitole jsme si ukázali datovou strukturu, která je použita pro uchování hodnot typu Decimal. Tento datový typ podporuje i serializaci hodnoty do sekvence bajtů, což nám umožní sledovat konkrétní způsob uložení numerických hodnot. Pro serializaci do sekvence bajtů se používá metoda nazvaná MarshalBinary, která vrací řez bajtů (slice) a taktéž informaci o chybě, ke které (čistě teoreticky) může dojít (prakticky k chybě nedojde, ovšem musí být splněno rozhraní BinaryMarshaller).
V dalším demonstračním příkladu se pokusíme o serializaci několika hodnot a prozkoumáme výsledky:
package main import ( "fmt" "github.com/shopspring/decimal" ) func main() { values := []float32{0, 1, -1, 10, -10, 100, 255, 256, 257, 2550, 2560, 2570, 1000, 1e38, -1e38} for _, value := range values { dec := decimal.NewFromFloat32(float32(value)) bin, _ := dec.MarshalBinary() fmt.Println(dec, "\t", bin) } }
Velmi zajímavý je pohled na zobrazené výsledky:
0 [0 0 0 0 2] 1 [0 0 0 0 2 1] -1 [0 0 0 0 3 1] 10 [0 0 0 1 2 1] -10 [0 0 0 1 3 1] 100 [0 0 0 2 2 1] 255 [0 0 0 0 2 255] 256 [0 0 0 0 2 1 0] 257 [0 0 0 0 2 1 1] 2550 [0 0 0 1 2 255] 2560 [0 0 0 1 2 1 0] 2570 [0 0 0 1 2 1 1] 1000 [0 0 0 3 2 1] 100000000000000000000000000000000000000 [0 0 0 38 2 1] -100000000000000000000000000000000000000 [0 0 0 38 3 1]
Nula je, jak je patrné, uložena bez mantisy.
Musíme si uvědomit, že ve vypsané sekvenci bajtů první čtyři bajty odpovídají desítkovému exponentu, což vysvětluje rozdíly mezi těmito hodnotami:
1 [0 0 0 0 2 1] 10 [0 0 0 1 2 1] 100 [0 0 0 2 2 1] 1000 [0 0 0 3 2 1] 100000000000000000000000000000000000000 [0 0 0 38 2 1]
Povšimněte si, že poslední dva bajty (což je, když poněkud předběhneme, znaménko a mantisa) jsou shodné, zatímco exponent je postupně roven 0, 1, 2 a 3, což přesně odpovídá reprezentované hodnotě.
Dále lze snadno zjistit, který bit v pátém bajtu odpovídá znaménku:
1 [0 0 0 0 2 1] -1 [0 0 0 0 3 1] 10 [0 0 0 1 2 1] -10 [0 0 0 1 3 1] 100000000000000000000000000000000000000 [0 0 0 38 2 1] -100000000000000000000000000000000000000 [0 0 0 38 3 1]
A konečně hodnota mantisy je uložena binárně:
255 [0 0 0 0 2 255] // 255 256 [0 0 0 0 2 1 0] // 1*256+0 257 [0 0 0 0 2 1 1] // i*255+1 2550 [0 0 0 1 2 255] // 10*(255) 2560 [0 0 0 1 2 1 0] // 10*(1*256+0) 2570 [0 0 0 1 2 1 1] // 10*(1*256+0)
16. Výpočet hodnoty π s typem Decimal
Opět se na chvíli vraťme k předchozím dvěma článkům, v nichž jsme si popisovali datové typy poskytované standardní knihovnou big, tj. typy big.Int, big.Rat a big.Float. V těchto článcích jsme mj. ukazovali i (pomalý) postupný výpočet konstanty π založený na Wallisově produktu (řadě členů, které se nesčítají, ale násobí). Tento algoritmus je pochopitelně možné přepsat do podoby, v níž budou pro výpočty použity hodnoty typu Decimal. A opět budeme zkoumat absolutní a relativní chybu těchto výpočtů při porovnání výsledků s „přesnou“ (ehm) reprezentací π ve formě konstanty math.Pi:
package main import ( "fmt" "math" "github.com/shopspring/decimal" ) func main() { result := decimal.NewFromInt32(2) one := decimal.NewFromInt32(1) limit := decimal.NewFromInt32(200) for n := decimal.NewFromInt32(1); n.Cmp(limit) <= 0; n = n.Add(one) { m := decimal.NewFromInt32(4) m = m.Mul(n) m = m.Mul(n) mn := m.Sub(one) m = m.Div(mn) result = result.Mul(m) f, _ := result.Float64() absError := math.Pi - f relError := 100.0 * absError / math.Pi fmt.Println(f, "\t", absError, "\t", relError) //, "\t", result.String()) } }
A vypočtené výsledky:
2.6666666666666665 0.4749259869231266 15.11736368432249 2.8444444444444446 0.29714820914534856 9.458521263277312 2.9257142857142857 0.2158783678755074 6.871621870799526 2.972154195011338 0.1694384585784552 5.3933936465264996 3.002175954556907 0.139416699032886 4.437771360127774 3.023170192001361 0.11842246158843217 3.7695040269818167 ... ... ... 3.1227247411658112 0.018867912423981892 0.6005843056203406 3.1231673669264297 0.018425286663363405 0.5864950900718922 3.1235897019320995 0.01800295165769361 0.573051749313274 3.1239931101333047 0.017599543456488398 0.56021086745215 ... ... ... 3.1376384825154533 0.003954171074339818 0.1258651744624345 3.137658290464056 0.003934363125737139 0.12523466787590917 3.137677900950937 0.0039147526388561005 0.12461044669119796
17. Rychlost výpočtů s hodnotami typu Decimal
Prozatím jsme si ukázali, jak jsou hodnoty typu Decimal interně uloženy a jaké operace je s nimi možné provádět. Ovšem důležitá je i rychlost prováděných výpočtů. Ta pochopitelně nemůže dosahovat rychlosti operací s hodnotami, jejichž formát odpovídá použité architektuře mikroprocesorů (tedy typicky byte, int s různou šířkou, float32 a float64). Pro zajímavost si porovnáme rychlost výpočtu konstanty π výše zmíněným Wallisovým produktem. Počítat přitom budeme dvěma způsoby – s využitím standardní knihovny math/big a taktéž s využitím knihovny shopspring/decimal. Povšimněte si, že u typu Decimal jsou výpočty zapsány mnohem jednodušším způsobem, bez nutnosti převodu mezi různými datovými typy atd.:
package main import ( "math" "math/big" "testing" "github.com/shopspring/decimal" ) func piBig(n int) float64 { var result big.Rat result.SetFrac64(2, 1) one := big.NewInt(1) limit := big.NewInt(1000) for n := big.NewInt(1); n.Cmp(limit) <= 0; n.Add(n, one) { m := big.NewInt(4) m.Mul(m, n) m.Mul(m, n) mn := big.NewInt(0) mn.Sub(m, one) var item big.Rat item.SetFrac(m, mn) result.Mul(&result, &item) } f, _ := result.Float64() return f } func piDecimal(n int) float64 { result := decimal.NewFromInt32(2) one := decimal.NewFromInt32(1) limit := decimal.NewFromInt32(200) for n := decimal.NewFromInt32(1); n.Cmp(limit) <= 0; n = n.Add(one) { m := decimal.NewFromInt32(4) m = m.Mul(n) m = m.Mul(n) mn := m.Sub(one) m = m.Div(mn) result = result.Mul(m) } f, _ := result.Float64() return f } func BenchmarkPiBig(b *testing.B) { for n := 1; n < b.N; n++ { f := piBig(n) if math.Abs(f-math.Pi) >= 0.01 { b.Fatal("Incorrect result", f) } } } func BenchmarkPiDecimal(b *testing.B) { for n := 1; n < b.N; n++ { f := piDecimal(n) if math.Abs(f-math.Pi) >= 0.01 { b.Fatal("Incorrect result", f) } } }
18. Výsledky benchmarků
Benchmark představený v předchozí kapitole nyní přeložíme a následně i spustíme:
$ ./run_benchmark.sh
Z výsledků je patrné, že výpočet s hodnotami Decimal je v tomto případě mnohem rychlejší, než je tomu u datového typu big.Float, což se ovšem dalo předpokládat vzhledem ke konverzím numerických hodnot v prvním benchmarku a taktéž kvůli složitější interní struktuře datového typu big.Float (na druhou stranu je rozdíl v rozsahu dvou řádů):
goos: linux goarch: amd64 pkg: div-round cpu: Intel(R) Core(TM) i7-8665U CPU @ 1.90GHz BenchmarkPiBig-8 1000 26720122 ns/op BenchmarkPiDecimal-8 1000 412662 ns/op PASS ok div-round 27.136s
19. Repositář s demonstračními příklady
Zdrojové kódy všech předminule, minule i dnes použitých demonstračních příkladů naprogramovaných v jazyku Go byly uloženy do Git repositáře, který je dostupný na adrese https://github.com/tisnik/go-root. V případě, že nebudete chtít klonovat celý repositář, můžete namísto toho použít odkazy na jednotlivé demonstrační příklady, které naleznete v následující tabulce:
20. Odkazy na Internetu
- Číselné hodnoty s neomezeným rozsahem a přesností v programovacím jazyku Go (1)
https://www.root.cz/clanky/ciselne-hodnoty-s-neomezenym-rozsahem-a-presnosti-v-programovacim-jazyku-go-1/ - Číselné hodnoty s neomezeným rozsahem a přesností v programovacím jazyku Go (2)
https://www.root.cz/clanky/ciselne-hodnoty-s-neomezenym-rozsahem-a-presnosti-v-programovacim-jazyku-go-2/ - Balíček big pro jazyk Go
https://pkg.go.dev/math/big - Zdrojové kódu pro balíček big
https://cs.opensource.google/go/go/+/master:src/math/big/ - Arbitrary-precision arithmetic
https://en.wikipedia.org/wiki/Arbitrary-precision_arithmetic - Floating-point error mitigation
https://en.wikipedia.org/wiki/Floating-point_error_mitigation - Beating Floating Point at its Own Game: Posit Arithmetic
http://www.johngustafson.net/pdfs/BeatingFloatingPoint.pdf - Unum (number format)
https://en.wikipedia.org/wiki/Unum_(number_format) - The GNU MPFR Library
https://www.mpfr.org/ - GMP: Arithmetic without limitations
https://gmplib.org/ - GNU MP 6.2.1 manual
https://gmplib.org/manual/index - Anatomy of a posit number
https://www.johndcook.com/blog/2018/04/11/anatomy-of-a-posit-number/ - Better floating point: posits in plain language
http://loyc.net/2019/unum-posits.html - Posits, a New Kind of Number, Improves the Math of AI: The first posit-based processor core gave a ten-thousandfold accuracy boost
https://spectrum.ieee.org/floating-point-numbers-posits-processor - Posit Standard Document (2022)
https://posithub.org/khub_widget - Standard for Posit™ Arithmetic (2022)
https://posithub.org/docs/posit_standard-2.pdf - Posit Calculator
https://posithub.org/widget/calculator/ - SoftPosit
https://gitlab.com/cerlane/SoftPosit - PySigmoid
https://github.com/mightymercado/PySigmoid - sgpositpy
https://github.com/xman/sgpositpy - SoftPosit.jl
https://github.com/milankl/SoftPosit.jl - SigmoidNumbers.jl
https://github.com/MohHizzani/SigmoidNumbers.jl - How many digits can float8, float16, float32, float64, and float128 contain?
https://stackoverflow.com/questions/56514892/how-many-digits-can-float8-float16-float32-float64-and-float128-contain - 15. Floating Point Arithmetic: Issues and Limitations (Python documentation)
https://docs.python.org/3/tutorial/floatingpoint.html - Number limits, overflow, and roundoff
https://www.khanacademy.org/computing/computers-and-internet/xcae6f4a7ff015e7d:digital-information/xcae6f4a7ff015e7d:limitations-of-storing-numbers/a/number-limits-overflow-and-roundoff - The upper and lower limits of IEEE-754 standard
https://math.stackexchange.com/questions/2607697/the-upper-and-lower-limits-of-ieee-754-standard