vb@rchiv
VB Classic
VB.NET
ADO.NET
VBA
C#
Erstellen von dynamischen Kontextmen?s - wann immer Sie sie brauchen!  
 vb@rchiv Quick-Search: Suche startenErweiterte Suche starten   Impressum  | Datenschutz  | vb@rchiv CD Vol.6  | Shop Copyright ©2000-2024
 
zurück
Rubrik: Verschiedenes   |   VB-Versionen: VB5, VB601.06.05
Der Gebrauch des Datentyps DECIMAL

Dieser Workshop zeigt Details zum DECIMAL-Datentyp Format auf erläutert Unterschiede zwischen dem IEEE-Format und dem DECIMAL-Format. Weiterhin wird auf den Dateizugriff bei numerischen VARIANT-Variablen eingegangen, sowie ein Klassenmodul zur Arrayverwaltung vorgestellt.

Autor:  Manfred BohnBewertung:     [ Jetzt bewerten ]Views:  26.210 

Dieser Workshop zeigt Details zum DECIMAL-Datentyp Format auf erläutert Unterschiede zwischen dem IEEE-Format und dem DECIMAL-Format. Weiterhin wird auf den Dateizugriff bei numerischen VARIANT-Variablen eingegangen, sowie ein Klassenmodul zur Arrayverwaltung vorgestellt.

Der Untertyp DECIMAL des allgemeinen VB-Datentyps VARIANT bietet genaue Ganzzahl- und Gleitkomma-Berechnungen durch 96-Bit-Zahlen.
Die Grundlagen und Anwendungen dieses Datentyps sind im Tipp  Der Gebrauch des Datentyps DECIMAL beschrieben und demonstriert worden.
Die Verwendbarkeit bei der Lösung numerischer Probleme ist im Tipp  Der Gebrauch des Datentyps DECIMAL (Teil 2) erläutert und mit einem Anwendungsbeispiel versehen.
Die Kenntnis dieser beiden Tipps wird im folgende vorausgesetzt.

Dieser Workshop informiert über 

  • die Unterschiede zwischen dem IEEE-Datenformat und dem DECIMAL-Format, 
  • die Verwendung von DOUBLE- und DECIMAL-Variablen bei großen Ganzzahlen,
  • den Dateizugriff bei numerischen VARIANT-Variablen,
  • die Klasse "clsDecimal", die ein DECIMAL-Array kapselt.

1. Das allgemeine IEEE-Gleitkomma-Format und das VB-spezifische Format DECIMAL

Die VB-Gleitkomma-Datentypen SINGLE bzw. DOUBLE speichern Werte imIEEE-Format. Das IEEE-Format (IEEE steht dabei für "Institute of Electrical and ElectronicsEngineers") ist eine allgemein definierte Gleitkommadarstellung für digitale Systeme. MICROSOFT begründet die Verwendung dieses Formats in Visual Basic hinsichtlich der Kompatibilität mit den Programmiersprachen FORTRAN und C und weil es sich dabei um einen weltweit anerkannten"Industrie-Standard" handelt. (Quick-Basic hatte ursprünglich (Version 3) ein MS-spezifisches Gleitkomma-Format mit zwar geringerer Spannweite aber dafür höherer Genauigkeit).

In IEEE wird für Variablen des Typs DOUBLE eine Gesamtlänge von 64 Bit festgelegt (davon 1 Bit Vorzeichen, 11 Bits Exponent, 52 BitsMantisse). Durch die Zerlegung von Gleitkommazahlen in Mantisse und Exponent erreicht man, dass ein größerer Zahlenbereich in beschränkterem Speicherplatz untergebracht werden kann, als es bei Festkommazahlen (wie z.B. Currency) der Fall ist. Eine Gleitkomma-Zahl wird dabei dargestellt als s * Mantisse * 2^Exponent (wobei s im Intervall -1 bis 1 liegt). Die üblichen PC-Prozessoren und ihre mathematischen Koprozessoren erlauben die besonders effiziente Verarbeitung desIEEE-Formats.

Dieses Format besitzt einige charakteristische Eigenschaften: 

Die Abbildung eines als stetig angenommenen Zahlenbereichs auf eine begrenzte Menge von Binärzuständen erzwingt Rundungs-Ungenauigkeiten. Dieser Rundungsfehler kann berechnet werden. Bei DOUBLE-Variablen beträgt die Rundungs-Ungenauigkeit 2^(-52) d.h. ca. 2.2 * 10^(-16). Diese Abweichung fließt in Gleitkommaberechnungen ein. Der Rundungsmechanismus in Visual Basic entspricht dem IEEE-Standard (Suche nach dem"nähesten Nachbarn").

Zur Verdeutlichung der Problematik einige zusammenfassende Anmerkungen nach einem Artikel, der in der MSDN enthalten ist:

Nur Zahlen, die in der Form p/(2^q) darstellbar sind, können durch eine begrenzte Zahl von Bits korrekt abgebildet werden. Sogar eine einfache Dezimalzahl wie 0.0001 benötigt für eine exakte IEEE-Darstellung eine sich wiederholende Periode von 104 Bits.

