﻿;-- TIME_STAMP   2022-12-12 18:37:27   v 0.1



#cs     FUNCTIONS

    About               The database is a map. The map stores some administrative keys that are for
                        internal use only. The datasets are added as a map and the passed
                        (or automatically generated) ID is their key in the DB map.

    _MapDB_New          Creates a new database map. Pass the dataset field names as semicolon-separated string or array.
                        Stores the passed field names inside. Field names are case sensitive!
                        Returns the map. The number of datasets is indicated by: $myDBMap.Count
                        The user can add an array of additional database fields. When you remove all
                        datasets from the DB, you can keep their values or delete the values as well (default).

    _MapDB_FromCSV      Creates a map database from a passed csv file.

    _MapDB_ToCSV        Stores a map database as passed csv file.

    _MapDB_Add          Adds a new dataset. Each field must be assigned a value (empty string if necessary).
                        - A maximum of 10 single values can be passed as direct parameters.
                        - Values can also be passed as an array. The number is not limited here, it only
                          has to match the defined number of fields.
                        - Values can also be passed as an map. The field names must match the field
                          names in the database definition.
                        To make the record unique, an ID must be passed.
                        If an empty string is passed instead, a consecutive numeric ID is generated.
                        The return value of the function is the ID.

    _MapDB_Modify       Modifies one or more values of a dataset.
                        If data is passed as direct parameters, array or map, an empty string must be passed
                        for values that are not to be changed. Each field must be assigned a value.
                        The fourth and certainly most common variant is to pass an assignment string of
                        the form "field1=value<DELIM>field2=value<DELIM>...".
                        Opt('GUIDataSeparatorChar') is used as <DELIM>. A sequence or number of values
                        need not be considered here.
                        Note!
                        Field names must be passed in the same notation (case sensitive) as stored
                        in the database definition.

    _MapDB_Remove       Removes a dataset or, if an empty ID is passed, all datasets from the database.
                        Returns the number of deletions.

    _MapDB_DSToString   Returns field and values of an DS by passed ID as string: "[field1] = value; [field2] = value;... "

    _MapDB_DSToArray    Returns field and values of an DS by passed ID as 2D array: [[field1,value],[field2,value],...] "

Rudimentary search functions:

    _MapDB_SearchAnd    Searches with one or more conditions passed as a string like "field1=val<DELIM>field2=val<DELIM>...".
                        All conditions must match.
                        Returns a map with one or more datasets matching the conditions.

    _MapDB_SearchOr     Searches with one or more conditions passed as a string like "field1=val<DELIM>field2=val<DELIM>...".
                        At least one condition must match.
                        Returns a map with one or more datasets matching the conditions.

#ce


#include-once
#include <File.au3>
#include <FileConstants.au3>


Func _MapDB_New($_vKeyNames, $_aAdditionalDBKeys='')
    Local $map[], $map2compare[]
    Switch VarGetType($_vKeyNames)
        Case 'String'
            $_vKeyNames = StringSplit($_vKeyNames, ';', 2)
        Case 'Array'
            ; nothing
        Case Else
            Return SetError(1,0,0)
    EndSwitch
    $map.__KEYNAMES_ARRAY__ = $_vKeyNames
    For $i = 0 To UBound($_vKeyNames) -1
        $map2compare[$_vKeyNames[$i]] = ''
    Next
    $map.__MAP_2_COMPARE__ = $map2compare
    $map.__MAP_MAX_NUMKEY__ = -1
    $map.Count = 0
    $map.__ADDKEYS_ARRAY__ = $_aAdditionalDBKeys
    If IsArray($_aAdditionalDBKeys) Then
        For $i = 0 To UBound($_aAdditionalDBKeys) -1
            $map[$_aAdditionalDBKeys[$i]] = ''
        Next
    EndIf
    Return $map
EndFunc


Func _MapDB_FromCSV($_sPathCSV, $_sDelim=';')
    Local $aCSV
    _FileReadToArray($_sPathCSV, $aCSV, 0)
    If @error Then Return SetError(@error,0,0)
    If $_sDelim <> ';' Then $aCSV[0] = StringReplace($aCSV[0], $_sDelim, ';')
    Local $map = _MapDB_New($aCSV[0]), $aLine
    For $i = 1 To UBound($aCSV) -1
        $aLine = StringSplit($aCSV[$i], $_sDelim, 2)
        _MapDB_Add($map, '', $aLine)
    Next
    Return $map
