Sortowanie nazw sekcji INI wg danych

1

Hej,
Mam taki dziwny problem...
Otóż mam plik (w formacie INI), który zawiera date (zapisaną jako liczba - Integer).
Chciałbym uporządkować te dane wg daty (rosnąco albo malejąco).

[EE61B863306341DEB3FACB7C5085226C]
Date=47174
[CCCCB86A306341DEB3F1CB7C5085226C]
Date=43174
[FF61B86A306341DEB3F1CB7C5085226C]
Date=43074
[AAAAB86A306341DEB3F1CB7C5085226C]
Date=45174
[BBBBB86A306341DEB3F1CB7C5085226C]
Date=45174

Czyli otrzymać posortowaną listę nazw sekcji (wg daty):
Dlaczego tak? Ponieważ potem będę mógł czytać odpowiednie dane wg danej sekcji.
Jak się do tego zabrać, żeby zrobić to szybko i prosto?

Czy poniższe ma sens i jest prawidłowe?

function MySortFunc(List: TStringList; Index1, Index2: Integer): Integer;
var
  s1, s2: string;
begin
  // EDIT -> to chyba eliminuje GUIDy, bo czytam tylko Daty
  s1 := List.Names[Index1];
  s2 := List.Names[Index2];
  
  //s1 := List[Index1]; -> tu byl caly wpis Data=GUID 
  //s2 := List[Index2];
  Result := CompareText(s1, s2);
end;

   SL_SORT_LIST := TStringList.Create;
   try
      // Przykladowa lista z danymi (Data = Nazwa Sekcji)

      SL_SORT_LIST.Delimiter := '=';  // EDIT
      SL_SORT_LIST.Add('47174' + '=' + 'EE61B863306341DEB3FACB7C5085226C');
      SL_SORT_LIST.Add('43174' + '=' + 'CCCCB86A306341DEB3F1CB7C5085226C');
      SL_SORT_LIST.Add('43074' + '=' + 'FF61B86A306341DEB3F1CB7C5085226C');
      SL_SORT_LIST.Add('45174' + '=' + 'AAAAB86A306341DEB3F1CB7C5085226C');
      SL_SORT_LIST.Add('45174' + '=' + 'BBBBB86A306341DEB3F1CB7C5085226C');


      SL_SORT_LIST.CustomSort(MySortFunc);
      ShowMessage(SL_SORT_LIST.Text); // wynik

   finally
      SL_SORT_LIST.Free;
   end;

Teoretycznie sortowane są wpisy - ale, czy Nazwy sekcji (te GUIDy) nie mają wpływu na wynik? Jeśli tak, to jak je zignorować (czy zmiana EDIT poprawia sytuacje?)?
Wynik:

43074=FF61B86A306341DEB3F1CB7C5085226C
43174=CCCCB86A306341DEB3F1CB7C5085226C
45174=AAAAB86A306341DEB3F1CB7C5085226C
45174=BBBBB86A306341DEB3F1CB7C5085226C
47174=EE61B863306341DEB3FACB7C5085226C

Proszę o podpowiedzi,
Słaby jestem z sortowania, wręcz beznadziejny... więc proszę o wyrozumiałość...

-Pawel

2

TStringList posiada domyślną metodę sortującą, nie musisz pisać własnej jeżeli chcesz wykonać zwykłe sortowanie.
właściwość Delimiter w tym przypadku nic nie zmienia, ponieważ jest wykorzystywana tylko gdy korzystasz z DelimitedText, np. utworzenie listy z tekstu rozdzielonego średnikiem:

stringlist.Delimiter := ';';
stringlist.DeleimitedText := 'aaaa;1111;ddffd;53dd;asddas;r3wrwersd;dsfsdfre;werte;dfdsfds;itd.';

Listę sekcji można pobrać w ten sposób:

var
  sekcjesort: TStringList;

procedure TForm1.Button1Click(Sender: TObject);
var
  ini: TINIFile;
  i, d: Integer;
  listasekcji: TStringList;