Es sind u.a. folgende Fehler-Arten zu unterscheiden:

  • hardwarespezifische Rundungsverfahren (inkompatible Rechen-Ergebnisse)
  • möglicher Verlust "signifikanter" Stellen bei der Speicherung von Zwischenergebnissen
  • stark fehlerbehaftete Quotienten bei Divisoren deren Betrag nahe bei Null liegt 
  • Auswirkungen spezifischer "Format-interner" Besonderheiten

Durch die unterschiedliche binäre und dezimale Darstellung der Zahlen kann es dazu kommen, dass Zahlen die unmittelbar "rund" erscheinen, alsoz. B. als 15.35 ausgegeben werden, in Wirklichkeit bei der Berechnung einen auf Bitebene dargestellten Wert von 15.3499999993278 erhalten (fiktives Beispiel). Zahlen im Gleitkommaformat (DOUBLE / SINGLE) sollte man deshalb nie auf Gleichheit prüfen [a=b], sondern stets die Größe der absoluten Differenz abfragen [ABS(a-b) < Epsilon]. Die erforderliche Ausprägung des Epsilon-Wertes ist von der Absolutausprägung der verglichenen Zahlen abhängig. 

Die Ergebnisse im folgenden Beispiel hat ein INTEL-CELERON-Prozessor geliefert:

Dim ax as Double, bx as Double
 
bx = 0.12345678901234
ax = (bx ^ 2.5) ^ (1 / 2.5)

Die berechnete Variable ax wird (scheinbar korrekt) ausgegeben als: 0.12345678901234
Die berechnete Differenz zwischen ax und bx (ax-bx) beträgt: -1,38777878078145E-17 

Näheren Aufschluss liefert die Betrachtung der 64-er Bitmuster, die die beiden Variablen enthalten:
Bitmuster ax = 1010001100101111011000101110110010111011010110011111110111111100
Bitmuster bx = 0110001100101111011000101110110010111011010110011111110111111100
Die beiden ersten Bits sind vertauscht.

bx = 12345678901234#
ax = (bx ^ 2.5) ^ (1 / 2.5)

Die Variable ax wird (scheinbar korrekt) ausgegeben als: 12345678901234
Die berechnete Differenz zwischen ax und bx beträgt: +0,021484375 

Auch hier wieder ein Blick auf die 64er-Bitmuster:
Bitmuster ax = 1101000000100111111110100011100111100111001011100110010101000010
Bitmuster bx = 0000000000100111111110100011100111100111001011100110010101000010
In diesem Fall unterscheiden sich sogar die ersten vier Bits. Trotzdem werden die beiden Werte in der dezimalen Darstellung identisch angezeigt.

Die Prüfung der "Gleichheit" von zwei Double-Variablen ist nicht so einfach, wie es oft dargestellt wird. Wählt man beim Vergleich großer Zahlen einen zu kleinen Wert für die Epsilon-Konstante, werden die beiden Zahlen nie als gleich erkannt. Die Routine "IsEqual" weist den Weg, wie man den Vergleich über die Berechnung eines relativen Epsilon-Wertes durchführen kann. (Da sich bei komplizierten Berechnungen mit vielen Wert-Zuweisungen die Rundungs-Ungenauigkeiten potenzieren können, muss die Epsilon-Konstante eventuell vergrößert werden.)

Der Vergleich CDec(ax) - CDec(bx) liefert in beiden Fällen die (dezimal korrekte) Differenz 0. Bei der Umwandlung eines Double-Wertes in einen Decimal-Wert durch CDEC wird also nicht auf die interne Bit-Darstellung des Double-Wertes zugegriffen, wie sie bei Berechnungen verwendet wird, sondern auf dessen Dezimal-Darstellung.

Die Verwendung der Funktion CDEC bietet dennoch KEINEN eleganten Ausweg, um die Gleichheit von zwei Double-Werten zu prüfen. Ungenauigkeiten können nämlich so groß werden, dass"eigentlich" identische Werte auch dezimal unterschiedlich dargestellt sind. Man kann der"Epsilontik" im IEEE-Fomat nicht entkommen.

' =============================================================================
' Beginn QuellCode von IsEqual
' =============================================================================
Function IsEqual(ByVal a As Double, ByVal b As Double, _
  Optional ByRef Differenz As Double, _
  Optional ByRef RelativeDifferenz As Double) As Boolean
 
  ' Die Funktion prüft, ob zwei Double-Werte als "gleich" betrachtet
  ' werden können
  ' In den optionalen Parametern wird der Betrag der absoluten
  ' und der relativen Differenz zurückgegeben
 
  ' absolute Epsilon-Konstante
  Const epsilon As Double = 0.000000000001
  ' Hilfs-Konstante
  Const zwei As Double = 2#
 
  ' relatives Epsilon
  Dim rel_eps As Double
  Dim Mittel As Double
 
  On Error GoTo fehler
 
  ' Rückgabe initialisieren
  Differenz = 0: RelativeDifferenz = 0
 
  ' Betrag der Differenz der beiden Werte
  ' Falls es hier zum Überlauf kommt, liegt mit
  ' Sicherheit ein Unterschied vor (Rückgabe: false)!
  Differenz = Abs(a - b)
 
  ' Bestimmung des geeigneten Epsilon-Wertes
  ' (Überlauf und Division durch Null verhindern!)
  Mittel = Abs(a / zwei + b / zwei)
  If Mittel < epsilon Then
    Mittel = epsilon
  Else
    ' Relative Differenz der beiden Werte
    ' bezogen auf deren arithmetisches Mittel
    RelativeDifferenz = Differenz / Mittel
  End If
  rel_eps = epsilon * Mittel
 
  ' Differenz-Betrag kleiner Vergleichs-Epsilon ?
  IsEqual = (Differenz < rel_eps)
 