EndFunc


Func _MapDB_ToCSV(ByRef $_mDB, $_sPathCSV, $_bOverwrite=False, $_sDelim=';')
    If $_mDB.Count = 0 Then Return SetError(1,0,0)
    If FileExists($_sPathCSV) And Not $_bOverwrite Then Return SetError(2,0,0)
    Local $sHead = '', $sLine = '', $sLines = ''
    Local $mDS, $sSkip = '  __ADDKEYS_ARRAY__  __KEYNAMES_ARRAY__  __MAP_2_COMPARE__  __MAP_MAX_NUMKEY__  Count  '
    Local $addKeys = $_mDB.__ADDKEYS_ARRAY__
    If IsArray($addKeys) Then
        For $i = 0 To UBound($addKeys) -1
            $sSkip &= $addKeys[$i] & '  '
        Next
    EndIf
    Local $aKeys = $_mDB.__KEYNAMES_ARRAY__
    For $i = 0 To UBound($aKeys) -1 ; column names
        $sHead &= $aKeys[$i] & $_sDelim
    Next
    $sHead = StringTrimRight($sHead, 1) & @CRLF
    For $kDB In MapKeys($_mDB)
        If StringInStr($sSkip, '  ' & $kDB & '  ') Then ContinueLoop ; reserved keys
        $mDS = $_mDB[$kDB]
        $sLine = ''
        For $i = 0 To UBound($aKeys) -1
            $sLine &= $mDS[$aKeys[$i]] & $_sDelim
        Next
        $sLines &= StringTrimRight($sLine, 1) & @CRLF
    Next
    Local $fH = FileOpen($_sPathCSV, BitOR($FO_OVERWRITE,$FO_CREATEPATH,$FO_UTF8_NOBOM))
    FileWrite($fH, $sHead & $sLines)
    FileClose($fH)
EndFunc



; Aufruf:
; 1. Daten als geordnete Variablen (max. 10), Reihenfolge wie Key-Liste     ODER
; 2. Daten als Array in "$_v1", Reihenfolge wie Key-Liste                   ODER
; 3. Daten als Map in "$_v1", Keys müssen übereinstimmen mit DB
; Zur Erstellung eines DS ist eine eindeutige ID (String od. positiv Integer) notwendig. Bei Weglassung (Leerstring) wird eine nummerische ID automatisch generiert.
; Die ID ist der Returnwert der Funktion.
Func _MapDB_Add(ByRef $_mDB, $_ID='', $_v1='', $_2=Null, $_3=Null, $_4=Null, $_5=Null, $_6=Null, $_7=Null, $_8=Null, $_9=Null, $_10=Null)
    If $_ID = '' Then
        $_mDB.__MAP_MAX_NUMKEY__ += 1
        $_ID = $_mDB.__MAP_MAX_NUMKEY__
    Else
        If MapExists($_mDB, $_ID) Then
            Return SetError(1,0,0)
        Else
            If IsInt($_ID) Then
                If $_mDB.__MAP_MAX_NUMKEY__ < $_ID Then $_mDB.__MAP_MAX_NUMKEY__ = $_ID
            EndIf
        EndIf
    EndIf

    Switch VarGetType($_v1)
        Case 'Array'
            Local $aVal[UBound($_mDB.__KEYNAMES_ARRAY__)]
            If UBound($_v1) <> UBound($aVal) Then Return SetError(4,0,0)
            For $i = 0 To UBound($aVal) -1
                $aVal[$i] = $_v1[$i]
            Next
            $_mDB.Count += 1
            Return __MapDB_AddDSArray($_mDB, $_ID, $aVal)
        Case 'Map'
            Local $resComp = __MapCompareKeys($_v1, $_mDB.__MAP_2_COMPARE__)
            If @error Then
                Return SetError(@error,0,$resComp)  ; @error: 2=Key-Count, 3=Key-Names
            Else
                $_mDB[$_ID] = $_v1
                $_mDB.Count += 1
                Return $_ID
            EndIf
        Case 'String'
            Local $aParam[] = [$_v1,$_2,$_3,$_4,$_5,$_6,$_7,$_8,$_9,$_10], $n = 0
            For $i = 0 To UBound($aParam) -1
                $n += 1
                If $aParam[$i] = Null Then
                    $n -= 1
                    ExitLoop
                EndIf
            Next
            ReDim $aParam[$n]
            If $n <> UBound($_mDB.__KEYNAMES_ARRAY__) Then
                Return SetError(4,0,0)
            Else
                $_mDB.Count += 1
                Return __MapDB_AddDSArray($_mDB, $_ID, $aParam)
            EndIf
    EndSwitch