begin
  if FileExists('D:\test_in.ini') then
  begin
    ini := TINIFile.Create('D:\test_in.ini');
    listasekcji := TStringList.Create;
    if not Assigned(sekcjesort) then
      sekcjesort := TStringList.Create
    else
      sekcjesort.Clear;
    try
      ini.ReadSections(listasekcji); //pobierz listę sekcji
      for i := 0 to listasekcji.Count - 1 do
      begin
        d := ini.ReadInteger(listasekcji[i], 'Date', -1);
        if d >= 0 then                         //jeżeli sekcja zawiera partametr 'Date' z prawidłową wartością
          sekcjesort.Add(FormatFloat('00000', d) + '=' + listasekcji[i]);  //to zapisz parę data=sekcja na osobnej liście
      end;
      sekcjesort.Sort;    //posortuj listę prawidłowych sekcji
    finally
      ini.Free;
      listasekcji.Free;
    end;
  end;
end;   

następnie możesz sprawdzić czy dana data istnieje:

var
  idx: Integer;
begin
  idx := sekcjesort.IndexOfName(FormatFloat('00000', data_int));
  if idx >= 0 then
    ShowMessage(Format('Data: % istnieje, nazwa przypisaneg do niej sekcji to: %s',
      [data_int, sekcjesort.ValueFromIndex[idx]])) //lub: sekcjesort.Values[FormatFloat('00000', data_int)]
  else
    ShowMessage(Format('Sekcja zawierająca datę: %d nie istnieje', [data_int]));
end; 
0

@Paweł Dmitruk:
Dziękuję za podpowiedź.

Porobiłem parę testów... Twoja metoda jest całkiem sprytna (wykorzystanie IndexOfName listy), eliminuje potrzebę wykorzystania mojej "zewnętrznej funkcji sortującej".
Jeszcze ją sprawdzę, jak wypada pod względem szybkości... Samo pobranie danych z INI (czy to wartości, nazw sekscji, etc to nie problem... jestem miłośnikiem INI :P - jak widzę xml czy tę pokrakę json, to mi się niedobrze robi (fakt, są bardziej elastyczne)).

Wracając do mojej funkcji sortującej... rzeczywiście, nie muszę korzystać z właściwości Delimiter (SL_SORT_LIST.Delimiter := '=';).
Testując tę funkcję stwierdzam, że jednak nie działa jak powinna. Sortuje, ale z pewnością nie liczby...

Ale, wiedząc że sortuje liczby typu Integer (data) napisałem coś takiego:

function MyIntegerCompare(AList: TStringList; AVal1, AVal2: Integer): Integer;
var
  i1, i2: Integer;
begin
  i1 := StrToInt(AList.Names[AVal1]);
  i2 := StrToInt(AList.Names[AVal2]);

  Result := CompareValue(i1, i2);
end;

Porównując liczby wynik jest prawidłowy (tak samo ze zmiennoprzecinkowymi)...
Zatem, teoretycznie mam 2 rozwiązania. Które jest lepsze? Bardziej wydajne?
Które byście zastosowali?

Ps: Mając gotową listę sekcji mogę teraz zapisać dane do pliku INI w odpowiedniej kolejności...

2

a co jak ktoś z zewnątrz zmieni Ci kolejność/doda coś do INI? J Osobiście olałbym zapis i skupił się na odczycie. Olej wbudowaną obsługę INI i wczytuj go samemu, linia po linii do odpowiedniej struktury, która da Ci dostęp do danych, jakiego potrzebujesz.

0

To nie jest problem - bo to program będzie tylko dla kilku osób - tylko i ja decyduje co ma być i jak. Strukturę danych ja ustalam i nikt (a niech tylko spróbuje :P) nie ma prawa nic zmienić. Głównie mi chodzi, żeby dowiedzieć się jak się to robi "dobrze" i szybko. Bo może się okazać (zazwyczaj tak bywa), że wyważam otwarte już drzwi (w starożytności :P).