fehler:
End Function
 
' =============================================================================
' Ende QuellCode von IsEqual
' =============================================================================

Im Gegensatz zu DOUBLE / SINGLE greift der VARIANT-Untertyp DECIMAL nicht auf ein exponentbehaftetes Gleitkomma-Verfahren zurück, sondern wird intern als vorzeichenlose, positive 96-Bit-Ganzzahl gespeichert. Die Anzahl der Nachkommastellen und das Vorzeichen werden als Zusatzinformation gespeichert. 

Aufbau einer 16 Byte langen Variable vom Typ VARIANT / Untertyp DECIMAL:

1. Byte:    Kennung für Decimal (14)
2. Byte:    unbelegt (0)
3. Byte:    Anzahl der Nachkommastellen (0-28)
4. Byte:    Vorzeichen (0=positiv; 128=negativ)
5-16. Byte: vorzeichenlose Ganzzahl (96-Bit-Muster)

Die Abfolge der Verwendung der Byte-Positionen für die Speicherung der 96-Bit-Ganzzahl beim Hochzählen von 0 ist anders, als man es erwarten würde: 9-10-11-12-13-14-15-16-5-6-7-8

Variablen des Datentyps DECIMAL bieten eine eindeutige Zuordnung zwischen dezimaler und binärer Darstellung. Dabei ergibt sich der Nachteil, dass der darstellbare Zahlenbereich beschränkt ist. Falls die Ergebnisse von Rechen-Operationen sich durch maximal 28 Stellen vollständig darstellen lassen (alsoz. B. keine periodischen Brüche auftreten und keine Kommastellen abgeschnitten werden), sind sie auf der Basis von Variablen des Typs DECIMAL exakt, weil keine Rundungen erforderlich werden. 

Wiederholung des obigen Beispiels für Decimals:

Dim ax As Variant, bx As Variant
 
bx = CDec(0.12345678901234)
ax = CDec((bx ^ CDec(2.5)) ^ (CDec(1) / CDec(2.5)))

Die 96er-Bitmuster (Byte 5-16) der beiden Variablenwerte sind identisch:
Bitmuster = 000000000000000000000000000000000100111111110100011100111100111001011100110100000000000000000000