EndFunc


; Aufruf:
; 1. Daten als geordnete Variablen (max. 10), Felder, die nicht verändert werden sollen, werden als Leerstring übergeben    ODER
; 2. Daten als Array in "$_v1", Reihenfolge und Belegung, wie vorab                                                         ODER
; 3. Daten als Map in "$_v1", hier eigentlich nur sinnvoll, wenn alle Inhalte geändert werden sollen                        ODER
; 4. "$_v1" enthält Zuweisungsstring der Form: "field-1=val<DELIM>field-2=val<DELIM>....". <DELIM> = Opt('GUIDataSeparatorChar')
Func _MapDB_Modify(ByRef $_mDB, $_ID, $_v1='', $_2=Null, $_3=Null, $_4=Null, $_5=Null, $_6=Null, $_7=Null, $_8=Null, $_9=Null, $_10=Null)
    If Not MapExists($_mDB, $_ID) Then Return SetError(1,0,0)

    Switch VarGetType($_v1)
        Case 'Array'
            Local $aVal[UBound($_mDB.__KEYNAMES_ARRAY__)]
            If UBound($_v1) <> UBound($aVal) Then Return SetError(1,0,0)
            For $i = 0 To UBound($aVal) -1
                $aVal[$i] = $_v1[$i]
            Next
            Return __MapDB_ModifyByArray($_mDB, $_ID, $aVal)
        Case 'Map'
            Local $resComp = __MapCompareKeys($_v1, $_mDB.__MAP_2_COMPARE__)
            If @error Then
                Local $aErr[] = [$resComp]
                Return SetError(@error,0,$aErr)  ; @error: 2=Key-Count, 3=Key-Names
            Else
                For $k In MapKeys($_v1)
                    If $_v1[$k] <> '' Then $_mDB[$_ID][$k] = $_v1[$k]
                Next
                Return $_ID
            EndIf
        Case 'String'
            Local $mKeyNames = $_mDB.__MAP_2_COMPARE__, $sKeyErr = ''
            Local $sDelim = Opt('GUIDataSeparatorChar')
            Local $aFieldVal = StringSplit($_v1, $sDelim, 1), $aAssignment
            If Not @error Then
                For $i = 1 To $aFieldVal[0]
                    $aAssignment = StringSplit($aFieldVal[$i], '=', 2)
                    If MapExists($mKeyNames, $aAssignment[0]) Then
                        $_mDB[$_ID][$aAssignment[0]] = $aAssignment[1]
                    Else
                        $sKeyErr &= $aAssignment[0] & ';'
                    EndIf
                Next
                If $sKeyErr <> '' Then
                    Local $aKeyErr = StringSplit(StringTrimRight($sKeyErr, 1), ';', 2)
                    Return SetError(3,UBound($aKeyErr),$aKeyErr)
                EndIf
            Else
                Local $aParam[] = [$_v1,$_2,$_3,$_4,$_5,$_6,$_7,$_8,$_9,$_10], $n = 0
                For $i = 0 To UBound($aParam) -1
                    $n += 1
                    If $aParam[$i] = Null Then
                        $n -= 1
                        ExitLoop
                    EndIf
                Next
                ReDim $aParam[$n]
                If $n <> UBound($_mDB.__KEYNAMES_ARRAY__) Then
                    Return SetError(1,0,0)
                Else
                    Return __MapDB_ModifyByArray($_mDB, $_ID, $aParam)
                EndIf
            EndIf
    EndSwitch
EndFunc