1
Pepe napisał(a):

Jak się do tego zabrać, żeby zrobić to szybko i prosto?

Użyj plików binarnych i zapisuj/odczytuj dane np. za pomocą TFileStream. ;)

Teoretycznie sortowane są wpisy - ale, czy Nazwy sekcji (te GUIDy) nie mają wpływu na wynik? Jeśli tak, to jak je zignorować (czy zmiana EDIT poprawia sytuacje?)?

GUID-y są częściami ciągów przetwarzanych są w trakcie sortowania — tak więc wpływają na wynik sortowania. W podanym przykładzie, wszystkie liczby w kluczach Date mają po pięć znaków, a co będzie, jeśli te liczby będą miały różną liczbę znaków? Sprawdzałeś jaki będzie rezultat sortowania?

Wynik:

43074=FF61B86A306341DEB3F1CB7C5085226C
43174=CCCCB86A306341DEB3F1CB7C5085226C
45174=AAAAB86A306341DEB3F1CB7C5085226C
45174=BBBBB86A306341DEB3F1CB7C5085226C
47174=EE61B863306341DEB3FACB7C5085226C

Czyli wartości z Date mają określać kolejność oraz być przy okazji nazwami kluczy, których wartościami mają być nazwy sekcji, w których pierwotnie znajdowały się daty. Skoro tak, to:

  • pobierz listę nazw sekcji — początkowo są nimi GUID-y,
  • dla każdej sekcji pobierz wartość klucza Date i wrzuć ją np. do listy,
  • posortuj listę kluczy — możesz użyć operatorów do stringów, np. < (sortowanie alfabetyczne),
  • zapisz dane w nowej sekcji, biorąc po kolei wartość daty (liczbę) oraz GUID.

Co istotne, sortowanie listy dat musi jednocześnie sortować GUID-y, tak aby można było zapisać wszystkie dane, bez konieczności szukania GUID-ów. Oznacza to, że jeden element powinien się składać z dwóch danych — liczbowa data oraz GUID. Dlatego też do tego celu powinieneś raczej wykorzystać strukturę słownika, w którym data jest kluczem, a GUID wartością, a nie listy.

We Free Pascalu wygląda to tak:

uses
  SysUtils, Classes, FGL, IniFiles;

type
  TIniValues = specialize TFPGMap<Integer, String>;

  // Funkcja porównująca klucze, czyli w tym przypadku daty w formie intów.
  function OnIniValuesCompare(const AKey1, AKey2: Integer): Integer;
  begin
    // Sortuje rosnąco.
    Result := AKey1 - AKey2;
  end;

var
  IniFile:     TIniFile;
  IniSections: TStringList;
  IniValues:   TIniValues;
  GUID:        String;
  Index:       Integer;
begin
  IniValues := TIniValues.Create();
  try
    IniFile     := TIniFile.Create('data.ini');
    IniSections := TStringList.Create();
    try
      // Pobierz listę sekcji z pliku (GUID-y).
      IniFile.ReadSections(IniSections);

      // Dodaj do słownika pary data-GUID (pasuje dodać walidację).
      for GUID in IniSections do
        IniValues.Add(IniFile.ReadInteger(GUID, 'Date', 0), GUID);
    finally
      IniFile.Free();
      IniSections.Free();
    end;

    // Sortowanko na podstawie dat.
    IniValues.OnCompare := @OnIniValuesCompare;
    IniValues.Sort();

    // Wyświetl wynik na ekranie.
    for Index := 0 to IniValues.Count - 1 do
      WriteLn(IniValues.Keys[Index], '=', IniValues.Data[Index]);
  finally
    IniValues.Free();
  end;
end.

Wynik:

43074=FF61B86A306341DEB3F1CB7C5085226C
43174=CCCCB86A306341DEB3F1CB7C5085226C
45174=AAAAB86A306341DEB3F1CB7C5085226C
45174=BBBBB86A306341DEB3F1CB7C5085226C
47174=EE61B863306341DEB3FACB7C5085226C

