====== std.feq — Fließkomma vergleichen und runden ======
Fließkommazahlen auf der Bit-Ebene: Vergleiche mit Toleranz, ULP-Abstand, Klassifikation, totale Ordnung und Rundungsarten, die der Bestand nicht hat.
→ [[lyx_-_programmiersprache:units:mathematik|Mathematik-Units]] · [[lyx_-_programmiersprache:units:mathx|std.mathx]] · [[lyx_-_programmiersprache:units:sum|std.sum]] · [[lyx_-_programmiersprache:sprache:datentypen|Datentypen]]
import std.feq;
Schicht 0, hängt an ''std.math'' und ''std.alloc''. 36 Funktionen. Alle Beispiele mit ''lyxc 1.0.21A'' übersetzt und ausgeführt.
----
===== Warum ''=='' bei f64 nicht reicht =====
unit main;
import std.io;
import std.string;
import std.feq;
fn ZI(t: pchar, v: int64): void { Print(t); PrintLn(IntToStr(v)); }
fn ZB(t: pchar, v: bool): void { Print(t); PrintLn(IntToStr(v as int64)); }
fn main(): int64 {
var a: f64 := 0.1;
var b: f64 := 0.2;
var s: f64 := a + b;
var d: f64 := 0.3;
ZB("0.1+0.2 == 0.3 : ", s == d);
ZI("UlpsBetween(s, 0.3) : ", FeqUlpsBetween(s, d));
ZB("FeqUlps(s,0.3,4,0.0) : ", FeqUlps(s, d, 4, 0.0));
ZB("FeqRel(s,0.3,1e-12) : ", FeqRel(s, d, 0.000000000001));
return 0;
}
0.1+0.2 == 0.3 : 0
UlpsBetween(s, 0.3) : 1
FeqUlps(s,0.3,4,0.0) : 1
FeqRel(s,0.3,1e-12) : 1
''0.1 + 0.2'' ist **nicht** ''0.3'' — aber die beiden liegen genau **ein ULP** auseinander, also so nah wie zwei verschiedene f64-Zahlen überhaupt sein können. Genau das misst ''FeqUlpsBetween'': nicht „wie groß ist der Unterschied", sondern „**wie viele darstellbare Zahlen liegen dazwischen**".
^ Vergleich ^ Wann ^
| ''FeqAbs(a, b, absTol)'' | Werte in bekannter Größenordnung, Toleranz absolut |
| ''FeqRel(a, b, relTol)'' | Werte über viele Größenordnungen, Toleranz relativ |
| ''FeqEqual(a, b, absTol, relTol)'' | beides zugleich — absolut nahe null, sonst relativ |
| ''FeqUlps(a, b, maxUlps, absTol)'' | Rechenfehler zählen statt schätzen |
| ''FeqZero(a, absTol)'' | gegen null, wo relativ sinnlos ist |
> **Die Wahl ist keine Geschmacksfrage.** Relativer Vergleich versagt in der Nähe von null (die Differenz zweier winziger Zahlen ist relativ riesig), absoluter Vergleich versagt bei großen Zahlen (1e12 und 1e12+1 sind absolut weit auseinander, praktisch aber gleich). ''FeqEqual'' deckt beides ab und ist die richtige Vorgabe, wenn die Größenordnung unbekannt ist.
----
===== Die Bit-Ebene =====
unit main;
import std.io;
import std.string;
import std.feq;
fn ZI(t: pchar, v: int64): void { Print(t); PrintLn(IntToStr(v)); }
fn main(): int64 {
ZI("Bits(1.0) : ", FeqBits(1.0));
ZI("Bits(Epsilon) : ", FeqBits(FeqEpsilon()));
ZI("Bits(Ulp(1.0)) : ", FeqBits(FeqUlp(1.0)));
ZI("Bits(MinSubnormal) : ", FeqBits(FeqMinSubnormal()));
ZI("Ulps(1, NextUp(1)) : ", FeqUlpsBetween(1.0, FeqNextUp(1.0)));
return 0;
}
Bits(1.0) : 4607182418800017408
Bits(Epsilon) : 4372995238176751616
Bits(Ulp(1.0)) : 4372995238176751616
Bits(MinSubnormal) : 1
Ulps(1, NextUp(1)) : 1
**''Epsilon'' und ''Ulp(1.0)'' sind dasselbe** — das Epsilon //ist// der Abstand von 1 zur nächsten darstellbaren Zahl. ''MinSubnormal'' hat das Bitmuster 1: die kleinste von null verschiedene f64-Zahl überhaupt.
**''PrintF64'' verdeckt jeden Genauigkeitsbefund.** Die Ausgabe hat sechs Nachkommastellen — ''FeqEpsilon()'' erscheint darin als ''0.000000''. Wer Genauigkeit misst, gibt das **Bitmuster** aus (''FeqBits'') und wandelt außerhalb zurück. Ein Fuzz-Lauf über ''Atan2F64'' meldete auf diesem Weg einmal „keine Abweichung", während die Funktion sechs Stellen zu wenig hatte.
''PrintF64'' setzt außerdem **selbst einen Zeilenumbruch**. Ein nachgestelltes ''PrintLn("")'' erzeugt eine Leerzeile.
^ Funktion ^ Liefert ^
| ''FeqBits(x)'' / ''FeqFromBits(b)'' | Bitmuster hin und zurück |
| ''FeqUlp(x)'' | Abstand zur nächsten darstellbaren Zahl bei ''x'' |
| ''FeqNextUp(x)'' / ''FeqNextDown(x)'' | Nachbarzahl nach oben / unten |
| ''FeqUlpsBetween(a, b)'' | Anzahl darstellbarer Zahlen dazwischen |
| ''FeqEpsilon()'' · ''FeqMax()'' · ''FeqMinNormal()'' · ''FeqMinSubnormal()'' | Eckwerte |
----
===== Klassifikation und Ordnung =====
unit main;
import std.io;
import std.string;
import std.feq;
fn ZI(t: pchar, v: int64): void { Print(t); PrintLn(IntToStr(v)); }
fn ZF(t: pchar, v: f64): void { Print(t); PrintF64(v); }
fn main(): int64 {
var nan: f64 := FeqFromBits(9221120237041090560);
var inf: f64 := FeqFromBits(9218868437227405312);
ZI("IsNaN(nan) : ", FeqIsNaN(nan) as int64);
ZI("IsInf(inf) : ", FeqIsInf(inf) as int64);
ZI("Compare(1.0, nan) : ", FeqCompare(1.0, nan));
ZI("TotalOrder(1.0, nan): ", FeqTotalOrder(1.0, nan));
ZF("SafeDiv(1, 0, -1) : ", FeqSafeDiv(1.0, 0.0, -1.0));
return 0;
}
IsNaN(nan) : 1
IsInf(inf) : 1
Compare(1.0, nan) : 2
TotalOrder(1.0, nan): -1
SafeDiv(1, 0, -1) : -1.000000
**Die beiden Vergleichsfunktionen haben verschiedene Aufgaben, und der Unterschied zeigt sich genau bei NaN:**
* ''FeqCompare'' folgt der IEEE-Semantik und liefert ''−1'', ''0'', ''1'' — oder **''2'' für „unvergleichbar"**, sobald ein NaN beteiligt ist. Zwei NaN gelten dabei als gleich (''0''). Dieser Rückgabewert **muss** abgefangen werden; wer nur auf ''< 0'' prüft, behandelt „unvergleichbar" wie „größer".
* ''FeqTotalOrder'' legt eine **vollständige** Ordnung über alle Bitmuster: hier ist NaN einsortierbar, das Ergebnis ist immer ''−1'', ''0'' oder ''1''. Das ist die Funktion zum **Sortieren** — ein Sortierverfahren mit ''FeqCompare'' würde bei NaN in den Daten in eine unbestimmte Reihenfolge laufen.
''FeqSafeDiv(a, b, fallback)'' gibt bei Division durch null den Ersatzwert zurück, statt ''inf'' oder NaN in die Rechnung zu tragen.
Dazu ''FeqIsFinite'', ''FeqIsZero'', ''FeqIsSubnormal'', ''FeqSignBit'', ''FeqClamp'' und ''FeqLerp''.
----
===== Runden — sieben Arten, und der Bruch bei negativen Zahlen =====
unit main;
import std.io;
import std.string;
import std.feq;
fn ZI(t: pchar, v: int64): void { Print(t); PrintLn(IntToStr(v)); }
fn ZF(t: pchar, v: f64): void { Print(t); PrintF64(v); }
fn main(): int64 {
ZF("RoundHalfAway(2.5) : ", FeqRoundHalfAway(2.5));
ZF("RoundHalfEven(2.5) : ", FeqRoundHalfEven(2.5));
ZF("RoundHalfAway(3.5) : ", FeqRoundHalfAway(3.5));
ZF("RoundHalfEven(3.5) : ", FeqRoundHalfEven(3.5));
ZF("RoundTo(3.14159,2) : ", FeqRoundTo(3.14159, 2));
ZI("FloorDiv(-7, 2) : ", FeqFloorDiv(-7, 2));
ZI("roh -7 / 2 : ", -7 / 2);
ZI("FloorMod(-7, 2) : ", FeqFloorMod(-7, 2));
ZI("roh -7 % 2 : ", -7 % 2);
ZI("CeilDiv(7, 2) : ", FeqCeilDiv(7, 2));
return 0;
}
RoundHalfAway(2.5) : 3.000000
RoundHalfEven(2.5) : 2.000000
RoundHalfAway(3.5) : 4.000000
RoundHalfEven(3.5) : 4.000000
RoundTo(3.14159,2) : 3.140000
FloorDiv(-7, 2) : -4
roh -7 / 2 : -3
FloorMod(-7, 2) : 1
roh -7 % 2 : -1
CeilDiv(7, 2) : 4
**Bei ''2.5'' gehen die beiden Rundungsarten auseinander, bei ''3.5'' nicht.** ''HalfEven'' rundet zur geraden Nachbarzahl — 2.5 wird zu 2, 3.5 zu 4. Über viele Werte hebt sich der Rundungsfehler damit auf, statt sich zu summieren; deshalb ist es die Vorgabe in der Statistik und im Bankwesen. ''HalfAway'' ist die Schulregel und rundet 2.5 auf 3.
> **''FeqFloorDiv'' ist nicht ''/''.** Die rohe Ganzzahldivision schneidet **zur Null hin** ab: ''−7 / 2'' ergibt ''−3''. ''FeqFloorDiv'' rundet **nach unten** und liefert ''−4''. Entsprechend hat ''FeqFloorMod'' immer das Vorzeichen des Divisors (''1''), der rohe Rest das des Dividenden (''−1'').
>
> Das ist keine Feinheit: bei Ringpuffern, Gitterkoordinaten und Zeitfenstern ist ''FloorMod'' die richtige Wahl, weil ein negativer Index sonst aus dem Puffer zeigt. ''−7 % 2 = −1'' als Feldindex ist ein Zugriff daneben.
Dazu ''FeqTrunc'', ''FeqFrac'', ''FeqFloorTo'' und ''FeqCeilTo'' (Rundung auf ein beliebiges Raster, etwa Vielfache von 0,05).
----
Letzte Aktualisierung: 2026-08-16 · alle Beispiele mit ''lyxc 1.0.21A'' übersetzt und ausgeführt; Bitmuster, Rundungs- und Divisionswerte gegen eine unabhängige Referenzrechnung geprüft.