Access-o-Mania

Access-Forum (Deutsch/German) => Access-Hilfe => Thema gestartet von: crystal am August 12, 2016, 18:44:05

Titel: Natürliche Sortierung
Beitrag von: crystal am August 12, 2016, 18:44:05
Hallo Leute,

(endlich ist das Forum wieder online.)

Meine Frage:
Gibt es eine Möglichkeit, die Sortierung in Access auf 'natürlich' einzustellen?
Der Explorer sortiert z. B. so:
datei 1
datei 2
...
datei 9
datei 10
datei 11
...

Access hingegen sortiert binär, also
datei 1
datei 10
datei 11
datei 2
...
datei 9

Ich habe keine Einstellung in Access 2016 gefunden, wo man das umstellen könnte. Bleibt als einzige Möglichkeit nur, führende Nullen einzufügen, also
datei 01
datei 02
...
datei 09
datei 10
datei 11

oder gibt es eine andere Lösung?

Edit 1. Okt. 2016: habe den Thread-Titel auf "Natürliche Sortierung" geändert
Titel: Re: Sortierung
Beitrag von: MzKlMu am August 12, 2016, 19:08:29
Hallo,
Du kannst über eine Abfrage die Zahl in ein extra Feld extrahieren (als Zahl!) und dann nach der Zahl sortieren.

Wie genau ist denn der Dateistring aufgebaut ?
Gibt es immer ein Leerzeichen vor der Zahl ?
Was steht denn genau in den Feldern, ein Dateiname ?
Titel: Re: Sortierung
Beitrag von: Lachtaube am August 12, 2016, 21:36:48
In Access kann man nur pfuschen - ihm fehlen eigene Datentypen und Arrays nach denen sortiert werden kann. Außerdem können keine Indexe auf Ausdrücken beruhen. D. h. die nachfolgende Funktion wird mit zunehmender Datenmenge und zunehmender Verschachtelung Abfragen langsam machen.

Man lege einen VBA-Verweis auf die Microsoft VBScript Regular Expressions 5.5 Bibliothek an und stelle folgende Funktion in einem allgemeinen Modul zur Verfügung.Public Function GetSortPart$(ByVal Value, Optional ByVal PartNum& = 1)

   Static RegExp As New VBScript_RegExp_55.RegExp
   Dim matchPart$

   If Nz(Value) = vbNullString Or PartNum < 1 Or PartNum > 3 Then
      GetSortPart = "0"
   Else

      With RegExp
         '// 1) Nicht-Ziffernfolge feststellen 0..n (soviel wie möglich)
         '// 2) Ziffernfolge feststellen 0..n (soviel wie möglich)
         '// 3) der Rest
         .Pattern = "(\D*)(\d+)(.*)"
         '// Treffer auslesen
         matchPart = .Execute(Value)(0).submatches(PartNum - 1)

         Select Case PartNum
         Case 1, 2
            If Len(matchPart) Then
               '// Zeichen- oder Ziffernfolge zurückgeben
               GetSortPart = matchPart
            Else
               '// reicht zum Sortieren und kann in eine Zahl umgewandelt werden
               GetSortPart = "0"
            End If
         Case 3
            '// wir wollen den Rest hinter der Ziffernfolge
            GetSortPart = matchPart
         End Select
      End With
   End If
End Function

Angewendet auf eine Tabelle data|----+---------------|
| id | filename      |
|----+---------------|
|  1 | datei 1       |
|  2 | datei 2       |
|  3 | datei 3       |
|  4 | datei 11      |
|  5 | datei 10      |
|  6 | datei 22      |
|  7 | datei 13      |
|  8 | datei 14      |
|  9 | datei15       |
| 10 | datei7        |
| 11 | datei  19     |
| 12 | datei    1116 |
| 13 | datei      5  |
| 14 | datei  3.2    |
| 15 | datei 3.0.1   |
| 16 | datei3.0.0    |
| 17 | datei3.0.2    |
| 18 | datei   3.1.1 |
| 19 | 4   datei 28  |
| 20 | 3datei5.1     |
|----+---------------|
sortiert diese Abfrage dannSELECT d.id,
       d.filename
  FROM data AS d
ORDER BY Trim(GetSortPart([filename])),
          CLng(GetSortPart([filename], 2)),
          CLng(GetSortPart(GetSortPart([filename], 3), 2)),
          CLng(GetSortPart(GetSortPart(GetSortPart([filename], 3), 3), 2));
die Daten zu diesem Ergebnis:|----+---------------|
| id | filename      |
|----+---------------|
| 20 | 3datei5.1     |
| 19 | 4   datei 28  |
|  1 | datei 1       |
|  2 | datei 2       |
| 16 | datei3.0.0    |
|  3 | datei 3       |
| 15 | datei 3.0.1   |
| 17 | datei3.0.2    |
| 18 | datei   3.1.1 |
| 14 | datei  3.2    |
| 13 | datei      5  |
| 10 | datei7        |
|  5 | datei 10      |
|  4 | datei 11      |
|  7 | datei 13      |
|  8 | datei 14      |
|  9 | datei15       |
| 11 | datei  19     |
|  6 | datei 22      |
| 12 | datei    1116 |
|----+---------------|

PS: in der Praxis wird man vermutlich kaum so verrückte Leerzeichen wie im Beispiel haben.
Titel: Re: Sortierung
Beitrag von: MzKlMu am August 12, 2016, 23:14:59
Hallo,
ist das nicht mit Kanonen auf Spatzen geschossen ?
Man weis ja auch nicht ob das wirklich um Dateinamen geht. Es war ja möglicherweise nur ein Beispiel.
Es wäre ja vielleicht auch sinnvoll, das in der Tabelle gleich auf 2 Felder aufzuteilen.
Und wenn wie dargestellt, die Zahl hinten steht mit einem Leerzeichen davor, kann man die Zahl mit Mid(x,y) abschneiden und als Zahl sortieren.