Nie próbuj zapisać tych danych do pliku Ini, jeśli daty mogą być takie same (a tutaj są), bo duplikaty nadpiszą poprzednio zapisane dane. Tutaj dwa klucze będą miały nazwę 45174, więc jeden GUID zostanie stracony.

0

@furious programming:
Dziękuję ci bardzo za ten przykład. Ciekawa implementacja. Nie wiem czy się skompiluje w Delphi (FGL) - sam też nie używam na codzień tych generycznych cudów (wiem - może najwyższy czas się tym zainteresować... ale to już niedługo, bo mam zamiar napisać w końcu program do obstawiania wyników na mistrzostwa świata w piłce nożnej :P).

Jedna uwaga. Nie ma strachu, że jakieś dane zostaną utracone/nadpisane.
Robię taki myk, że kopiuje oryginalny plik INI i z niego czytam dane, a te nowe wpisuje do oryginalnego (wcześniej czyszcząc w pętli sekcje).
Na koniec usuwam tymczasowy plik z danymi (czyli tymi sprzed sortowania). Nie wiem czy to najlepsze rozwiązanie, ale nie umiem jednocześnie czytać i pisać do pliku, bez ryzyka utraty danych.

0
Pepe napisał(a):

Nie wiem czy się skompiluje w Delphi (FGL) - sam też nie używam na codzień tych generycznych cudów […]

FGL to moduł stricte free-pascalowy — w Delphi jest klasa TDictionary, która pełni tę samą rolę co TFPGMap, więc możesz z niej skorzystać. W razie gdybyś nie chciał używać kontenerów generycznych, zawsze możesz skorzystać ze zwykłego TObjectList i do takiej listy wrzucać obiekty reprezentujące te pary danych:

type
  TIniValue = class
    GUID: String;
    Date: Integer;
  end;

Potem możesz sobie te obiekty posortować za pomocą metody Sort i uzyskać taki sam efekt.

Nie wiem czy to najlepsze rozwiązanie, ale nie umiem jednocześnie czytać i pisać do pliku, bez ryzyka utraty danych.

Jeśli chodzi o obsługę plików Ini, to możesz bezpiecznie naprzemian wczytywać i zapisywać dane do takiego pliku.

0
Pepe napisał(a):

Jedna uwaga. Nie ma strachu, że jakieś dane zostaną utracone/nadpisane.
Robię taki myk, że kopiuje oryginalny plik INI i z niego czytam dane, a te nowe wpisuje do oryginalnego (wcześniej czyszcząc w pętli sekcje).
Na koniec usuwam tymczasowy plik z danymi (czyli tymi sprzed sortowania). Nie wiem czy to najlepsze rozwiązanie, ale nie umiem jednocześnie czytać i pisać do pliku, bez ryzyka utraty danych.

furious programming napisał(a):

Jeśli chodzi o obsługę plików Ini, to możesz bezpiecznie naprzemian wczytywać i zapisywać dane do takiego pliku.

Pokombinowałem i okazuje się, że nie muszę korzystać z 2 plików.
Tworzę 2 instancje klasy TMemIni dla pliku z danymi - przy czym jedna jest tylko źródłem danych (przed sortowaniem), a druga docelowym (po sortowaniu). Jest to możliwe, bo dane są buforowane w pamięci i nie trzeba odwołań do pliku na dysku.

0
Pepe napisał(a):

Jedna uwaga. Nie ma strachu, że jakieś dane zostaną utracone/nadpisane.
Robię taki myk, że kopiuje oryginalny plik INI i z niego czytam dane, a te nowe wpisuje do oryginalnego (wcześniej czyszcząc w pętli sekcje).
Na koniec usuwam tymczasowy plik z danymi (czyli tymi sprzed sortowania). Nie wiem czy to najlepsze rozwiązanie, ale nie umiem jednocześnie czytać i pisać do pliku, bez ryzyka utraty danych.