; $_ID='' removes all datasets
; $_bClearAddkeyValues=False leaves the additional keys untouched
; returns number of removed datasets
Func _MapDB_Remove(ByRef $_mDB, $_ID='', $_bClearAddkeyValues=True)
    Local $nDeleted = 0, $aAddKeys
    If $_ID = '' Then
        $nDeleted = $_mDB.Count
        If Not $_bClearAddkeyValues Then
            $aAddKeys = $_mDB.__ADDKEYS_ARRAY__
            Local $aAddVal[UBound($aAddKeys)]
            For $i = 0 To UBound($aAddKeys) -1
                $aAddVal[$i] = $_mDB[$aAddKeys[$i]]
            Next
        EndIf
        $_mDB = _MapDB_New($_mDB.__KEYNAMES_ARRAY__)
        ; restore AddValues
        If Not $_bClearAddkeyValues Then
            For $i = 0 To UBound($aAddKeys) -1
                $_mDB[$aAddKeys[$i]] = $aAddVal[$i]
            Next
        EndIf
    Else
        If MapExists($_mDB, $_ID) Then
            MapRemove($_mDB, $_ID)
            $_mDB.Count -= 1
            $nDeleted = 1
        EndIf
    EndIf
    Return SetError(($nDeleted = 0 ? 1 : 0),0,$nDeleted)
EndFunc


Func _MapDB_SearchAnd(ByRef $_mDB, $_sConditions)
    Return __MapDB_Search($_mDB, $_sConditions, 'and')
EndFunc


Func _MapDB_SearchOr(ByRef $_mDB, $_sConditions)
    Return __MapDB_Search($_mDB, $_sConditions, 'or')
EndFunc


Func __MapDB_Search(ByRef $_mDB, $_sConditions, $_mode='and')
    Local $mKeyNames = $_mDB.__MAP_2_COMPARE__, $sKeyErr = ''
    Local $sDelim = Opt('GUIDataSeparatorChar')
    Local $aFieldVal = StringSplit($_sConditions, $sDelim, 1), $aComparison
    ; check field names
    For $i = 1 To $aFieldVal[0]
        $aComparison = StringSplit($aFieldVal[$i], '=', 2)
        If Not MapExists($mKeyNames, $aComparison[0]) Then Return SetError(1,0,0)
    Next
    ; comparison
    Local $mDS, $sSkip = '  __ADDKEYS_ARRAY__  __KEYNAMES_ARRAY__  __MAP_2_COMPARE__  __MAP_MAX_NUMKEY__  Count  '
    Local $addKeys = $_mDB.__ADDKEYS_ARRAY__
    If IsArray($addKeys) Then
        For $i = 0 To UBound($addKeys) -1
            $sSkip &= $addKeys[$i] & '  '
        Next
    EndIf
    Local $mResults[], $iRes
    For $key In MapKeys($_mDB)
        If StringInStr($sSkip, '  ' & $key & '  ') Then ContinueLoop ; reserved keys

        $mDS = $_mDB[$key]
        $iRes = 0
        For $i = 1 To $aFieldVal[0]
            $aComparison = StringSplit($aFieldVal[$i], '=', 2)
            If $mDS[$aComparison[0]] = $aComparison[1] Then $iRes += 1
            If $_mode = 'or' And $iRes > 0 Then ; at least one comparison successful
                $mResults[$key] = $mDS
                ExitLoop
            EndIf
        Next
        If $_mode = 'and' And $iRes = $aFieldVal[0] Then ; each comparison successful
            $mResults[$key] = $mDS
        EndIf
    Next
    Return $mResults
EndFunc

; one DS as string
Func _MapDB_DSToString(ByRef $_mDB, $_ID)
    If Not MapExists($_mDB, $_ID) Then Return SetError(1,0,0)
    Local $mDS = $_mDB[$_ID]
    $sDS = ''
    For $kDS In MapKeys($mDS)
        $sDS &= StringFormat('[%s] = %s; ', $kDS, $mDS[$kDS])
    Next
    Return StringTrimRight($sDS, 2)
EndFunc

