version=1.5.6 vdate= 21.10.2019 fname=log4vba.cls ns=%NAMESPACE% fpath=/vba/classes ====== [VBA] Log4vba ====== //Eine Klasse für Access und Excel um Errors abzufangen, zu loggen etc.// ==Version %%version%% %%vdate%%== >//Das Modul hat versteckte Attribute. Damit diese aktiv übernommen werden reicht es nicht aus, den Code in ein neues Modul zu kopieren. Man muss das Modul aus der Datei nach VBA importieren.// >{{popup>:vba:vba_importfile.png|Bild zum Import}} {{%%fname%%|Download %%fname%% (V-%%version%%)}} Mit dem Errohandling und Debug-Informationen in VBA ist es immer wieder mühsam, jedesmal alles auszuprogrammieren. Darum habe ich für mich diese Klasse geschrieben. Sie ist einfach zu implementieren und ermöglicht ein einheitliches Error-Handling über ein gesammtes Projekt. ===== Definitionen ===== > Für die Ausgabe der Resultate verwendete ich die Funktion [[:vba:functions:print_r:|print_r() bzw. d()]]. ==== Compile-Settings ==== Am Anfang der Klasse hat es Compile-Settings. Falls die entsprechenden Module von mir im Projekt vorhanden sind, können die entsprechenden Settings auf True gestellt werden. Die Ausgaben in die Konsole oder in das LogFile können damit verbessert werden. Sie sind abernicht zwingend! === ms_product === Gewisse Funktionen sind abhängig, ob die Klasse in einem Word, Access, Excel verwendet wird. #Const C_ACCESS = "ACCESS" #Const C_EXCEL = "EXCEL" #Const ms_product = C_ACCESS === lib_json_exists === Angabe, ob das [[vba:cast:json]] in diesem Projekt vorhanden ist oder nicht. Falls nicht reduziert sich nur die Ausgabe beim Debugen. Ist aber kein Problem. 'Ab lib_json Version 2.1.0 #Const lib_json_exists = False === lib_printr_exists === Angabe, ob das Modul [[vba:functions:print_r:index]] in diesem Projekt vorhanden ist oder nicht #Const lib_printr_exists = False ==== Creatoren ==== Es gibt verschiedene Möglichkeiten ein Log4vba zu initialisieren ^ Methode ^ Rückgabetyp ^ Beschreibung ^ | [[#instance]] | %%Log4vba%% | Erstellt eine neue Instance | | [[#startMethode]] | %%Log4vba%% | Erstellt ein Clone einer Instance und setzt die Methode fest | | [[#startClass]] | %%Log4vba%% | Erstellt ein Clone einer Instance und setzt die Klasse fest | | [[#construct]] | %%Log4vba%% | Initialisiert ein bestehendes Objekt neu | ==== Auslöser ==== Die folgenden Funktion werden verwendet um ein Fehler oder eine Meldung auszuwerten ^ Methode ^ Rückgabetyp ^ Beschreibung ^ | [[#debugMsg]] | Boolean | Logt eine Nachricht, sofern DebugMode = True ist | | [[#debugValue]] | Boolean | Logt eine Variable, sofern DebugMode = True ist | | [[#info]] | Boolean | Logt als Information | | [[#warning]] | Boolean | Logt als Warnung | | [[#error]] | Boolean | Logt als Error | | logUserType | Boolean | Logt einen ausgewählten Typ. AUch UserType | ==== LogTypen ==== Es gibt verschiedene Logtypen. Diese sind über den Enum eLogType in manchen Funktionen auswählbar. Die Werte sind **nicht** kombinierbar. ^ Typ ^ Enum ^ Wert ^ Beschreibung ^ Auslöser ^ | Debug | ltDebug | 1 | Debug ist dazugeeignet, während dem Entwickeln und Testen verschiedene Informationen auszugeben. Darum werdedie Informationen im auf Debug-Lever nur ausgegeben, wenn die Eigenschaft debugMode auf True gesetzt ist | [[#debugMsg]], [[#debugValue]]| | Info | ltInfo | 2 | Einfache informative Ausgabe. | [[#info]] | | Warning | ltWarning | 3 | Eine Warnung ist zwar wichtig, aber nicht wichtig genug um den Code zu unterbechen | [[#warning]] | | Error | ltError | 4 | Bei einem Fehler. Meistens will man da den Code unterbrechen | [[#error]] | Mit der Funktion addUserType() können Applikationspezifische weitere Levels defniert werden ==== TypeSettings ==== Jedem [[#LogTypen|LogTyp]] kann man definieren, wie er sich verhalten soll. Die Settings des Enum eTypeSetting sind kombinierbar ^ Enum ^ Wert^ Beschreibung ^ | eprNothing | 0| Keine Rück-/Ausgabe | | //Angaben über den Return-Wert. Sind nicht kombinierbar. Bei mehrfachwahl ist wird der Erste verwendet// ||| | eprReturnAssert | 1| Rückgabewert für debug.assert | | eprReturnMsg | 2| Rückgabewert ist die Fehlermeldung | | //Angaben, was mit der Meldung gemacht werden soll. Sie können kombiniert werden// ||| | eprOutConsole | 4| Ausgabe ins Direktfenster | | eprOutMsgBox | 8| Als MassegeBox ausgeben | | eprOutLogFile | 16| In ein LogFile schreiben(DB-Pfad\DB-Name.log) | | eprOutFunction | 512 | Es kann eine Funktion angegeben werden, die ausgeführt wird (eval). Nur für Access | | eprOutTable | 1024 | Die Logeinträge werden in eine Tabelle geschrieben | | //Angaben zur Formatierung. Sie können kombiniert werden// ||| | eprFrmtSourceExtend | 32| Beim zusammensetzen der Source die Originalsource nicht ersetzen sondern erweitern | | eprFrmtNoSource | 256 | Die Source nicht anzeigen | | eprFrmtJson | 64| Json wird höher gewertet als [[#lib_printr_exists]] | | eprFrmtPrintR | 128| PrintR wird höher gewertet als json, wird nur verwendet wenn [[#lib_json_exists]] eingbunden ist | | eprFrmtNoFormat | 2048 | Weder json noch print_r unterstützen, auch wenn die Constante dies zulassen würde | ==== Properties ==== Einige Properties können gesetzt oder gelesen werden. Alle last* Properties sind information zur letzten Meldung, die verarbeitet wurde ^ Name ^ Type ^ Lesen%%(r)%%/Schreiben(w) ^Beschreibung ^ | debugMode | Boolean | rw | DebugMode ein/ausschalten | | typeSetting(eLogType) | eTypeSetting | rw | Die gesammelten Paramter für ein Logtype | | lastLogType | eLogType | r | Letzter Log-Type | | lastLogMessage | String | r | Letzte Message | | lastLogSource | String | r | letzte Source | | lastLogTimeStamp | String | r | Letzter Timestamp | ===== Creatoren ===== ==== singleton() ==== singleton ist ab Version 1.5.0 keine eigene Funktion mehr. Ich habe festgestellt, dass VBA singleton unterstützt, wenn die Einstellung Attribute VB_PredeclaredId = True gesetzt ist. Und das ist bei dieser Klasse der Fall. Um auf die singelton zuzugreiffen, müssen die Klammern weggelassen werden. Log4vba === Beispiel === 'Info mit ausgeben log4vba.info "Test", "Immediate" 02.05.2017 11:31:14 INFO Immediate - Test 'Ausgabe anpassen log4vba.typeSettings(ltInfo) = eprOutConsole+eprFrmtNoSource 'Erneuter Test. Die Source wird nicht mehr ausgegeben log4vba.info "Test", "Immediate" 02.05.2017 11:32:04 INFO Test 'Debug ist normalerweise nicht eingeschaltet Darum keine Ausgabe log4vba.debugMsg "Debug Test" 'Debug einschalten Log4vba.debugMode = True 'Test wiederholen log4vba.debugMsg "Debug Test" 02.05.2017 11:33:00 DEBUG Debug Test ==== instance() ======= Gibt eine neue Instanz von Log4vba zurück object = Log4vba.instance([debugMode]) '/** ' * Erstellt eine neue Instanz von Log4vba ' * @param eLogType Standard LogType ' * @return Log4vba ' */ Public Function instance(Optional ByVal iDebugMode = False) As Log4vba === Beispiel === Dim logger As Log4vba Set logger = Log4vba.instance(true) '... debug.assert logger.error (Err) ==== startMethode() ==== Erstellt einen Clone der aktuellen Instanz und setzt die Methode fest. Der Start und auch später das Ende werden als Debug gelogt. Der Name der Methode muss mitgegeben werden. Zudem empfiehlt es sich, alle Parameter, welche die Methode empfängt, ebenfall zu übergeben. Siehe im Beispiel am Ende der Seite [[#Loggen in einem Projekt]]. Set object = object.startMethode(name [,values]) '/** ' * Startet eine Logging für eine Methode ' * @param String Name der Methode ' * @param Array<> Ein Arry mit den Aufrufparamtern der Method ' * @return Log4vba Ein Clone des Objektes ' */ Public Function startMethode( _ ByVal iSource As String, _ ParamArray iValueArray() As Variant _ ) As Log4vba ==== startClass() ==== Erstellt ein Cloneder aktuellen Instanz und setzt die Klasse fest. Siehe dazu das Beispiel [[#Loggen mit Klassen]] Set object = object.startMethode(class) '/** ' * Startet eine Logging für eine Methode ' * @param Variant Name der Klasse oder die Instanz ' * @return Log4vba Ein Clone des Objektes ' */ Public Function startClass(ByRef iClass As Variant) As Log4vba ==== construct() ==== Iniitialisier ein bestehendes Objekt neu. object.construct([debugMode]) '/** ' * Setzt Standardwerte für eine bestehende Instanz ' * @param eLogType Standard LogType ' * @return Log4vba ' */ Public Function construct(Optional ByVal iDebugMode = False) As Log4vba ===== Auslöser ===== ==== Rückgabewerte der Auslöser ==== Alle Auslöser haben Boolean als Rückgabewert. Wenn eprAssert gesetzt ist, wird ein entsprechender Wert zurück gegeben. Ansonsten immer True. * True: Der Code soll weiterlaufen * False: Der Code soll unterbrochen werden Somit kann der Rückgabewert mit Debug.assert abgefangen werden um in den Debug-Mode zu wechseln. === Beispiel === 'Error auf eprAsset + MsgBox mit den Frage Debug Y/N Log4vba().typeSettings(ltError) = eprAssert + eprMsgBox 'Fehler generieren. In der MsgBox wird Yes gedrückt ? Log4vba().error("Ich bin ein Fehler", vbObjectError) False 'und jetzt mit No drücken. Ergo nicht in den Debug-Mode wechseln ? Log4vba().error("Ich bin ein Fehler", vbObjectError) True ==== debugMsg() ==== Sofern debugMode auf True ist, schreibt der Logger einfach die Message ins Log flag = Log4vba().debugMsg(Message [,Settings]) '/** ' * Nur eine Message im Debugmodus ausgeben ' * @param String Freier Text der im Log erscheint ' * @param eTypeSetting Die Setings für diesen Type einmalig übersteuern ' * @return Boolen/String Rückgabewert für debug.assert oder LogMessage ' */ Public Function debugMsg( _ ByVal iComment As String, _ Optional ByVal iOverwriteSetting As eTypeSetting = eTypeSetting.[_NA] _ ) As Variant ==== debugValue() ==== Parst ein Value in ein String und schreibt diesen zusammen mit einem Kommentar ins Log, sofern debugMode True ist. flag = Log4vba.debugValue([Message] ,Variable [,Settings]) '/** ' * Debug für eine Variable ' * @param String Freier Text der im Log erscheint ' * @param Variant Variable, die ausgewertet werden soll ' * @param eTypeSetting Die Setings für diesen Type einmalig übersteuern ' * @return Boolen/String Rückgabewert für debug.assert oder LogMessage ' */ Public Function debugValue( _ Optional ByVal iComment As String, _ Optional ByVal iValue As Variant, _ Optional ByVal iOverwriteSetting As eTypeSetting = eTypeSetting.[_NA] _ ) As Variant === Beispiel === Ein Beipiel in Direktfenster Log4vba.debugMode = true Log4vba.debugValue "Mein Array", array("Stefan", "Erb", "Winterthur") 25.11.2016 10:41:07 DEBUG Mein Array: ['Stefan','Erb','Winterthur'] Die Ausgabe mit [[#lib_printr_exists]] aktiviert und eprPrintR für ltDebug. Log4vba.debugMode = true Log4vba.typeSettings(ltDebug) = eprConsole+eprPrintR 'Ist als Standard bereits gesetzt. Ist hier nur zur Veranschaulichung Log4vba.debugValue "Mein Array", array("Stefan", "Erb", "Winterthur") 25.11.2016 12:44:09 DEBUG Mein Array ( [0] => 'Stefan' [1] => 'Erb' [2] => 'Winterthur' ) ==== info() ==== Schreibt eine Information. Der Erste Paramter kann ein Text oder ein ErrObject sein flag = Log4vba.info(Err) flag = Log4vba.info(Message [,ErrorBumber [,Source [,Array(Parameters) [,Settings]]]]) '/** ' * @param ErrObject/String ' * @param Long Error-Nummer ' * @param String Name der Methode ' * @param Array<> Ein Arry mit den Aufrufparamtern der Method ' * @param eTypeSetting Die Setings für diesen Type einmalig übersteuern ' * @return Boolen/String Rückgabewert für debug.assert oder LogMessage ' */ Public Function info( _ ByRef iError As Variant, _ Optional ByVal iSource As String, _ Optional ByRef iValueArray As Variant, _ Optional ByVal iNumber As Long, _ Optional ByVal iOverwriteSetting As eTypeSetting = eTypeSetting.[_NA] _ ) As Variant === Paramters === ***iError** Ein ErrObject oder ein Fehler/Lig/Infotext ***//iSource//** Name des Funktion, in der der Fehler abgefangen wird ***//iValueArray//** Array mit den Werten, mit denen die Methode aufgerufen wurde ***//iNumber//** Fehlernummer ***//iOverwriteSetting//** Damit kann das Settings für den Type Info einmalig übersteuert werden === Standartsetting === //eprConsole//\\ Ausgabe in Konsole === Beispiel === Public Sub testLog4vba2(ByVal iId As Long, ByRef iArray As Variant) On Error GoTo Err_Handler 'Fehler generieren Err.raise 11 'Division by zero Exit Sub Err_Handler: Log4vba().info "Manuelle Meldung", "testLog4vba2()" Debug.Assert Log4vba.info(Err, , "testLog4vba2()", Array(iId, iArray)) End Sub testLog4vba2 123, array("Stefan", "Erb", "Winterthur") 25.11.2016 11:19:05 INFO testLog4vba2()() (-2147221504) Manelle Meldung 25.11.2016 11:19:05 INFO testLog4vba2()(123, ['Stefan','Erb','Winterthur']) (11) Division by zero ==== warning() ==== Analog zu [[#info]] === Standartsetting === //eprConsole + eprMsgBox//\\ Ausgabe in Konsole und als MessageBox (OKonly) ==== error() ==== Analog zu [[#info]] === Standartsetting === //eprConsole + eprMsgBox + eprAssert//\\ Ausgabe in Konsole und als Message. Bei der Message kann in den Debug-Mode gewechselt werden, sofern die Methode auf debug.assert gesetzt wird. ===== Ausgabe ===== Die Log-Info können auf verschiedene Arten ausgegeben werden. Das kann sich nach LogType unterscheiden. Die verschiedenen Ausgabetypen sind kombinierbar logger.typeSettings(ltWarning) = eprOutConsole + eprOutLogFile ==== Konsole ==== Mit dem TypeSetting eprOutConsole aktiviert man die Ausgabe in die Konsole (DirektFenster). ==== Message Box ==== Die Meldungen können auch als MsgBox angezeigt werden. Dazu verwendet man das TypeSetting eprOutMsgBox. ==== Logdatei ==== Das Log kann auch in eine Externe Datei geschrieben werden. Mit dem TypeSetting eprOutLogFile wird das aktiviert. Es wirt automatisch eine Datei erstellt und verwendet. Sie ist so zu finden: DB-Pfad\DB-Name.log ==== Eigene Funktion ==== //Nur in MS Access// \\ Man kann die Auswertung des Logs auch einer eigenen Funktion deligieren. Mit dem TypeSetting eprOutFunction wird das aktiviert. Dem Property functionName muss der lokale Funktionsname mitgegeben werden. Es muss eine Funktion und keine Sub sein, da hier mit eval() gearbeitet wird. Die Parameter der Funktion müssen so sein Public Function myLogFunction(ByVal iLogType As eLogType, ByVal iText As String) Weiteres dazu im Beispiel [[#Loggen mit eigener Auswertung eprOutFunction]]. ==== Log Tabelle ==== Das Log kannauch in eine eigene Tabelle (Access) oder Sheet (Excel) geschrieben werden. Dazu beim TypeSetting den Wert eprOutTable setzen und falls der Name nicht T_LOG sein soll, noch den Tabellennamen dem Property logTable übergeben. ===== Weitere Methoden ===== ==== resetSettings() ==== Setzt das Objektauf den Standard zurück. Wenn kein logType mitgegeben wird, dann wird alles zurückgesetzt. Ink. dem debugMode. Ansonsten werde nur die Settings des ausgewählten LogType zurückgesezt object.resetSettings([eLogType) '/** ' * Setzt alle settings auf den Standard zurück ' * @param LogType Falls gesetzt, wird nur der eine Logtype zurückgesetzt ' */ Public Sub resetSettings( _ Optional ByVal iLogType As eLogType = eLogType.[_NA] _ ) ===== Beispiel ===== ==== Loggen in einem Projekt ==== In dem Property myLogger wird der Logger definiert. Die Methode myStart führt nur die Methode myMethode aus. Im Direktfenster sieht man nachher schön, wie die verschieden Schritte durch die Funktionen aufegezeichnet werden myLogger ist eine Statische Instanz von log4vba. Da werden alle Standardsettings gehandhabt '/** ' * Den eigenen Logger definieren ' * @return myLogger ' */ Public Property Get myLogger() As Log4vba Static logger As Log4vba If logger Is Nothing Then Set logger = Log4vba.instance 'Spezielle Settings, wie man die gerne im Projekt hätte logger.typeSettings(ltWarning) = eprOutConsole logger.typeSettings(ltError) = eprOutConsole 'Ich will den DebuMode haben, da mein Projek noch lange nicht fertig ist. logger.debugMode = True End If Set myLogger = logger End Property myStart ist eine einfache Sub, die mindestens eine Nummer als Parameter erwartet. Mit [[#startMethode]] wird eine Instanz von log4vba erstellt, die nur innerhalb der Methode Gültigkeit hat. Public Sub myStart(ByVal iId As String, Optional ByRef iObject As Object) Dim log As Log4vba: Set log = myLogger.startMethode("myStart", iId, iObject) On Error GoTo Err_Handler myMethode iId, Array(1, 2), iObject Exit Sub Err_Handler: Debug.Assert log.error(Err) End Sub Eine Weitere Methode, wieder mit einer eigenen Instanz von log4vba Public Sub myMethode(ByVal iId As String, ByRef iArray As Variant, Optional ByRef iObject As Object) Dim log As Log4vba: Set log = myLogger.startMethode("myMethode", iId, iArray, iObject) Dim nextId As Long Dim dividend As Long On Error GoTo Err_Handler 'Eine Variabel auswerten nextId = iId + 1 log.debugValue "Nächste ID", nextId log.debugValue "übergebener Array", iArray 'Und jetzt ein passiert ein Fehler. Der 11er wird abgefangen dividend = 0 If dividend = 0 Then log.warning "Achtung, Dividend muss definiert werden", "test()", , 11, eprFrmtSourceExtend nextId = iId / dividend 'Und noch ein Fehler. Der 94 nicht nextId = iId / Null Exit Sub Err_Handler: 'Division by zero -> Ersetzen mit Division durch 1 If Err.number = 11 Then log.info Err dividend = 1 Resume End If 'Restliche Fehler als Fehler behandeln Debug.Assert log.error(Err) End Sub Und der Test im Direktfenster zeigt, wie die Methodengebunden Instanzen sauber ausgeben, wo was passiert ist myStart 125, currentdb 30.11.2016 11:35:11 DEBUG myStart - started (125, ) 30.11.2016 11:35:11 DEBUG myMethode - started (125, [1,2], ) 30.11.2016 11:35:11 DEBUG myMethode - Nächste ID: 126 30.11.2016 11:35:11 DEBUG myMethode - übergebener Array: [1,2] 30.11.2016 11:35:11 INFO myMethode - [#11] Division by zero 30.11.2016 11:35:11 ERROR myMethode - [#94] Invalid use of Null 30.11.2016 11:35:11 DEBUG myMethode - ended 30.11.2016 11:35:11 DEBUG myStart - ended ==== Loggen mit Klassen ==== Hier will ich zeigen, wie man mit Klassen und Methoden loggen kann. Als erstes eine Klasse. Hier eine Testklasse mit dem passenden Namen LoggerTest. Das Property myLogger ist im Beispiel weiter oben bereits erläutert. Option Explicit 'Der Klasenlogger Private classLogger As Log4vba '/** ' * Initialisieren des Objektes. Hier wird auch der Klassenlogger serstellt ' */ Private Sub Class_Initialize() Set classLogger = myLogger.startClass(Me) End Sub ' * Eine Trstfunktion ' * @param Long Eine Zahl ' */ Public Sub runTest(ByVal iValue As Long) 'Den Methodenlogger aus dem Klassenlogger erstellen Dim log As Log4vba: Set log = classLogger.startMethode("runTest", iValue) On Error GoTo Err_Handler 'Eine beliebige Info loggen log.info "Jetzt folgt dann gleich ein Fehler" 'En Fehler generieren Debug.Print iValue / 0 Exit Sub Err_Handler: 'Fehler loggen log.error Err End Sub Dann eine lokale Methode, welche die Klasse LoggerTest verwendet. Sub runLoggerTest() Dim log As Log4vba: Set log = myLogger.startMethode("runLoggerTest") Dim test As New LoggerTest test.runTest 1 End Sub Das Resultat: 19.12.2016 09:30:48 DEBUG runLoggerTest - started 19.12.2016 09:30:48 DEBUG LoggerTest - created 19.12.2016 09:30:48 DEBUG LoggerTest.runTest - started (1) 19.12.2016 09:30:48 INFO LoggerTest.runTest - Jetzt folgt dann gleich ein Fehler 19.12.2016 09:30:48 ERROR LoggerTest.runTest - [#11] Division by zero 19.12.2016 09:30:48 DEBUG LoggerTest.runTest - ended 19.12.2016 09:30:48 DEBUG LoggerTest - terminated 19.12.2016 09:30:48 DEBUG runLoggerTest - ended Und wenn man in myLogger den Debug-Mode ausschaltet bliebt noch das folgende übrig 19.12.2016 09:32:58 INFO LoggerTest.runTest - Jetzt folgt dann gleich ein Fehler 19.12.2016 09:32:58 ERROR LoggerTest.runTest - [#11] Division by zero ==== Loggen mit eigener Auswertung (eprOutFunction) ==== Wenn man das Log selber auswerten will, kann man entweder die Klassen erweitern oder man schreibt sich eine Funktion, welche mit eval() ausführbar ist. \\Ich habe zum Beispiel in einem Projekt ein Formular, welches ein grosses Textfeld beinhaltet. Das Formular hat ein Textfeld mit dem Namen txtConsole Public Sub log(ByVal iText As String) Me.txtConsole = IIf(Not IsNull(Me.txtConsole), Me.txtConsole & vbCrLf, Empty) & iText Me.txtConsole.SelStart = Len(Me.txtConsole) Me.txtConsole.SelLength = 0 End Sub in einem Modul habe ich die Funktion %%writeToConsole()%% hinterlegt. Diese öffnet das Formular FRM_SYS_CONSOLE und übergibt den Logtext. Die Funktion muss die 2 Parameter eLogType und String haben. Diese werden vom eval() abgefüllt '/** ' * Funktion um die Logger-Beiträge in das Konsolenformular zu schreiben ' * Die Typen und Reihenfolge der Paramter sind durch log4vba definiert ' * @param eLogType ' * @param String ' */ Public Function writeToConsole(ByVal iLogType As eLogType, ByVal iText As String) If Not CurrentProject.AllForms("FRM_SYS_CONSOLE").IsLoaded Then DoCmd.OpenForm "FRM_SYS_CONSOLE" Form_FRM_SYS_CONSOLE.log iText End Function Meine Loggerdefinition Beinhaltet die folgende Einstellung Set cLog = Log4vba.instance() cLog.functionName = "writeToConsole" cLog.typeSettings(ltInfo) = eprOutConsole + eprFrmtNoSource + eprOutFunction Wenn ich nun eine Info logge, dann wird writeToConsole() ausgeführt ===== Code ===== {{%%fname%%|Download %%fname%% (V-%%version%%)}}