Czyli chyba potrzebujesz bazy (i można się kłócić czy nawet relacyjnej - choc jakiś fajny key value NoSQL też wchodzi)
Dla mnie sztuczne kombinowanie widelca do zupy wynikające z niewłaściwego wyboru

0

Użyj plików binarnych i zapisuj/odczytuj dane np. za pomocą TFileStream

Myślę że trochę "przekombinowałeś" z plikami binarnymi. Klasa Tstringlist posiada całą wymaganą w tym wątku funkcjonalność do obsługi tekstowych plików .ini . W tym zapis, odczyt oraz obsługę kodowania (np. UTF8) .

0
grzegorz_so napisał(a):

Myślę że trochę "przekombinowałeś" z plikami binarnymi.

To było w formie żartu — w końcu wątek dotyczy plików Ini.

Ale mimo wszystko moje słowa pozostają prawdziwe. Wszelkie tekstowe pliki konfiguracyjne są zdecydowanie wolniejsze w obsłudze, dlatego że wymagają parsowania ich zawartości oraz konwersji danych z formy natywnej to postaci ciągu znaków i vice versa. Pliki amorficzne są zdecydowanie łatwiejsze do obróbki i nie wymagają żadnych konwersji, dlatego też ich przetwarzanie charakteryzuje się znacznie wyższą wydajnością. Jeśli dane nie powinny być czytelne dla człowieka i nie muszą być modyfikowane ręcznie (np. w edytorze tekstowym), użycie tekstowych konfigów jest marnotrawstwem czasu CPU.

Klasa Tstringlist posiada całą wymaganą w tym wątku funkcjonalność do obsługi plików .ini.

Klasa TStringList nie służy do obsługi plików Ini i nie posiada całej wymaganej funkcjonalności do ich obróbki — nie ma np. metod do obsługi sekcji. Po to jest TIniFile i TMemIniFile, aby mieć wszystko i nie musieć niczego szukać ręcznie.

W tym zapis, odczyt oraz obsługę kodowania (np. UTF8) .

Obsługa kodowania zaimplementowana jest po stronie języka i menedżerów stringów. Nie ma żadnego problemu, aby dowolne ciągi o dowolnym kodowaniu przechowywać w pliku binarnym.

0

**Ale mimo wszystko moje słowa pozostają prawdziwe. Wszelkie tekstowe pliki konfiguracyjne są zdecydowanie wolniejsze w obsłudze, dlatego że wymagają parsowania ich zawartości oraz konwersji danych z formy natywnej to postaci ciągu znaków i vice versa.

"dlatego że wymagają parsowania" ...
"Parsowanie" to zupełnie inny problem

Żaden język programowania (Pascal, C, C++, C## itp. itd ) nie daje takich możliwości jak assembler, gdzie możesz pisać kod na poziomie maszynowym i optymalizować kod pod kątem przetwarzania nawet pojedynczego bitu. Tylko czy o to chodzi ? Szukamy rozwiązań wydajnych czasowo i prostych w implementacji

0

@furious programming:
Mam prośbę.... Czy mógł byś wyjaśnić tę klauzulę z Twojego kodu:..

TIniValues = specialize TFPGMap<Integer, String>;

Free Pascal nie jest moją mocną stroną ...:)

edt.. znalazłem coś w necie :) . https://www.freepascal.org/docs-html/ref/refse54.html

0

W trybie {$MODE DELPHI} słówko specialize nie jest potrzebne, tak samo jak deklaracja osobnego typu (ze specializacją), bo można to zrobić w miejscu deklaracji (a nawet w wywołaniu konstruktora). Niestety Free Pascal otrzymał wsparcie generyków przed Delphi i wybrano taki dziwaczny sposób (wszędobylskie specjalizacje), który jest nieco irytujący. :D

1 użytkowników online, w tym zalogowanych: 0, gości: 1