bx = CDec(12345678901234#)
ax = CDec((bx ^ CDec(2.5)) ^ (CDec(1) / CDec(2.5)))

Auch hier sind die 96er-Bitmuster (Byte 5-16) identisch:
Bitmuster = 000000000000000000000000000000000100111111110100011100111100111001011100110100000000000000000000

Wer genau hinschaut wird feststellen, dass beide Bitmuster identisch sind. Das liegt daran, dass dieser Datentyp eine vorzeichenlose Ganzzahl enthält. Der Unterschied zwischen den Zahlen in beiden Beispielen wird erst erkennbar, wenn man auch die vier Verwaltungsbytes der Variant-Variable mit einbezieht. Das dritte Byte definiert die unterschiedlichen Nachkommastellen.

Verwaltungsbits im ersten Beispiel = 01110000 | 00000000 | 01110000 | 00000000
Verwaltungsbits im zweiten Beispiel = 01110000 | 00000000 | 00000000 | 00000000

Die Verwaltung des Datentyps DECIMAL als VARIANT-Variable erfordert eine Speicherung von 16 Byte. Die Verwendung von Arrays des Datentyps VARIANT belegt mehr Speicher als notwendig, weil 13 Byte (12 Byte + 1 Byte für Vorzeichen und Nachkommastellen) ausreichen, um alle benötigten Informationen aufzunehmen. (Das Verwaltungsbyte wird dabei mit der Summe der Bytes 3/4 der VARIANT-Variablen belegt.)

Die beigefügte Klasse "clsDecimal" kapselt ein Bytearray, das Arrayfelder des Typs DECIMAL speichersparend in jeweils 13 Bytes ablegt. Durch Verwendung der API-Funktion"RtlMoveMemory" bei der Zuweisung der Datenfelder wird dafür kaum zusätzliche Rechenzeit benötigt.
 

2. Ganzzahlen im IEEE-Format (DOUBLE) und im DECIMAL-Format

Visual Basic bietet für die Verarbeitung von Ganzzahlen die Datentypen INTEGER und LONG. Der Nachteil beim 16- bzw. 32-Bit-Format ist die relativ geringe zulässige Spannweite der Werte. Bei LONG-Variablen beträgt sie +/-2147483647 (=2^31-1).

Für größere Ausprägungen werden meist Variablen des Datentyps DOUBLE eingesetzt. Die 51-Bit-Mantisse dieses Formats ermöglicht deutlich größere Ganzzahlen im Bereich +/-999999999999999 (= -1E15+1 bis 1E15-1). Allerdings ist Vorsicht geboten. Da es sich um ein Gleitkomma-Format handelt, muss man bei Wertzuweisungen darauf achten, dass intern keine automatische Umstellung auf (gerundete) Exponentialdarstellung erfolgt. Auch in der Dezimaldarstellung ganzzahlig erscheinende Daten können intern gerundet sein, so dassz. B. die Abfrage auf "Gleichheit" scheitert. Die Typumwandlungsfunktionen CINT bzw. CLNG sind nur bei Werten im Geltungsbereich INTEGER bzw. LONG verwendbar. Man ist deshalb auf die Funktionen ROUND bzw. INT angewiesen. Sie sollten bei jeder Wertzuweisung (Berechnungs-Ergebnis) auf eine als Ganzzahl verwendete DOUBLE-Variable eingesetzt werden - und zwar am besten bereits BEI der Zuweisung (nicht danach). Zusätzlich muss darauf geachtet werden, dass kein "impliziter Ganzzahl-Überlauf" stattfindet, weil sonst automatisch zur Exponentialdarstellung gewechselt wird - die Variable enthält dann keine Ganzzahl mehr.

Die ROUND-Funktion verwendet ein "ungewöhnliches" Rundungsverfahren, das insbesondere bei der Umwandlung von Gleitkommazahlen in Ganzzahlen unerwartete Effekte zeigen kann. (Dieses - in der Mathematik gebräuchliche Verfahren - sorgt dafür, dass die letzte beibehaltene Ziffer stets gerade ist, falls die erste verworfene Ziffer 5 ist.)

Die Datenreihe

-3.5-2.5-1.5-0.5 0 0.5 1.5 2.5 3.5

wird von der VB-Funktion ROUND in folgende Ganzzahlen umgesetzt:

-4 -2 -2 0 0 0 2 2 4

Dieses Umwandlungs-Ergebnis ist vermutlich in den meisten Fällen nicht erwünscht. Besser ist es, man schreibt eine eigene Funktion, die bei .5 immer einheitlich auf- oder abrundet (die INT-Funktion ermittelt dabei den ungerundeten Vorkomma-Anteil). Als weitere Alternative ist ein Rundungsprinzip denkbar, das bei positiven Zahlen ab 0.5 auf- und bei negativen Zahlen ab 0.5 abrundet. Die Wahl des Rundungsverfahrens kann einen erheblichen Einfluss auf das erzielte Ergebnis haben.

Die Routine GANZZAHL sorgt dafür, dass in einer Double-Variable tatsächlich ein ganzzahliger Wert enthalten ist (und löst bei Ganzzahl-Überlauf einen Fehler aus). 

Sie wandelt die obige Datenreihe in

-3 -2 -1 0 0 1 2 3 4

um.

' ==========================================================================
' Start Quellcode Routine GANZZAHL
' ==========================================================================
Function Ganzzahl(ByVal wert As Double) As Double
  ' Die Funktion wandelt einen DOUBLE-Wert in eine
  ' Ganzzahl um (Rundung)
 
  Dim iwert As Double
 
  ' maximal mögliche Ganzzahl in DOUBLE - 1 (wg. Rundung)
  Const MaxInteger As Double = (1E+15 - 2)
 
  iwert = Abs(wert)
  If iwert > MaxInteger Then
    ' impliziter Ganzzahl-Überlauf
    Err.Raise 6
    Exit Function
  End If
 
  ' Rundung auf Ganzzahl
  iwert = Int(wert) ' INT liefert stets kleinere Ganzzahl
  If wert - iwert >= CDbl(0.5) Then
    ' ab .5 immer aufrunden
    wert = iwert + 1
  Else
    ' sonst abrunden
    wert = iwert
  End If
 
  Ganzzahl = wert
End Function
 
' ==========================================================================
' Ende Quellcode Routine GANZZAHL
' ==========================================================================

Der VB-Datentyp CURRENCY verarbeitet 64-Bit als Festkomma-Zahl mit vier Nachkommastellen.
Variablen diese Typs können Ganzzahlen im Bereich +/-922.337.203.685.477 darstellen - erreicht also nahezu die Spannweite die mit einer DOUBLE-Variable zu erzielen ist. Bei diesem Datentyp kann die Ganzzahl-Zuweisung über eine ähnliche Routine kontrolliert werden. Auch bei CURRENCY liefert die VB-FunktionINT stets die nächst kleinere Ganzzahl. In der Routine "Ganzzahl" müssen die Konstante MaxInteger, der Typ der Variable IWERT und die Typen der Funktionsparameter geändert werden, damit sie für CURRENCY-Variablen einsetzbar ist. (Die Typ-UmwandlungsfunktionCCUR verwendet das gleiche Rundungsverfahren wie die ROUND-Funktion: 1.55555 -> 1.5555, 2.55555 -> 2.5556).

Der Datentyp DECIMAL bietet eine noch erheblich größere Spannweite für die Ausprägung von Ganzzahlen als DOUBLE-Variable. In den 96-Bit dieses Datenformats können Werte im Bereich +/-79.228.162.514.264.337.593.543.950.335 enthalten sein (ca. 2^96 - 2^43).
Das DECIMAL-Format enthält intern zwar eine Ganzzahl und auch die Berechnungen werden mit Ganzzahl-Arithmetik vorgenommen (abgesehen von den VB-Funktionen, die im Ergebnis DOUBLE-Werte liefern) - die Kommastellen werden separat gehandhabt - dennoch ist bei der Verwendung als Ganzzahl die automatische Umstellung auf Gleitkomma-Werte zu beachten. Dazu kommt, dass VARIANT-Variablen bei Wertzuweisungen immer eine geeignete Anpassung des Untertyps vornehmen.
Da die VB-Funktion INT auch auf Decimal-Werte anwendbar ist, kann die Routine Ganzzahl_Dec ähnlich wie die Routine Ganzzahl entwickelt werden.

' ==========================================================================
' Start Quellcode Routine GANZZAHL_DEC
' ==========================================================================
Function Ganzzahl_Dec(ByVal wert As Variant) As Variant
  ' Die Funktion wandelt einen numerischen Wert in eine
  ' Ganzzahl um (Rundung): Rückgabe DECIMAL
 
  Dim iwert As Variant
 
  ' maximal mögliche Ganzzahl in DECIMAL - 1 (wg. Rundung)
  Dim MaxInteger As Variant
  MaxInteger = CDec("79228162514264337593543950334")
 
  If Not IsNumeric(wert) Then
    ' Typenkennungs-Alarm
    Err.Raise 13
    Exit Function
  End If
 
  iwert = Abs(wert)
  If iwert > MaxInteger Then
    ' Überlauf Decimal
    Err.Raise 6
  End If
 
  ' Untertyp sicherstellen
  If VarType(wert) <> vbDecimal Then
    wert = CDec(wert)
  End If
 
  ' Rundung auf Ganzzahl
  iwert = Int(wert) ' INT liefert stets kleinere Ganzzahl
  If wert - iwert >= CDec(0.5) Then
    ' ab .5 immer aufrunden
    wert = iwert + CDec(1)
  Else
    ' sonst abrunden
    wert = iwert
  End If
 
  Ganzzahl_Dec = wert
End Function
 
' ==========================================================================
' Ende Quellcode Routine GANZZAHL
' ==========================================================================

Um einen Eindruck von den mit den Datentypen zu erreichenden Spannweiten zu bekommen: LONG-Variablen können die Fakultät 12 darstellen, DOUBLE-Variablen die Fakultät 17 (als Ganzzahl !) und DECIMAL-Variablen die Fakultät 27.

Vielleicht stellt sich jetzt noch die Frage, wie die Zahlenwerte mit den vielen Nullen benannt sind (in Klammern: die abweichende Bezeichnungsweise im US-System):

10^6 Million 10^9 Milliarde (billion)
10^12 Billion (trillion) 10^15 Billiarde (quadrillion)
10^18 Trillion (quintillion) 10^21 Trilliarde (sextillion)
10^24 Quadrillion (septillion) 10^27 Quadrilliarde (octillion)
10^30 Quintillion (nonillion) 10^33 Quintilliarde (decillion)
10^36 Sextillion (undecillion) 10^39 Sextilliarde (duodecillion)
10^42 Septillion (tredecillion) 10^45 Septilliarde (quattuordecillion)
10^48 Oktillion (quindecillion) 10^51 Oktilliarde (sexdecillion)
10^54 Nonillion (septendecillion) 10^57 Nonilliarde (octodecillion)
10^60 Decillion (novemdecillion) 10^63 Decilliarde (vigintillion)

 

3. Das Verhalten des Datentyps DECIMAL beim Dateizugriff

Der sequentielle Dateizugriff durch WRITE / INPUT und der binäre bzw. wahlfreie Dateizugriff durch PUT / GET unterstützen auch den Datentyp DECIMAL.

Sequentieller Zugriff:

Wenn mit Write# Daten des Typs Variant/numerischer Untertyp sequentiell in eine Datei geschrieben werden, gelten einige Besonderheiten, damit diese Daten - unabhängig vom Gebietsschema - mit Input# wieder korrekt eingelesen und auf Variable zugewiesen werden können:Numerische Daten werden immer mit dem Punkt als Dezimaltrennzeichen geschrieben. Wenn Daten in der Ausgabeliste den Wert "Empty" haben, werden keine Daten in die Datei geschrieben. Für den Wert Null wird #NULL# in die Datei geschrieben.

Bei der sequentiellen Ausgabe mit WRITE# werden die DECIMAL-Daten unformatiert ausgegeben. Dabei werden jeweils nur so viele Ziffern ausgegeben, wie nötig (keine führenden oder angehängten Nullen). Die Zahlen in der Ausgabedatei können deshalb sehr unübersichtlich angeordnetsein. Bei Zahlen, die mit INPUT# wieder gelesen werden sollen, wird eine formatierte Ausgabe durch PRINT# in der VB-Dokumentation nicht empfohlen. Will man dennoch Zahlen formatiert ausgeben, muss zuvor das Dezimaltrennzeichen ausgetauscht werden und - falls mehrere Zahlen in einer Zeile stehen - als Daten-Trennzeichen das Komma eingefügt werden.

Beim sequentiellen Lesen mit INPUT# ist zu beachten, dass in Deutschland als Dezimaltrennzeichen in Datendateien oft ',' (Ascii-Code 44) und nicht der Dezimalpunkt '.' (Ascii-Code 46) verwendet wird. Vor dem Lesen aus solchen Dateien muss deshalb zunächst ein globales Ersetzen durchgeführt werden. Damit es dabei nicht zu Kollisionen mit dem Tausendertrennzeichen kommt, sollte zuvor ggf. noch der meist als Tausender-Trennzeichen verwendeteStandard-Dezimalpunkt aus der Datei entfernt werden.

Die Klasse "clsDecimal" enthält die beiden Methoden WRITEARRAY undINPUTARRAY für das sequentielle Schreiben und Lesen des gekapselten Array-Inhaltes. WRITEARRAY formatiert die ausgegebenen Zahlen so, dass der Dezimalpunkt in der Datei bei allen Daten eine feste Position besitzt. Eine durchWRITEARRAY gefüllte Datei kann mit INPUTARRAY wieder korrekt gelesen werden.

Bei überlangen Zahlenschlangen in einer sequentiellen Datei (>28 Ziffern) und bei Exponential-Zahlen über +/-1E28 löstINPUTARRAY die Ereignismeldung "Überlauf" aus. Nicht-Numerische Angaben in der Datei führen zur Ereignismeldung"Typen unverträglich". Beim ersten Auftreten eines Fehlers brichtINPUTARRAY den Lesevorgang ab und gibt "false" zurück. Exponentialzahlen werden beim Lesen in Dezimalzahlen umgewandelt (1.2E08 --> 120000000). Bei zu vielen Nachkommastellen wird gerundet (abhängig von der Zahl der Vorkommastellen). Leerzeilen in der Datei werden als cDec(0) ins Array eingetragen!!

Binärer Zugriff:

Der binäre Dateizugriff eignet sich besonders gut, um einen komplexen benutzerdefinierten Datentyp (UDT), der aus Strings, numerischen Typen, variablen Arrays und anderen UDTs zusammengesetzt sein kann, mit nur einer PUT-Anweisung in eine Datei zu schreiben, und mit nur einer GET-Anweisung alle Daten aus der Datei wieder einzulesen.

Ein typischer Anwendungsfall ist das Speichern der Member-Variablen einer Klasse bzw. das Füllen einer neuen Objektinstanz mit diesen Daten aus einer Datei.
(Objektverweise dürfen in einer derart verwendeten UDT nicht enthalten sein. Das wäre auch sinnlos, weil diese Verweise auf Hauptspeicher-Adressen bezogen sind, die sich dynamisch ändern. Ausgeschlossen sind auch VARIANT-Variablen, die ein Datenfeld enthalten.)

Wenn die zu schreibende UDT-Variable ein dynamisches Datenfeld enthält, schreibt PUT# zunächst einen Array-Deskriptor, dessen Länge 2 plus 8 mal Anzahl der Array-Dimensionen beträgt. 

Abfolge der Bytes in der Datei nach dem Schreiben eines zweidimensionalen dynamischen Arrays (Redim-Deklaration) durchPUT# :
2 Byte: Anzahl der Dimensionen des Array
4 Byte: Zahl der Indices (2. Dimension)
4 Byte: Index-Untergrenze der 2. Dimension
4 Byte: Zahl der Indices (1. Dimension)
4 Byte: Index-Untergrenze der 1. Dimension

Danach werden die Daten geschrieben 
(VARIANT/Untertyp-Decimal: (16 + 2) Byte pro Array-Feld)

Beim Schreiben einer UDT-Variable mit PUT# wird die Beachtung des folgenden Hinweises empfohlen:

Beim Lesen der Variable die Arrays enthält mit GET#, erwartet diese Funktion an den korrekten Stellen die oben aufgelisteten Angaben zur korrekten Dimensionierung des Array. Verwendet man beim Lesen eine Datei, die nicht mit einer korrespondierenden PUT#-Anweisung binär geschrieben worden ist, kann es beim Lesen mit GET# zum Versuch der Dimensionierung riesiger Felder kommen. Das kann unabsehbare Effekte nach sich ziehen. Man schreibt deshalb mit PUT# zunächst einen eindeutigen Kennstring in die Datei, erst dann die Variable vom benutzerdefinierten Typ und zum Abschluss noch einmal den Kennstring. Beim Lesen mit GET# wird zunächst der Kennstring am Anfang und am Ende der Datei gelesen und geprüft. Falls durch diese Prüfung die Korrektheit der Datei gesichert ist, kann mit GET die Typvariable eingelesen werden. 

Die Klasse "clsDecimal" enthält die Methoden PUTARRAY / GETARRAY, die dieses Vorgehen demonstrieren.

(Wer absolut sicher gehen will, keine inadäquaten Daten zu lesen, kann nach dem Schreiben der Datei zusätzlich eine Prüfsumme für den Dateiinhalt berechnen und -z. B. angehängt am Dateinamen - abspeichern. Vor dem Lesen einer solchen Datei wird zunächst die Prüfsumme kontrolliert und mit dem gespeicherten Wert verglichen.)

Wahlfreier Zugriff:

Beim wahlfreien Zugriff auf Datensätze einer Datei ist die korrekte Angabe der Datensatzlänge beim Öffnen der Datei zwingend notwendig.

Ist die geschriebene Variable vom Typ VARIANT / numerischer Untertyp, so schreibt PUT zunächst 2 Bytes, die den Untertypkennzeichnen und dann den Inhalt der Variable. Bei einer VARIANT-Variablen des Typs 14 (DECIMAL) schreibt PUT 18 Bytes: 2 Bytes legen die VARIANT-Variable als DECIMAL fest, und 16 Bytes enthalten die Daten. Die im Len-Abschnitt der Open-Anweisung angegebene Datensatzlänge muss also mindestens 2 Bytes größer sein als die tatsächlich zum Speichern der Variablen benötigte Anzahl an Bytes. 

Wenn die Zielvariable ein dynamisches Datenfeld ist, schreibt PUT zuerst einen Deskriptor, dessen Länge 2 plus 8 mal der Anzahl der Dimensionen entspricht. Die durch den Len-Abschnitt der Open-Anweisung angegebene Datensatzlänge muss größer oder gleich der Summe aller Bytes sein, die erforderlich sind, um die Daten des Datenfeldes und den Datenfeld-Deskriptor zu schreiben. 

Beim Lesen von Dateien durch wahlfreien Zugriff mit UDT-Variablen die dynamisch deklarierte Arrays enthalten, kann es zu den gleichen Effekten kommen wie beim binären Zugriff (vgl. oben). Es sollten deshalb beim wahlfreien Datei-Zugriff immer"statische Arrays" verwendet werden. 

4. Kurzbeschreibung der Eigenschaften und Methoden der Klasse "clsDecimal"

Die Klasse "clsDecimal" kapselt ein eindimensionales Bytearray, dessen Felder Zahlen des Typs DECIMAL aufnehmen. Fehlerhafte Zugriffe oder interne Fehler lösen das Ereignis <FehlZugriff> aus, falls die Zugriffs-Variable der Klasse modulglobal mit dem Schlüsselwort WITHEVENTS deklariert worden ist. 

Intern werden die Daten in einem zweidimensionalen Byte-Array verwaltet (13 Byte / Feld). Es können nur Variablen vom Typ DECIMAL zugewiesen werden.
Für eine schnelle Verwaltung des Arrays wird das Verschieben der Daten durch die Windows-API-Funktion"RtlMoveMemory" realisiert. Die Methode "EINFÜGEN" erlaubt den schnellen und gezielten Datentransfer zwischen verschiedenen Instanzen dieser Klasse (Kopieren, Verschieben, Überschreiben, Zusammenfassen von Daten in einem Teilbereich desArrays).

Methode: DIMENSIONIEREN(Long, Long, [Boolean])
Initialisierung und Dimensionierung des Datenfeldes
1. Parameter: Index-Untergrenze des Array; 2. Parameter: Obergrenze 
Der 3. Parameter legt fest, ob das Array mit 0 oder mit Zufallszahlen initialisiert ist.
true: Dimensionierung erfolgreich / false: Dimensionierung fehlgeschlagen
Erst nach der korrekten Ausführung dieser Methode stehen auch die anderen Methoden der Klasse zur Verfügung. 

Eigenschaft: VERSION
Ausgabe der Versionskennung der Klasse

Methode: CLEAR
Das Datenfeld wird gelöscht.

Eigenschaft: ISTDIMENSIONIERT
Abfrage, ob das Array dimensioniert ist

Eigenschaft: OBERGRENZE / UNTERGRENZE
schreibgeschützt: aktuelle Grenzen des Array
0, falls das Datenfeld noch nicht dimensioniert worden ist

Eigenschaft: LIMIT
schreibgeschützt: zulässige numerische Obergrenze (absolut) für Daten, die in Variablen des Typs DECIMAL gespeichert werden können

Eigenschaft: NUMERISCH_TOLERANT
Falls diese Eigenschaft auf true gesetzt wird (Voreinstellung false) akzeptiert die Instanz der Klasse die Zuweisung von Werten, die numerisch interpretierbar sind und im Geltungsbereich des DatentypsDECIMAL liegen - sonst nur explizite DECIMAL-Werte

Eigenschaft: ZUGRIFF(Long, Variant)
Abfrage bzw. Zuweisung eines numerischen Wertes auf das Arrayfeld an Position"Index"
Diese Eigenschaft sollte als <Voreinstellung> für die Klasse definiert werden

Methode: OBERGRENZE_NEU(Long)
Neue Festlegung der Obergrenze eines initialisierten Array. Der Wert darf nicht kleiner als die UNTERGRENZE sein.
Die Methode führt intern ein REDIM PRESERVE für das gekapselte Array durch. Ist die neue Obergrenze niedriger als die alte, gehen die betroffenen Daten verloren.

Eigenschaft: ARRAY_POINTER
schreibgeschützt: Abfrage der Speicher-Startposition (VARPTR) des Arrays (gerufen von der Methode EINFÜGEN).
0, falls das Array noch nicht dimensioniert worden ist.

Methode: EINFÜGEN(clsDecimal, [Long], [Long], [Variant])
Die Methode fügt den Inhalt der übergebenen Klasse (1. Parameter) ab dem Wert ein, der im vierten Parameter angegeben worden ist.
Der zweite und dritte Parameter definieren den Bereich der übergebenen Klasse, der eingetragen werden soll.
Falls der dritte Parameter größer als der zweite ist (= keine Angaben) wird der gesamte Inhalt eingetragen.
Falls der vierte Parameter um 1 größer ist, als die aktuelle Obergrenze des Zielarrays wird hinten angehängt.
Falls das Zielarray nicht initialisiert ist, wird es zunächst neu dimensioniert (2./3. Parameter) und mit den entsprechenden Daten aus der Quell-Klasse gefüllt.
true: Übertragung funktioniert / false: Übertragung fehlgeschlagen

Methode: LÖSCHEN([Long], [Long])
Der angegebene Bereich des Array wird gelöscht und das Array entsprechend verkleinert.
Fehlen die Parameter oder ist der 2. größer als der 1. wird das Array gelöscht.

Methode: ISTBEREICHIDENTISCH([Long], [Long], [clsDecimal], [Long], [Long])
Die Methode überprüft, ob ein Array-Bereich, der in den beiden ersten Parametern vorgegeben wird, identisch ist mit dem Bereich der Klasse im 3. Parameter, der durch den vierten und fünften Parameter festgelegt ist.
Der dritte Parameter muss angegeben werden (kein optionaler Parameter!)
Falls die anderen Parameter weggelassen werden, wird jeweils der gesamte Array-Inhalt für den Vergleich herangezogen.
true: identischer Bereich / false: Unterschied(e) oder ungeeignete Parameter 

Methode: WRITEARRAY(String, [Boolean])
Der aktuelle Inhalt des Arrays wird sequentiell in eine Datei geschrieben
Die Ausgabedatei wird ggf. überschrieben (2. Parameter false) oder es wird angehängt (2. Parameter true)
true: Speichern funktioniert / false: Speichern fehlgeschlagen

Methode: INPUTARRAY(String)
Die numerischen Werte in einer Datei werden durch INPUT# sequentiell gelesen.
Nach dem Lesen informiert OBERGRENZE über die Zahl der gelesenen Werte.
Die Methode löscht den aktuellen Array-Inhalt.
true: Laden funktioniert / false: Laden fehlgeschlagen

Methode: PUTARRAY(String, Boolean)
Der aktuelle Inhalt des Arrays wird binär durch PUT# in eine Datei geschrieben
Die Ausgabedatei wird ggf. überschrieben (2. Parameter false) oder es wird angehängt (2. Parameter true)
true: Speichern funktioniert / false: Speichern fehlgeschlagen

Methode GETARRAY(String)
Die Methode löscht den aktuellen Array-Inhalt.
Die numerischen Werte in einer Datei werden durch GET# binär gelesen.
Nach dem Lesen informiert UNTERGRENZE / OBERGRENZE über die Dimensionierung des gelesenenArrays.
true: Laden funktioniert / false: Laden fehlgeschlagen

Inhalt des Demoprojekts

  • clsDecimal.cls: Quellcode der Klasse 'clsDecimal'
  • frmDecimal_Demo.frm: Quellcode Demonstrationsbeispiel
  • frmDecimalCheck.frm: Quellcode Testprozedur für 'clsDecimal'

Die Klasse "clsDecimal" macht ausgiebig Gebrauch von der berüchtigten API-Funktion "RtlMoveMemory". Es ist deshalb eine Testprozedur erstellt worden, die 10000x eine Instanz der Klasse erstellt, zufällig dimensioniert, mit Zufalls-Werten gefüllt und dann die Methoden der Klasse ausgeführt hat. Diesen Test hat das Klassenmodul bestanden. Zur Dokumentation ist die verwendete Testroutine dem Zip-File beigefügt (frmDecimalCheck).

Dieser Workshop wurde bereits 26.210 mal aufgerufen.

Über diesen Workshop im Forum diskutieren
Haben Sie Fragen oder Anregungen zu diesem Workshop, können Sie gerne mit anderen darüber in unserem Forum diskutieren.

Neue Diskussion eröffnen

nach obenzurück


Anzeige

Kauftipp Unser Dauerbrenner!Diesen und auch alle anderen Workshops finden Sie auch auf unserer aktuellen vb@rchiv  Vol.6
(einschl. Beispielprojekt!)

Ein absolutes Muss - Geballtes Wissen aus mehr als 8 Jahren vb@rchiv!
- nahezu alle Tipps & Tricks und Workshops mit Beispielprojekten
- Symbol-Galerie mit mehr als 3.200 Icons im modernen Look
Weitere Infos - 4 Entwickler-Vollversionen (u.a. sevFTP für .NET), Online-Update-Funktion u.v.m.
 
   

Druckansicht Druckansicht Copyright ©2000-2024 vb@rchiv Dieter Otter
Alle Rechte vorbehalten.
Microsoft, Windows und Visual Basic sind entweder eingetragene Marken oder Marken der Microsoft Corporation in den USA und/oder anderen Ländern. Weitere auf dieser Homepage aufgeführten Produkt- und Firmennamen können geschützte Marken ihrer jeweiligen Inhaber sein.

Diese Seiten wurden optimiert für eine Bildschirmauflösung von mind. 1280x1024 Pixel