; one DS as 2D array
Func _MapDB_DSToArray(ByRef $_mDB, $_ID)
    If Not MapExists($_mDB, $_ID) Then Return SetError(1,0,0)
    Local $mDS = $_mDB[$_ID], $n = 0
    Local $aKeys = MapKeys($mDS)
    Local $aResult[UBound($aKeys)][2]
    For $kDS In $aKeys
        $aResult[$n][0] = $kDS
        $aResult[$n][1] = $mDS[$kDS]
        $n += 1
    Next
    Return $aResult
EndFunc

; all DS as 2D array
Func _MapDB_ToArray(ByRef $_mDB)
    Local $sSkip = '', $aResult[1], $aKeysM, $aKeys[1], $iR = 1, $iCount, $mDS
    If MapExists($_mDB, 'Count') And MapExists($_mDB, '__KEYNAMES_ARRAY__')  And MapExists($_mDB, '__ADDKEYS_ARRAY__') Then
        $sSkip = '  __ADDKEYS_ARRAY__  __KEYNAMES_ARRAY__  __MAP_2_COMPARE__  __MAP_MAX_NUMKEY__  Count  '
        Local $addKeys = $_mDB.__ADDKEYS_ARRAY__
        If IsArray($addKeys) Then
            For $i = 0 To UBound($addKeys) -1
                $sSkip &= $addKeys[$i] & '  '
            Next
        EndIf
        $aKeys = $_mDB.__KEYNAMES_ARRAY__
        ReDim $aResult[$_mDB.Count+1][UBound($aKeys)]
    Else
        $aKeysM = MapKeys($_mDB)
        $iCount = UBound($aKeysM)
        $mDS1 = $_mDB[$aKeysM[0]]
        $aKeys = MapKeys($mDS1)
        ReDim $aResult[$iCount+1][UBound($aKeys)]
    EndIf
    For $i = 0 To UBound($aKeys) -1 ; column names
        $aResult[0][$i] = $aKeys[$i]
    Next
    For $kDB In MapKeys($_mDB)
        If StringInStr($sSkip, '  ' & $kDB & '  ') Then ContinueLoop ; reserved keys
        $mDS = $_mDB[$kDB]
        For $i = 0 To UBound($aKeys) -1
            $aResult[$iR][$i] = $mDS[$aKeys[$i]]
        Next
        $iR += 1
    Next

    Return $aResult
EndFunc



#Region - Internal only
; adds new data set by a passed array
Func __MapDB_AddDSArray(ByRef $_mDB, $_ID, $_array)
    Local $map[]
    Local $aKeyNames = $_mDB.__KEYNAMES_ARRAY__
    For $i = 0 To UBound($_array) -1
        $map[$aKeyNames[$i]] = $_array[$i]
    Next
    $_mDB[$_ID] = $map
    Return $_ID
EndFunc

; modifies a data set by a passed array
Func __MapDB_ModifyByArray(ByRef $_mDB, $_ID, $_aVal)
    Local $aKeyNames = $_mDB.__KEYNAMES_ARRAY__
    For $i = 0 To UBound($aKeyNames) - 1
        If $_aVal[$i] <> '' Then $_mDB[$_ID][$aKeyNames[$i]] = $_aVal[$i]
    Next
    Return $_ID
EndFunc

; compares a test map with a comparison map regarding 1. the number of keys and 2. the names of the keys
; - different numbers: @error = 2, returns: [count map_test, count map_comparison]
; - different names: @error = 3, returns: number of differences
Func __MapCompareKeys(ByRef $_test_map, ByRef $_comparison_map)
    ; 1. Comparison Key-Count
    Local $aCount[] = [__MapCountKeys($_test_map),__MapCountKeys($_comparison_map)]
    If $aCount[0] <> $aCount[1] Then Return SetError(2,0,$aCount)
    ; 2. Comparison Key-Names
    Local $countDiff = 0
    For $k In MapKeys($_test_map)
        If Not MapExists($_comparison_map, $k) Then $countDiff += 1
    Next
    Return SetError(($countDiff = 0 ? 0 : 3),0,$countDiff)
EndFunc

; counts the keys of a map
Func __MapCountKeys(ByRef $_map)
    Local $count = 0
    For $k In MapKeys($_map)
        $count += 1
    Next
    Return $count
EndFunc

#EndRegion