Daher wäre erst mal abzuwarten, bis man die angefragten Zusammenhänge/Hintergründe kennt.
Titel: Re: Sortierung
Beitrag von: Lachtaube am August 13, 2016, 08:59:48
Nun, die Fragestellung ist ja nach natürlicher Sortierung ausgerichtet - egal wie und wo Ziffern angeordnet sind. Und da ist Access einfach nicht mehr zeitgemäß mit seinem uralten SQL-Dialekt. Wie eine moderne Datenbank eine natürliche Sortierung implementieren kann, ist in diesem Beispiel (http://sqlfiddle.com/#!15/adb3c/3) zu sehen.
Titel: Re: Sortierung
Beitrag von: PhilS am August 13, 2016, 10:18:31
Zitat von: Lachtaube am August 13, 2016, 08:59:48Und da ist Access einfach nicht mehr zeitgemäß mit seinem uralten SQL-Dialekt. Wie eine moderne Datenbank eine natürliche Sortierung implementieren kann, ist in diesem Beispiel (http://sqlfiddle.com/#!15/adb3c/3) zu sehen.
Das SQL Fiddle zeigt eine benutzerdefinierte Funktion. Der Implementierungsansatz ist genau der gleiche wie in deinem (übrigens sehr guten) Beispiel oben.

Die einzigen, mittelbaren Unterschiede sind, dass du mit einem Server-DBMS (SQL Fiddle war für PostgreSQL) die Funktion bereits serverseitig implementieren kannst, und, unter gewissen Umständen, auch die Ergebnisse indizieren kannst.

Access kann aufgrund seiner Architektur als Desktop-Datenbank beides nicht. Das hat aber nichts mit dem SQL-Dialekt und seiner "Modernität" zu tun.
Titel: Re: Sortierung
Beitrag von: Lachtaube am August 13, 2016, 11:16:38
Ob Desktop- oder Server-Datenbanksystem dürfte für die Implementierung von Funktionalitäten eher eine untergeordnete Rolle spielen. SQLite3 ist auch eine reine Desktop-Datenbank und weist eine Schnittstelle für Collating Sequences (http://www.sqlite.org/c3ref/create_collation.html) auf (klar, auch hier muss man die Sortierung selbst implementieren).
Titel: Re: Sortierung
Beitrag von: PhilS am August 13, 2016, 14:13:55
Zitat von: Lachtaube am August 13, 2016, 11:16:38
Ob Desktop- oder Server-Datenbanksystem dürfte für die Implementierung von Funktionalitäten eher eine untergeordnete Rolle spielen.
Nein, keineswegs. Eine Desktop-Datenbank führt, per Definition, die DB-Engine auf dem Client (Desktop) aus. Eine serverseitige Implementierung jedweder Funktionalität ist damit ausgeschlossen.

Eine Indizierung von funktionsbasierten, virtuellen Spalten ist auch bei den DBMS, die das grundsätzlich können, nur mit sehr strikten Einschränkungen möglich. - Mit PostgreSQL kenne ich mich kaum aus, würde aber aus wager Erinnerung dort dazu das Stichwort Purity Level anführen.

In Access/VBA ist eine Überprüfung von benutzerdefinierten VBA-Funktionen auf die dafür erforderlichen Merkmale (z.B. Determinismus) aufgrund des durch externe Bibliotheken praktisch unbegrenzten Funktionsumfangs schlichtweg unmöglich.

Zitat von: Lachtaube am August 13, 2016, 11:16:38
SQLite3 ist auch eine reine Desktop-Datenbank und weist eine Schnittstelle für Collating Sequences (http://www.sqlite.org/c3ref/create_collation.html) auf (klar, auch hier muss man die Sortierung selbst implementieren).
Das ist eine schöne Möglichkeit spezifisch Sortierreihenfolgen zu implementieren. Läuft aber, soweit ich das ohne SQLLite-Kenntnisse beurteilen kann, vom Endergebnis aus das gleiche Hinaus, wie dein Access/VBA-Beispiel.

Natürlich wäre es wünschenswert, wenn der Funktionsumfang von Access/ACE/Jet größer wäre. Die konkreten Punkte, die du hier beanstandest, haben jedoch nichts damit zu tun, dass Access "nicht modern" ist, sondern beruhen auf grundsätzlichen Architekturentscheidungen in der Implementierung der DB-Engine. Andere System mit einer vergleichbaren Architektur haben (bzw. hätten, ich weiß nicht ob es sie wirklich gibt) ähnliche Einschränkungen.
Titel: Re: Sortierung
Beitrag von: Lachtaube am August 13, 2016, 23:12:08
Hier wäre eine Umsetzung für eine SQLite3-Datenbank. Der Code verwendet Olaf Schmidts hervorragendes vbRichClient-Framework (http://vbrichclient.com) mit einem VBA-Verweis auf vbRichClient5.

Zunächst wird eine Klasse für die Implementierung unserer Custom Collation benötigt.'// KlassenModul: AdditionalCollations

Option Explicit

Implements ICollation

Private NC As cStringCompare, LCID_DE&

Private Sub Class_Initialize()
  '// Wrapper um die API-Funktion CompareString
  Set NC = New_c.StringCompare
  LCID_DE = NC.MakeLCID(German_Germany)
End Sub

Private Function ICollation_CallbackCollate(ByVal ZeroBasedNameIndex As Long, _
                                            S1 As String, S2 As String) As Long
   Const cmpDigitalsAsNumbers = 8

   Select Case ZeroBasedNameIndex
   Case 0
      '// Sortierung wie im "Explorer"
      ICollation_CallbackCollate = NC.CompareString(S1, S2, _
                                                    cmpIgnoreCase Or _
                                                    cmpDigitalsAsNumbers, LCID_DE)
   End Select
End Function

Private Property Get ICollation_DefinedNames() As String
   '// Name der Sortierung festlegen
   ICollation_DefinedNames = "DE_NC"
End Property
Der Test erfolgt in einem allgemeinen Modul.Option Compare Database
Option Explicit


Public Sub TestNaturalCollation()

   Dim cnn As cConnection, va, v

   '// Beispieldaten
   va = Array(Array("street", "9, James street"), _
              Array("street", "10, James street"), _
              Array("street", "10 ter, James street"), _
              Array("street", "10 bis"), _
              Array("street", "James street"), _
              Array("street", "5, Rudolph street"), _
              Array("version", "v1.10.1"), Array("version", "v1.10.1.10"), _
              Array("version", "v1.10.1.9"), Array("version", "v1.10.1.9.7"), _
              Array("version", "v1.10.1.9.14"), Array("version", "v1.9", "v1.10"))

   '// Memory-Datenbank erstellen
   Set cnn = New_c.Connection(OpenMode:=DBCreateInMemory)
   '// unsere Sortierung hinzufügen
   cnn.AddUserDefinedCollation New AdditionalCollations
   '// Tabelle erzeugen
   cnn.Execute "CREATE TABLE t (grp text not null, txt text not null);)"
   '// für Memory-Datenbank nicht relevant
   'cnn.BeginTrans
   '// Anfügeabfrage anlegen
   With cnn.CreateCommand("INSERT INTO t (grp, txt) VALUES (?, ?);")
      '// Beispieldaten auslesen
      For Each v In va
         '// Parameter setzen
         .SetText 1, CStr(v(0)): .SetText 2, CStr(v(1))
         '// Daten in Tabelle einfügen
         .Execute
      Next
   End With
   'cnn.CommitTrans

   '// ADODB-Recordset erzeugen
   With cnn.OpenRecordset("SELECT * FROM t ORDER BY 1, 2 COLLATE DE_NC;") _
        .GetADORsFromContent
      '// sortierte Ausgabe in Direktbereich schreiben
      Debug.Print .GetString(, , vbTab, vbCrLf)
   End With
End Sub
Und hier dann das Ergebnis:street   5, Rudolph street
street   9, James street
street   10 bis
street   10 ter, James street
street   10, James street
street   James street
version  v1.9
version  v1.10.1
version  v1.10.1.9
version  v1.10.1.9.7
version  v1.10.1.9.14
version  v1.10.1.10
Titel: Re: Sortierung
Beitrag von: crystal am August 16, 2016, 11:44:46
Hallo!
Sorry, dass ich erst jetzt antworte, war leider krank.

In der Tat ist mein Beispiel wohl zu stark vereinfacht, es sollte nur dazu dienen, die "natürliche" Explorer-Sortierung darzustellen. Das Wort "datei" ist keineswegs konstant, die Zahlen existieren nicht immer, es gibt also auch Daten ohne Zahlen.

Die Idee, eine API-Funktion zu nutzen, gefällt mir sehr gut, denn immerhin schafft es der Explorer ja auch.

Ich tendiere dazu, ein zusätzliches Feld "Sortierung" in meine Tabelle einzufügen und die Originaldaten in passende Sortierdaten zu wandeln. Das wäre vielleicht aus Performancegründen sinnvoll.

Erstmal vielen Dank für die Beispiele.

Titel: Re: Sortierung
Beitrag von: PhilS am August 16, 2016, 14:13:34
Zitat von: crystal am August 16, 2016, 11:44:46Die Idee, eine API-Funktion zu nutzen, gefällt mir sehr gut, denn immerhin schafft es der Explorer ja auch.

Die API-Funktion, die der Windows Explorer dafür verwendet ist StrCmpLogicalW (https://msdn.microsoft.com/en-us/library/bb759947.aspx). - Allerdings habe ich im Moment keine Idee, wie man diese sinnvoll im Kontext einer Access-Abfrage einsetzen könnte.
Titel: Re: Sortierung
Beitrag von: crystal am August 17, 2016, 10:57:18
Hallo PhilS,
ZitatAllerdings habe ich im Moment keine Idee, wie man diese sinnvoll im Kontext einer Access-Abfrage einsetzen könnte.

So geht' mir auch. Die einzige Möglichkeit besteht wohl darin, einen Sotierwert zusätzlich in der Tabelle zu führen.

Ich fand dafür LCMapStringEx, https://msdn.microsoft.com/en-us/library/windows/desktop/dd318702(v=vs.85).aspx

Diese Funktion konvertiert einen String entsprechend der Spracheinstellung und kennt u.a. auch das Flag SORT_DIGITSASNUMBERS beim Flag LCMAP_SORTKEY.

Aber wie kann ich LCMapStringEx in VBA benutzen? Vielleicht kannst du ein Beispiel bauen, denn wie man einen Wrapper definiert, ist zu viel für mich. Es müsste eine Funktion sein, der man den Originalstring übergibt und die dann den konvertierten String zurückgibt. Für meine Zwecke könnten Flags und andere Parameter hart gesetzt sein.



Titel: Re: Sortierung
Beitrag von: Lachtaube am August 17, 2016, 12:50:33
Die Desktop-Datenbank Access hat keine öffentliche Schnittstelle, um CompareString, CompareStringEx, LCMapStringEx, lstrcmp, lstrcmp direkt nutzen zu können. Was damit geht, ist, ein Datenfeld (Array) in VBA zu sortieren.

Wie groß ist denn der Datenbestand der Tabelle, und ist Access als Backend in Stein gemeißelt?
Titel: Re: Sortierung
Beitrag von: crystal am August 17, 2016, 14:29:19
Hallo Lachtaube,
Ja, ich weiß, daß Access keine "offizielle" Schnittstelle dafür hat.

Aber ich denke doch, dass es prinzipiell möglich wäre, die genannte API-Funktion aufzurufen und zu nutzen, um einmalig alle Daten meines Datenfeldes in ein zusätzliches Sortierfeld in der Tabelle zu kopieren.

Ich will also nicht ein
"ORDER BY sortnatural(tabellenfeld)",
sondern ein
"ORDER BY sortfeld"
erreichen, wobei "tabellenfeld" den Originalstring und "sortfeld" einen sortierbar konvertierten String enthält.

Ein solches Vorgehen wird übrigens auch im MSDN beim Thema "Sorting" aus Performance-Gründen empfohlen, was ja auch offensichtlich und naheliegend ist.

Um dieses "sortfeld" zu erzeugen, dachte ich mir, es wäre vielleicht sinnvoll, LCMapStringEx zu benutzen, statt eine eigene Funktion zu bauen.

LCMapStringEx übrigens sortiert nicht, sondern "übersetzt" nur einen String in einen sortierbaren Wert, der dann mit binärem, byte- oder sogar bitweisem Vergleich sortiert werden kann.

Ähnliches könnte ich für die (zu stark abstrahierten) Daten aus meinem initialen Beispiel erreichen, indem ich eine Funktion baue, die die zu sortierenden Werte schlicht mit ausreichen vielen führenden Nullen ergänzt. Aber LCMapStringEx ist ja wohl auch in der Lage, eine entspr. Übersetzung für landesspezifische Zeichen (z.B. Umlaute) zu machen. Warum also das Rad neu erfinden, statt diese API-Funktion einfach zu benutzen?

Ich bin nur leider nicht in der Lage, einen VBA-Wrapper für LCMapStringEx zu bauen.

Meine Tabelle besteht momentan aus knapp 10.000 Datensätzen, Tendenz steigend...
Titel: Re: Sortierung
Beitrag von: daolix am August 17, 2016, 14:47:27
Hallo
hier ein Ansatz, ist aber so nicht voll funktionsfähig, evtl noch ein wenig mit den flag spielen:

Private Declare Function LCMapStringEx Lib "KERNEL32.DLL" _
    ( _
        ByVal lpLocaleName As Long, _
        ByVal dwMapFlags As Long, _
        ByVal lpSrcStr As Long, _
        ByVal cchSrc As Long, _
        ByVal lpDestStr As Long, _
        ByVal cchDest As Long, _
        ByVal lpVersionInformation As Long, _
        ByVal lpReserved As Long, _
        ByVal lParam As Long _
) As Long

Private Declare Function GetLocaleInfo Lib "kernel32" Alias "GetLocaleInfoW" _
    ( _
        ByVal Locale As Long, _
        ByVal LCType As Long, _
        ByVal lpLCData As Long, _
        ByVal cchData As Long _
    ) As Long
   
Private Declare Function GetUserDefaultLCID Lib "kernel32" () As Long

Private Function GetLocale(lLocaleEnum&, Optional ByVal lLCID As Long) As String
    Dim lRet As Long
    Dim sret As String
    If lLCID = 0 Then lLCID = GetUserDefaultLCID()
    lRet = GetLocaleInfo(lLCID, lLocaleEnum&, StrPtr(sret), 0)
    sret = String(lRet, 0)
    lRet = GetLocaleInfo(lLCID, lLocaleEnum&, StrPtr(sret), lRet)
    If lRet Then GetLocale = Left(sret, lRet - 1)
End Function


Public Function GetSortKeyString(ByVal stxt As String, Optional ByVal LCID As Long = 1031) As String
    Dim sLocale$
    Dim sret$
    Dim lRet&
    Const lFlag& = &H400 Or &H8
   
    sLocale = GetLocale(92, LCID)
    lRet = LCMapStringEx(StrPtr(sLocale), lFlag, StrPtr(stxt), Len(stxt), StrPtr(sret$), 0, 0, 0, 0)
    If lRet Then
        sret$ = String$(lRet, 0)
        lRet = LCMapStringEx(StrPtr(sLocale), lFlag, StrPtr(stxt), Len(stxt), StrPtr(sret$), lRet, 0, 0, 0)
        GetSortKeyString = Left(sret, lRet)
    End If
End Function


die Funktion "GetSortKeyString" dann mal in deiner Abfrage aufrufen


PS: das flag müsste wohl lauten : &H400 Or &H8 Or &H800
Titel: Re: Sortierung
Beitrag von: crystal am August 17, 2016, 15:08:02
Hi daolix,

von wegen "von nix nen plan", wie du in deinem Profil schreibst...

Das ist genau, was ich mir vorgestellt habe. Allerdings werde ich deine Funktion "GetSortKeyString" nicht in einer Abfrage aufrufen, sondern um das zusätzliche "sortfeld" in meiner Tabelle zu erzeugen.

Ich werde berichten, ob und wie es geklappt hat.

Danke für deine Mühe, diesen Wrapper zu schreiben und dabei sogar noch GetLocaleInfo etc. zu berücksichtigen!

Scheint mir insgesamt eine excellente Lösung zu sein!

Gruß,

crystal
Titel: Re: Sortierung
Beitrag von: daolix am August 17, 2016, 15:30:34
Wie gesagt mit den Flags musst du noch ein wenig spielen, denn immo  wird nicht unterschieden zw z.b.  "D 9" und "D9", ausser du definierst eine sekundäre Sortierung  ala "..ORDER BY sortfeld, Tabellenfeld
Titel: Re: Sortierung
Beitrag von: PhilS am August 17, 2016, 15:42:31
Zitat von: daolix am August 17, 2016, 14:47:27die Funktion "GetSortKeyString" dann mal in deiner Abfrage aufrufen

PS: das flag müsste wohl lauten : &H400 Or &H8 Or &H800
Mit den Konstanten ist es weniger kryptisch:
Private Const LCMAP_SORTKEY = &H400                   '  WC sort key (normalize)
Private Const LCMAP_BYTEREV = &H800                   '  byte reversal
Private Const SORT_DIGITSASNUMBERS     As Long = &H8         '  use digits as numbers sort method


Ich habe auch ein wenig mit der LCMapStringEx-Function gespielt. - Grundsätzlich genau der richtige Ansatz. Ich sehe nur ein grundlegendes Problem in Verbindung mit Access als Backend.

LCMapStringEx generiert als Sortkey ein Byte-Array. Access kann nicht über Binärfelder sortieren. Wenn man das Byte-Array jetzt als String behandelt, verwendet Access für die Sortierung die jeweils für die DB eingestellt Sortierreihenfolge. Dabei kann es aber IMO passieren, dass die Sortierung am Ende nicht der binären Sortierung der Bytes entspricht und damit nicht mehr korrekt ist.

Übersehe ich etwas?
Titel: Re: Sortierung
Beitrag von: daolix am August 17, 2016, 16:00:29
ZitatÜbersehe ich etwas?
Nö, du hast recht, wie ich gerade übelst feststellen durfte.

ZitatMit den Konstanten ist es weniger kryptisch:
Dat ist wohl war, is halt die faulheit die siegt beim Übernehmen der Werte aus nen anderen dialekt.
Titel: Re: Sortierung
Beitrag von: PhilS am August 17, 2016, 16:35:16
Ich glaube dafür ist mir eine Lösung eingefallen:
Private Function GetByteString(buffer() As Byte, ByVal buffLen As Long) As String
    Dim retVal As String
    Dim i As Long
   
    For i = 0 To buffLen - 1
        retVal = retVal & Format(buffer(i), "000") & ","
    Next i

    GetByteString = retVal
End Function

Mit der Function fülle ich die Byte-werte immer auf drei Stellen mit Nullen auf und hänge sie als String hintereinander. - Das sollte funktionieren.

Das Endergebnis entspricht allerdings noch nicht so ganz meinen Erwartungen. (Siehe Screenshot).



Titel: Re: Sortierung
Beitrag von: Lachtaube am August 17, 2016, 18:20:12
Was wiederum geht, ist, ein Binärfeld anzulegen, dessen Kapazität allerding maximal 510 Bytes umfasst. Das Ergebnis aus LCMapStringEx lässt sich dann innerhalb der Größenbeschrnkung ablegen, und das Binärfeld kann zum Sortieren verwendet werden.Option Explicit

'int LCMapStringEx(
'  _In_opt_  LPCWSTR          lpLocaleName,
'  _In_      DWORD            dwMapFlags,
'  _In_      LPCWSTR          lpSrcStr,
'  _In_      int              cchSrc,
'  _Out_opt_ LPWSTR           lpDestStr,
'  _In_      int              cchDest,
'  _In_opt_  LPNLSVERSIONINFO lpVersionInformation,
'  _In_opt_  LPVOID           lpReserved,
'  _In_opt_  LPARAM           sortHandle
');

Private Const LOCALE_NAME_USER_DEFAULT$ = vbNullString
Private Const LCMAP_SORTKEY& = &H400            ' // WC sort key (normalize)
Private Const SORT_DIGITSASNUMBERS& = &H8       ' // use digits as numbers sort method

#If VBA7 Then
Private Declare PtrSafe Function LCMapStringEx Lib "kernel32" ( _
   ByVal lpLocaleName As LongPtr _
, ByVal dwMapFlags As Long _
, ByVal lpSrcStr As LongPtr _
, ByVal cchSrc As Long _
, ByVal lpDestStr As LongPtr _
, ByVal cchDest As Long _
, Optional ByVal lpVersionInformation As LongPtr _
, Optional ByVal lpReserved As LongPtr _
, Optional ByVal lParam As Long _
) As Long
#Else
Private Declare Function LCMapStringEx Lib "kernel32" ( _
   ByVal lpLocaleName As Long _
, ByVal dwMapFlags As Long _
, ByVal lpSrcStr As Long _
, ByVal cchSrc As Long _
, ByVal lpDestStr As Long _
, ByVal cchDest As Long _
, Optional ByVal lpVersionInformation As Long _
, Optional ByVal lpReserved As Long _
, Optional ByVal lParam As Long _
) As Long
#End If

Public Function GetMappedStringArray(ByVal Source$) As Byte()
   Dim r&, dest() As Byte

   r = LCMapStringEx(StrPtr(LOCALE_NAME_USER_DEFAULT), _
                     LCMAP_SORTKEY Or SORT_DIGITSASNUMBERS, _
                     StrPtr(Source), Len(Source), 0, 0)
   If r Then
      ReDim dest(r - 1)
      r = LCMapStringEx(StrPtr(LOCALE_NAME_USER_DEFAULT), _
                        LCMAP_SORTKEY Or SORT_DIGITSASNUMBERS, _
                        StrPtr(Source), Len(Source), _
                        VarPtr(dest(0)), _
                        UBound(dest) + 1)
      GetMappedStringArray = dest
   End If
End Function
Titel: Re: Sortierung
Beitrag von: PhilS am August 17, 2016, 19:40:15
Zitat von: Lachtaube am August 17, 2016, 18:20:12Was wiederum geht, ist, ein Binärfeld anzulegen, dessen Kapazität allerding maximal 510 Bytes umfasst.
Natürlich, per DDL, du hast recht!

Leider kann man die Funktion mit Rückgabewert Byte() nicht direkt in eine Abfrage einbauen. Also muss man erst über ein Recordset in die Tabelle den binären SortKey schreiben und kann dann danach sortieren. - Was mir nicht ganz so gut gefällt, weil es Update-Anomalien mit sich bringen könnte.

Auch wenn das Byte-Array sicherlich der eigentlich performantere und bessere Datentyp wäre, würde ich trotzdem meinen Lösungsvorschlag mit der Umwandlung des Byte-Arrays in einen String vorziehen. Das kann man direkt in einer Abfrage verwenden, ohne den SortKey speichern zu müssen.


Noch eine Anmerkung zu meinem Screenshot mit der inkorrekten Sortierung: Dies wurde offenbar durch den Flag LCMAP_BYTEREV verursacht, den ich eingebaut hatte, um seine Auswirkung zu testen. Außerdem hatte ich mit LCMAP_BYTEREV gelegentliche Access-Abstürze, wenn ich die Funktion in der Recordset-Schleife für die ganze Tabelle aufgerufen habe. Ohne den Flag ist das Ergebnis so wie ich es erwartet hätte.
Titel: Re: Sortierung
Beitrag von: daolix am August 17, 2016, 19:58:35
ZitatDies wurde offenbar durch den Flag LCMAP_BYTEREV verursacht, den ich eingebaut hatte
hmm, also bei mir nicht. Da hat deine Idee sowohl mit als auch ohne diesem Flag funktioniert.
Titel: Re: Sortierung
Beitrag von: Lachtaube am August 17, 2016, 20:59:18
Das Flag LCMAP_BYTEREV käme dann sinnvoll zum Einsatz, wenn der SrcStr im Big Endian Format vorläge.

Tja, auf Anomalien muss man natürlich immer achten. Im Zweifelsfall muss man halt noch einmal vorher eine Aktualisierungsabfrage laufen lassen. Der Vorteil des Binärfelds besteht darin, dass es indiziert werden kann.

Ich habe noch zwei weitere Versionen der Funktion erstellt. Eine, die auch in Abfragen auf das Binärfeld angewendet werden kann (z. Bsp. zum Aktualisieren) und eine, die zur Sortierung in Abfragen geeignet ist (Verzicht auf Index-Nutzung). Diese wird deutlich mehr Kapazität als die Version der aneinandergeketteten "000",-Bytefolgen aufweisen und auch schneller sein, weil hier die Zeichen-Verkettung entfällt. Hier noch einmal der komplette Code.Option Explicit

'int LCMapStringEx(
'  _In_opt_  LPCWSTR          lpLocaleName,
'  _In_      DWORD            dwMapFlags,
'  _In_      LPCWSTR          lpSrcStr,
'  _In_      int              cchSrc,
'  _Out_opt_ LPWSTR           lpDestStr,
'  _In_      int              cchDest,
'  _In_opt_  LPNLSVERSIONINFO lpVersionInformation,
'  _In_opt_  LPVOID           lpReserved,
'  _In_opt_  LPARAM           sortHandle
');

Private Const LOCALE_NAME_USER_DEFAULT$ = vbNullString
Private Const LCMAP_SORTKEY& = &H400            ' // WC sort key (normalize)
Private Const SORT_DIGITSASNUMBERS& = &H8       ' // use digits as numbers sort method

#If VBA7 Then
Private Declare PtrSafe Function LCMapStringEx Lib "kernel32" ( _
   ByVal lpLocaleName As LongPtr _
, ByVal dwMapFlags As Long _
, ByVal lpSrcStr As LongPtr _
, ByVal cchSrc As Long _
, ByVal lpDestStr As LongPtr _
, ByVal cchDest As Long _
, Optional ByVal lpVersionInformation As LongPtr _
, Optional ByVal lpReserved As LongPtr _
, Optional ByVal lParam As Long _
) As Long
#Else
Private Declare Function LCMapStringEx Lib "kernel32" ( _
   ByVal lpLocaleName As Long _
, ByVal dwMapFlags As Long _
, ByVal lpSrcStr As Long _
, ByVal cchSrc As Long _
, ByVal lpDestStr As Long _
, ByVal cchDest As Long _
, Optional ByVal lpVersionInformation As Long _
, Optional ByVal lpReserved As Long _
, Optional ByVal lParam As Long _
) As Long
#End If

'// Nur zur Verwendung in Recordsets geeignet
Public Function GetMappedStringAsByteArray(ByVal Source$) As Byte()
   Dim r&, dest() As Byte

   r = LCMapStringEx(StrPtr(LOCALE_NAME_USER_DEFAULT), _
                     LCMAP_SORTKEY Or SORT_DIGITSASNUMBERS, _
                     StrPtr(Source), Len(Source), 0, 0)
   If r Then
      ReDim dest(r - 1)
      r = LCMapStringEx(StrPtr(LOCALE_NAME_USER_DEFAULT), _
                        LCMAP_SORTKEY Or SORT_DIGITSASNUMBERS, _
                        StrPtr(Source), Len(Source), _
                        VarPtr(dest(0)), _
                        UBound(dest) + 1)
      '// mehr als 510 Bytes passen nicht in ein Binärfeld
      If r > 510 Then ReDim Preserve dest(509)
      GetMappedStringAsByteArray = dest
   End If
End Function

'// Zur Verwendung in Abfragen und Recordsets - nicht zur Sortierung in Abfragen geeignet
Public Function GetMappedStringArrayAsAnsiString$(ByVal Source$)
   Dim r&, dest() As Byte

   r = LCMapStringEx(StrPtr(LOCALE_NAME_USER_DEFAULT), _
                     LCMAP_SORTKEY Or SORT_DIGITSASNUMBERS, _
                     StrPtr(Source), Len(Source), 0, 0)
   If r Then
      ReDim dest(r - 1)
      r = LCMapStringEx(StrPtr(LOCALE_NAME_USER_DEFAULT), _
                        LCMAP_SORTKEY Or SORT_DIGITSASNUMBERS, _
                        StrPtr(Source), Len(Source), _
                        VarPtr(dest(0)), _
                        UBound(dest) + 1)
      '// mehr als 510 Bytes passen nicht in ein Binärfeld
      If r > 510 Then ReDim Preserve dest(509)
      GetMappedStringArrayAsAnsiString = dest
   End If
End Function

'// Zur Verwendung als Sortierschlüssel in Abfragen und Recordsets geeignet
'// ob Access dabei mehr als 255 Zeichen berücksichtigt, müsste man prüfen
'// - ich vermute eher nicht
Public Function GetMappedStringArrayAsUTF16$(ByVal Source$)
   Dim r&, dest() As Byte

   r = LCMapStringEx(StrPtr(LOCALE_NAME_USER_DEFAULT), _
                     LCMAP_SORTKEY Or SORT_DIGITSASNUMBERS, _
                     StrPtr(Source), Len(Source), 0, 0)
   If r Then
      ReDim dest(r - 1)
      r = LCMapStringEx(StrPtr(LOCALE_NAME_USER_DEFAULT), _
                        LCMAP_SORTKEY Or SORT_DIGITSASNUMBERS, _
                        StrPtr(Source), Len(Source), _
                        VarPtr(dest(0)), _
                        UBound(dest) + 1)
      GetMappedStringArrayAsAnsiString = StrConv(dest, vbUnicode)
   End If
End Function
PS: Wer nicht weiß, wie ein Binärfeld erstellt werden kann, kann temporär die Systemtabellen der Datenbank einblenden und sich z. Bsp. aus der MSysObjects-Tabelle das Owner-Feld in seine Tabelle kopieren und umbenennen.
Titel: Re: Sortierung
Beitrag von: crystal am August 17, 2016, 21:29:27
Hi Leute,

wirklich Klasse, was ihr hier für Antworten geliefert habt!!!

LCMapStringEx liefert ja scheinbar ein Byte-Array, bei dem je 2 Bytes einem Unicode-Zeichen entsprechen, oder? Wenn das so ist, könnte man diese je 2 Bytes nicht auch in ASCII-Äquivalente zurückwandeln, um "sortfeld" lesbar zu bekommen? Denn das Byte-Array ist natürlich nicht besonders "schön". (Übrigens könnte man auf das Komma verzichten, da man den zusammengesetzten String eh nicht lesen kann.)

Da es in meiner Anwendung "nur" um den Fall geht, in einem String "irgendwo" eingebettete Zahlen (und Umlaute) natürlich zu sortieren, tendiere ich inzwischen doch wieder zu einer eigenen Funktion, die an passenden Stellen führende Nullen (bzw. 2-Zeichen-Äquivalente) einfügt, so schön LCMapStringEx auch ist. Nach grober Sichtung meiner knapp 10.000 Datensätze habe ich gesehen, dass numerische Bestandteile maximal 3 Vorkommastellen haben, so dass ich mit einer Auffüllung auf 4 führende Nullen wohl einigermaßen auf der sicheren Seite wäre. Notfalls müsste ich eine solche Wandlungs-Funktion auf mehr Stellen ändern und dann einmalig alle Datensätze bearbeiten.

Aber vielleicht habt ihr ja noch bessere Ideen.

Danke jedenfalls für diese lehrreichen Beispiele zur Nutzung von LCMapStringEx.

crystal

PS: Laut MSDN-Doku ist die Nutzung des Flags LCMAP_SORTKEY nicht mit anderen Main-Flags kombinierbar, vielleicht kam es deshalb zu Abstürzen.










Titel: Re: Sortierung
Beitrag von: Lachtaube am August 17, 2016, 21:57:57
Zitat von: crystal am August 17, 2016, 21:29:27LCMapStringEx liefert ja scheinbar ein Byte-Array, bei dem je 2 Bytes einem Unicode-Zeichen entsprechen, oder? Wenn das so ist, könnte man diese je 2 Bytes nicht auch in ASCII-Äquivalente zurückwandeln, um "sortfeld" lesbar zu bekommen?
Die Funktion GetMappedStringArrayAsUTF16$ macht das ja - nur wird daraus dann noch kein lesbarer Text. Für bestimmte Unicode-Zeichen werden auch schon einmal 10 Bytes verbraten. Ich denke, dass außer einem eindeutigen Sortierschlüssel, den Bytefolgen nicht viek Sinn hinterlegt ist. Ansonsen würde ich auch eine Umkehr-API Funktion erwarten.

Ein nächster Schritt sollte ein Benchmarking umfassen, um die verschiedenen Versionen auf Praxistauglichkeit testen zu können.
Titel: Re: Sortierung
Beitrag von: PhilS am August 18, 2016, 01:12:30
Zitat von: Lachtaube am August 17, 2016, 20:59:18...und eine, die zur Sortierung in Abfragen geeignet ist (Verzicht auf Index-Nutzung). Diese wird deutlich mehr Kapazität als die Version der aneinandergeketteten "000",-Bytefolgen aufweisen und auch schneller sein, weil hier die Zeichen-Verkettung entfällt.
Du möchtest deine Funktion GetMappedStringArrayAsUTF16 für die Sortierung in Abfragen verwenden?

Du hast vermutlich überlesen, was ich dazu in #17 geschrieben hatte:
Zitat von: PhilS am August 17, 2016, 15:42:31Wenn man das Byte-Array jetzt als String behandelt, verwendet Access für die Sortierung die jeweils für die DB eingestellt Sortierreihenfolge. Dabei kann es aber IMO passieren, dass die Sortierung am Ende nicht der binären Sortierung der Bytes entspricht und damit nicht mehr korrekt ist.
Das "IMO" oben streiche ich. - Ich hab ein konkretes Beispiel.
Gibt mal in einer Tabelle mit Text-Spalte die folgenden Zeichen ein (je eines pro Datensatz): ! . ? a ü x z
Dann verwende diese Spalte als Input für GetMappedStringArrayAsUTF16  und sortiere über den Rückgabewert der Funktion.

Mir ist schon klar, dass meine numerischen Byte-Werte-Folgen von der Performance suboptimal sind, aber eine Alternative sehe ich noch nicht.
Titel: Re: Sortierung
Beitrag von: Lachtaube am August 18, 2016, 08:53:16
Ja, das stimmt.Als Alternative könnte man eine Hex-Zeichenkette erstellen.'// Zur Verwendung als Sortierschlüssel geeignet
'// Ausgabe als Hex-String
Public Function GetMappedStringArrayAsHex(ByVal Source)
   Dim i&, r&, dest() As Byte

   If Len(Nz(Source)) = 0 Then Exit Function

   '// erforderliche Länge ermitteln
   r = LCMapStringEx(StrPtr(LOCALE_NAME_USER_DEFAULT), _
                     LCMAP_SORTKEY Or SORT_DIGITSASNUMBERS, _
                     StrPtr(Source), Len(Source), 0, 0)
   If r > 0 Then
      '// Ziel-Array dimensionieren
      ReDim dest(r - 1)
      '// Ziel-Array füllen
      r = LCMapStringEx(StrPtr(LOCALE_NAME_USER_DEFAULT), _
                        LCMAP_SORTKEY Or SORT_DIGITSASNUMBERS, _
                        StrPtr(Source), Len(Source), _
                        VarPtr(dest(0)), r)
                       
      '// Ergebnispuffer mit Leerzeichen initialisieren
      GetMappedStringArrayAsHex = Space$(2 * (r - 1))

      '// 0-Byte am Ende ignorieren
      For i = 0 To UBound(dest) - 1
         Mid$(GetMappedStringArrayAsHex, 2 * i + 1, 2) _
               = Right$("00" & Hex$(dest(i)), 2)
      Next
   Else
      GetMappedStringArrayAsHex = CVErr(vbObjectError + &H2001)
   End If
End Function
Bei einem Benchmarking mit einer lokalen Tabelle über 50000 Datensätze und Textlängen zwischen 30 und 50 Zeichen braucht diese Funktion knappe 10 Sekunden zur Sortierung - hingegen erledigt ein indiziertes Binärfeld in ca. 0,1 Sekunden diese Aufgabe, und ein Update aller Sortierwerte benötigt ca. 4,8 Sekunden.
Titel: Re: Sortierung
Beitrag von: crystal am September 30, 2016, 21:16:22
Hallo nochmal, Lachtaube.
Ist ja schon eine Weile her, aber dein Code ist eben "zeitlos" toll. Es gibt nur einen kleinen Fehler, den ich hier korrigiert habe (siehe Kommentar "xxxxx" ganz am Ende).

Ich denke, dieser Thread sollte in ein neues Unterforum aufgenommen werden: "Excellente Lösungen".


Option Explicit

'int LCMapStringEx(
'  _In_opt_  LPCWSTR          lpLocaleName,
'  _In_      DWORD            dwMapFlags,
'  _In_      LPCWSTR          lpSrcStr,
'  _In_      int              cchSrc,
'  _Out_opt_ LPWSTR           lpDestStr,
'  _In_      int              cchDest,
'  _In_opt_  LPNLSVERSIONINFO lpVersionInformation,
'  _In_opt_  LPVOID           lpReserved,
'  _In_opt_  LPARAM           sortHandle
');

Private Const LOCALE_NAME_USER_DEFAULT$ = vbNullString
Private Const LCMAP_SORTKEY& = &H400            ' // WC sort key (normalize)
Private Const SORT_DIGITSASNUMBERS& = &H8       ' // use digits as numbers sort method

#If VBA7 Then
Private Declare PtrSafe Function LCMapStringEx Lib "kernel32" ( _
   ByVal lpLocaleName As LongPtr _
, ByVal dwMapFlags As Long _
, ByVal lpSrcStr As LongPtr _
, ByVal cchSrc As Long _
, ByVal lpDestStr As LongPtr _
, ByVal cchDest As Long _
, Optional ByVal lpVersionInformation As LongPtr _
, Optional ByVal lpReserved As LongPtr _
, Optional ByVal lParam As Long _
) As Long
#Else
Private Declare Function LCMapStringEx Lib "kernel32" ( _
   ByVal lpLocaleName As Long _
, ByVal dwMapFlags As Long _
, ByVal lpSrcStr As Long _
, ByVal cchSrc As Long _
, ByVal lpDestStr As Long _
, ByVal cchDest As Long _
, Optional ByVal lpVersionInformation As Long _
, Optional ByVal lpReserved As Long _
, Optional ByVal lParam As Long _
) As Long
#End If

'// Nur zur Verwendung in Recordsets geeignet
Public Function GetMappedStringAsByteArray(ByVal Source$) As Byte()
   Dim r&, dest() As Byte

   r = LCMapStringEx(StrPtr(LOCALE_NAME_USER_DEFAULT), _
                     LCMAP_SORTKEY Or SORT_DIGITSASNUMBERS, _
                     StrPtr(Source), Len(Source), 0, 0)
   If r Then
      ReDim dest(r - 1)
      r = LCMapStringEx(StrPtr(LOCALE_NAME_USER_DEFAULT), _
                        LCMAP_SORTKEY Or SORT_DIGITSASNUMBERS, _
                        StrPtr(Source), Len(Source), _
                        VarPtr(dest(0)), _
                        UBound(dest) + 1)
      '// mehr als 510 Bytes passen nicht in ein Binärfeld
      If r > 510 Then ReDim Preserve dest(509)
      GetMappedStringAsByteArray = dest
   End If
End Function

'// Zur Verwendung in Abfragen und Recordsets - nicht zur Sortierung in Abfragen geeignet
Public Function GetMappedStringArrayAsAnsiString$(ByVal Source$)
   Dim r&, dest() As Byte

   r = LCMapStringEx(StrPtr(LOCALE_NAME_USER_DEFAULT), _
                     LCMAP_SORTKEY Or SORT_DIGITSASNUMBERS, _
                     StrPtr(Source), Len(Source), 0, 0)
   If r Then
      ReDim dest(r - 1)
      r = LCMapStringEx(StrPtr(LOCALE_NAME_USER_DEFAULT), _
                        LCMAP_SORTKEY Or SORT_DIGITSASNUMBERS, _
                        StrPtr(Source), Len(Source), _
                        VarPtr(dest(0)), _
                        UBound(dest) + 1)
      '// mehr als 510 Bytes passen nicht in ein Binärfeld
      If r > 510 Then ReDim Preserve dest(509)
      GetMappedStringArrayAsAnsiString = dest
   End If
End Function

'// Zur Verwendung als Sortierschlüssel in Abfragen und Recordsets geeignet
'// ob Access dabei mehr als 255 Zeichen berücksichtigt, müsste man prüfen
'// - ich vermute eher nicht
Public Function GetMappedStringArrayAsUTF16$(ByVal Source$)
   Dim r&, dest() As Byte

   r = LCMapStringEx(StrPtr(LOCALE_NAME_USER_DEFAULT), _
                     LCMAP_SORTKEY Or SORT_DIGITSASNUMBERS, _
                     StrPtr(Source), Len(Source), 0, 0)
   If r Then
      ReDim dest(r - 1)
      r = LCMapStringEx(StrPtr(LOCALE_NAME_USER_DEFAULT), _
                        LCMAP_SORTKEY Or SORT_DIGITSASNUMBERS, _
                        StrPtr(Source), Len(Source), _
                        VarPtr(dest(0)), _
                        UBound(dest) + 1)
'xxxxx
'      GetMappedStringArrayAsAnsiString = StrConv(dest, vbUnicode)
'xxxxx
      GetMappedStringArrayAsUTF16 = StrConv(dest, vbUnicode)
   End If
End Function


Ich habe den Code in einem größeren Access-Projekt eingesetzt und das Ergebnis überzeugt total!!!

Vielen Dank also für die grandiose Lösung! :)

;D
Titel: Re: Sortierung
Beitrag von: PhilS am September 30, 2016, 21:31:10
Ich habe die hier erarbeitete Lösung inzwischen auch im Artikel Natürliche Zahlensortierung in Access mit VBA (http://codekabinett.com/rdumps.php?Lang=1&targetDoc=logische-zahlensortierung-access-vba) auf meiner Webseite zusammengefasst.
Titel: Re: Sortierung
Beitrag von: Hondo am Oktober 01, 2016, 13:31:13
Hallo,
clever gelöst! Ihr habt auch an alles gedacht. Auf das Gebietsschema wäre ich nicht gekommen.

Gruß Andreas
Titel: Re: Sortierung
Beitrag von: crystal am Oktober 01, 2016, 13:41:59
Hallo daolix, MzKlMa, Lachtaube und PhilS,

Natürlich gebührt der "Ruhm für diese tolle Lösung" euch allen.

Speziellen Dank an PhilS für den Artikel auf deiner Website: er ist auch didaktisch hervorragend!

Solche Threads und Lösungen sind wirklich fantastisch für ein aktives Access- und VBA-Forum. Ich bin mir sicher, dass die Lösung bald weiter bekannt wird und weltweite Beachtung findet.

Bravo!

Titel: Re: Natürliche Sortierung
Beitrag von: crystal am Oktober 01, 2016, 13:53:15
Hallo,
soeben habe ich den Thread-Titel auf "Natürliche Sortierung" geändert, damit die tolle Lösung vielleicht noch einfacher gefunden werden kann.

Bitte, liebe Mods, erstellt ein neues Unterfoum "Excellente Lösungen" oder ähnlich, um solche Threads zusammenfassen zu können.

Titel: Re: Natürliche Sortierung
Beitrag von: Hondo am Oktober 01, 2016, 13:57:08
Hallo,
danke für den Vorschlag, wir werden darüber nachdenken.

Andreas
Titel: Re: Natürliche Sortierung
Beitrag von: crystal am Oktober 01, 2016, 20:24:47
Noch eine kleine Verbesserung der Performance (Speichern des Sortwerts in der Tabelle selbst).
Die Idee ist lediglich, alle Hex-Strings der Zahlen 0 bis 255 >>>einmal<<<  zu erzeugen und dann nur noch zu referenzieren.

Die alte Version braucht für 10.000 Datensätze ca. 6 Sekunden, die neue unter 1 Sekunde - ohne Garantie. (Änderungen siehe Kommentar "'crystal".)


Option Compare Database
Option Explicit

Private Const LOCALE_NAME_USER_DEFAULT$ = vbNullString
Private Const LCMAP_SORTKEY& = &H400            ' // WC sort key (normalize)
Private Const SORT_DIGITSASNUMBERS& = &H8       ' // use digits as numbers sort method

#If VBA7 Then
Private Declare PtrSafe Function LCMapStringEx Lib "kernel32" ( _
   ByVal lpLocaleName As LongPtr _
, ByVal dwMapFlags As Long _
, ByVal lpSrcStr As LongPtr _
, ByVal cchSrc As Long _
, ByVal lpDestStr As LongPtr _
, ByVal cchDest As Long _
, Optional ByVal lpVersionInformation As LongPtr _
, Optional ByVal lpReserved As LongPtr _
, Optional ByVal lParam As Long _
) As Long
#Else
Private Declare Function LCMapStringEx Lib "kernel32" ( _
   ByVal lpLocaleName As Long _
, ByVal dwMapFlags As Long _
, ByVal lpSrcStr As Long _
, ByVal cchSrc As Long _
, ByVal lpDestStr As Long _
, ByVal cchDest As Long _
, Optional ByVal lpVersionInformation As Long _
, Optional ByVal lpReserved As Long _
, Optional ByVal lParam As Long _
) As Long
#End If

Public strH As String               'crystal
Public sHex                         'crystal
Public blnInitialized As Boolean    'crystal
'

'// Zur Verwendung als Sortierschlüssel geeignet
'// Ausgabe als Hex-String
Public Function GetMappedStringArrayAsHex(ByVal Source)
   Dim i&, r&, dest() As Byte

   If Len(Nz(Source)) = 0 Then Exit Function

   '// erforderliche Länge ermitteln
   r = LCMapStringEx(StrPtr(LOCALE_NAME_USER_DEFAULT), _
                     LCMAP_SORTKEY Or SORT_DIGITSASNUMBERS, _
                     StrPtr(Source), Len(Source), 0, 0)
   If r > 0 Then
      '// Ziel-Array dimensionieren
      ReDim dest(r - 1)
      '// Ziel-Array füllen
      r = LCMapStringEx(StrPtr(LOCALE_NAME_USER_DEFAULT), _
                        LCMAP_SORTKEY Or SORT_DIGITSASNUMBERS, _
                        StrPtr(Source), Len(Source), _
                        VarPtr(dest(0)), r)
                       
      '// Ergebnispuffer mit Leerzeichen initialisieren
      GetMappedStringArrayAsHex = Space$(2 * (r - 1))

      '// 0-Byte am Ende ignorieren
      For i = 0 To UBound(dest) - 1
         Mid$(GetMappedStringArrayAsHex, 2 * i + 1, 2) _
               = GetHex(dest(i))                                'crystal
'               = Right$("00" & Hex$(dest(i)), 2)               'crystal
      Next
   Else
      GetMappedStringArrayAsHex = CVErr(vbObjectError + &H2001)
   End If
End Function

Public Function GetHex(ByVal intValue) As String                'crystal

    If Not blnInitialized Then
        strH = "00,01,02,03,04,05,06,07,08,09,0A,0B,0C,0D,0E,0F," & _
               "10,11,12,13,14,15,16,17,18,19,1A,1B,1C,1D,1E,1F," & _
               "20,21,22,23,24,25,26,27,28,29,2A,2B,2C,2D,2E,2F," & _
               "30,31,32,33,34,35,36,37,38,39,3A,3B,3C,3D,3E,3F," & _
               "40,41,42,43,44,45,46,47,48,49,4A,4B,4C,4D,4E,4F," & _
               "50,51,52,53,54,55,56,57,58,59,5A,5B,5C,5D,5E,5F," & _
               "60,61,62,63,64,65,66,67,68,69,6A,6B,6C,6D,6E,6F," & _
               "70,71,72,73,74,75,76,77,78,79,7A,7B,7C,7D,7E,7F," & _
               "80,81,82,83,84,85,86,87,88,89,8A,8B,8C,8D,8E,8F," & _
               "90,91,92,93,94,95,96,97,98,99,9A,9B,9C,9D,9E,9F," & _
               "A0,A1,A2,A3,A4,A5,A6,A7,A8,A9,AA,AB,AC,AD,AE,AF," & _
               "B0,B1,B2,B3,B4,B5,B6,B7,B8,B9,BA,BB,BC,BD,BE,BF," & _
               "C0,C1,C2,C3,C4,C5,C6,C7,C8,C9,CA,CB,CC,CD,CE,CF," & _
               "D0,D1,D2,D3,D4,D5,D6,D7,D8,D9,DA,DB,DC,DD,DE,DF," & _
               "E0,E1,E2,E3,E4,E5,E6,E7,E8,E9,EA,EB,EC,ED,EE,EF," & _
               "F0,F1,F2,F3,F4,F5,F6,F7,F8,F9,FA,FB,FC,FD,FE,FF,"
               
        sHex = Split(strH, ",")
        blnInitialized = True
    End If
   
    GetHex = sHex(intValue)
   
End Function



Das ganze mit dieser Abfrage:



UPDATE Produkte SET Produkte.SortName = GetMappedStringArrayAsHex(Produkte.Name);


Vielleicht könnte man die Performance noch weiter verbessern, wenn man das Ziel-Array "dest" nicht bei jedem Aufruf neu bildet, sondern nur einmal public definiert und z. B. auf eine Länge von 1024 Zeichen initialisiert. Dann könnte man den zweifachen Aufruf von LCMapStringEx auf einen reduzieren und müßte nur noch auf r<1024 prüfen.


Nur Ideen....sicher verbesserungsfähig....

Gruß,

crystal
Titel: Re: Natürliche Sortierung
Beitrag von: daolix am Oktober 02, 2016, 17:12:49
Hallo
ZitatDann könnte man den zweifachen Aufruf von LCMapStringEx auf einen reduzieren
Könnte man machen, aber ich glaube nicht das das zu einer signifikanten Verbesserung führt. Der Performancefresser ist hier die Hexbildung.
Und hier könnte man ggf statt mit der Mid-Funktion [Mid$(GetMappedStringArrayAsHex ...] mit einem Pointer und CopyMemory arbeiten. Aber sehr viel mehr wird man mit VB(A) nicht mehr rauskizueln können.
Titel: Re: Natürliche Sortierung
Beitrag von: PhilS am Oktober 04, 2016, 09:09:59
Zitat von: crystal am Oktober 01, 2016, 20:24:47
Noch eine kleine Verbesserung der Performance (Speichern des Sortwerts in der Tabelle selbst).
Die Idee ist lediglich, alle Hex-Strings der Zahlen 0 bis 255 >>>einmal<<<  zu erzeugen und dann nur noch zu referenzieren.
Wenn du den SortKey in einer Tabelle speichern willst, solltest du direkt das Byte-Array als Binär-Datentyp (http://codekabinett.com/rdumps.php?Lang=1&targetDoc=binaerdaten-access-datenbank-sortieren-indizieren) speichern. Dieser hat im Vergleich eine wesentlich größere Speicherdichte, da es nur jeweils ein Byte für jedes Byte im Byte-Array benötigt. Der Hex-String hat seine Daseinsberechtigung nur als Rückgabewert einer Funktion, die ad-hoc in einer Abfrage aufgerufen wird, weil hier kein anderer, adäquater Datentyp möglich ist.

Wenn du das Byte-Array in einen Hex-String konvertierst und dann erst speicherst, verlierst du doppelt Speicherplatz. Erstmal, weil bei der Umwandlung in den Hex-String ein Byte zu zwei Zeichen wird und dann nochmal weil beim Speichern pro Zeichen zwei Bytes (Unicode) in der Tabelle belegt werden.
Weiterhin ist der Binär-Datentyp in der Tabelle sicherlich auch deshalb noch schneller, weil er auch Binär sortiert werden kann und keine spezielle Sortierreihenfolge dafür angewendet werden muss.


Die Idee einfach ein vordefiniertes Byte-Array der maximal verwertbaren Länge zu verwenden und ohne den ersten Aufruf von LCStringMapEx an die Funktion zu übergeben halte ich für eine Idee, die durchaus die Performance noch etwas verbessern kann.

Abhängig von den oben genannten Limits für die weitere Verwendung des ByteArray kann man einfach die maximale Länge an Bytes übergeben und muss den Rest ohnehin ignorieren. (Ich habe jetzt allerdings nicht geprüft, ob LCStringMapEx die ersten Bytes überhaupt in den Buffer schreibt, wenn vorher klar ist, dass die Gesamtlänge nicht ausreicht.

Evtl. macht es dann mehr Sinn, einfach einen noch größeren Buffer an die Funktion zu übergeben und dann den (zu großen) Rest des Arrays einfach zu verwerfen, weil man ihn ja eh nicht für die Sortierung benutzen kann. – Dies verursacht dann natürlich potenzielle Ungenauigkeiten bei der Sortierung von langen Texten.
Titel: Re: Natürliche Sortierung
Beitrag von: crystal am Oktober 04, 2016, 13:07:37
Hallo PhilS,
danke für deine Hinweise und die ausführlichen Begründungen!
Wieder ein schönes Beispiel für exakte, verständliche und vollständige Hilfe ohne erhobenen Zeigefinger (und ohne Ironie oder Geringschätzung des Fragenden).
So müssen Beiträge in einem Forum geschrieben werden: sachlich, fachlich fundiert und konstruktiv.

Bravo! 5 von 5 Sternen für deinen Beitrag und dessen didaktische Qualität!

Gruß, crystal

Titel: Re: Natürliche Sortierung
Beitrag von: crystal am Oktober 06, 2016, 17:37:01
Hallo nochmal PhilS,

ich habe jetzt versucht, mit meiner Aktualisierungs-Abfrage ein biäres Feld zu erzeugen.

UPDATE Produkte SET Produkte.SortName = GetMappedStringArrayAsByteArray(Produkte.Name);


Das funktioniert aber leider nicht.
Erst wenn ich im Code


Public Function GetMappedStringAsByteArray(ByVal Source$) As Byte()


auf


Public Function GetMappedStringAsByteArray(ByVal Source$)


ändere, klappt es, sonst kommt nur der Fehler

Unbekannte Funktion "GetMappedStringAsByteArray".

Komisch, aber wahr...
Titel: Re: Natürliche Sortierung
Beitrag von: PhilS am Januar 10, 2021, 11:12:27
Zitat von: crystal am Oktober 06, 2016, 17:37:01ich habe jetzt versucht, mit meiner Aktualisierungs-Abfrage ein biäres Feld zu erzeugen.
UPDATE Produkte SET Produkte.SortName = GetMappedStringArrayAsByteArray(Produkte.Name);
Das funktioniert aber leider nicht.
Erst wenn ich im Code

Public Function GetMappedStringAsByteArray(ByVal Source$) As Byte()
auf

Public Function GetMappedStringAsByteArray(ByVal Source$)
ändere, klappt es, sonst kommt nur der Fehler

Unbekannte Funktion "GetMappedStringAsByteArray".

Komisch, aber wahr...
Diesen Post hatte ich mir vor Jahren mal auf Wiedervorlage gelegt, und zufällig jetzt gerade wieder im Blick.

Mit dem Abstand ist das Problem hier relativ klar.
Funktionen können nur direkt in Abfragen verwendet werden, wenn sie primitive Datentypen (z.B. String, Zahl, Datum) zurückgeben, aber nicht mit komplexen Typen, wie einem Array.
Ein Byte-Array ist aber ja fast ein "binary". - Daher vermute ich, wenn man den Rückgabetyp der Funktion nicht explizit definiert, wird das Byte-Array implizit in einen Binary-Typ in der DB konvertiert.