Code Monkey home page Code Monkey logo

stdvba's Introduction

stdVBA

A Collection of libraries to form a common standard layer for modern VBA applications.

Benefits

  • Code faster!
  • Improve code maintainability.
  • Let the library handle the complicated stuff, you focus on the process
  • Heavily inspired by JavaScript APIs - More standard
  • Open Source - Means the libraries are continually maintained by the community. Want something added, help us make it!

The full roadmap has more detailed information than here.

Short example

Please visit the examples repository for more fully featured applications/examples using this library.

sub Main()
  'Create an array
  Dim arr as stdArray
  set arr = stdArray.Create(1,2,3,4,5,6,7,8,9,10) 'Can also call CreateFromArray

  'Demonstrating join, join will be used in most of the below functions
  Debug.Print arr.join()                                                 '1,2,3,4,5,6,7,8,9,10
  Debug.Print arr.join("|")                                              '1|2|3|4|5|6|7|8|9|10

  'Basic operations
  arr.push 3
  Debug.Print arr.join()                                                 '1,2,3,4,5,6,7,8,9,10,3
  Debug.Print arr.pop()                                                  '3
  Debug.Print arr.join()                                                 '1,2,3,4,5,6,7,8,9,10
  Debug.Print arr.concat(stdArray.Create(11,12,13)).join                 '1,2,3,4,5,6,7,8,9,10,11,12,13
  Debug.Print arr.join()                                                 '1,2,3,4,5,6,7,8,9,10 'concat doesn't mutate object
  Debug.Print arr.includes(3)                                            'True
  Debug.Print arr.includes(34)                                           'False

  'More advanced behaviour when including callbacks! And VBA Lamdas!!
  Debug.Print arr.Map(stdLambda.Create("$1+1")).join          '2,3,4,5,6,7,8,9,10,11
  Debug.Print arr.Reduce(stdLambda.Create("$1+$2"))           '55 ' I.E. Calculate the sum
  Debug.Print arr.Reduce(stdLambda.Create("application.worksheetFunction.Max($1,$2)"))      '10 ' I.E. Calculate the maximum
  Debug.Print arr.Filter(stdLambda.Create("$1>=5")).join      '5,6,7,8,9,10
  
  'Execute property accessors with Lambda syntax
  Debug.Print arr.Map(stdLambda.Create("ThisWorkbook.Sheets($1)")) _ 
                 .Map(stdLambda.Create("$1.Name")).join(",")            'Sheet1,Sheet2,Sheet3,...,Sheet10
  
  'Execute methods with lambdas and enumerate over enumeratable collections:
  Call stdEnumerator.Create(Application.Workbooks).forEach(stdLambda.Create("$1.Save")
  
  'We even have if statement!
  With stdLambda.Create("if $1 then ""lisa"" else ""bart""")
    Debug.Print .Run(true)                                              'lisa
    Debug.Print .Run(false)                                             'bart
  End With
  
  'Execute custom functions
  Debug.Print arr.Map(stdCallback.CreateFromModule("ModuleMain","CalcArea")).join  '3.14159,12.56636,28.274309999999996,50.26544,78.53975,113.09723999999999,153.93791,201.06176,254.46879,314.159

  'Let's move onto regex:
  Dim oRegex as stdRegex
  set oRegex = stdRegex.Create("(?<county>[A-Z])-(?<city>\d+)-(?<street>\d+)","i")

  Dim oRegResult as object
  set oRegResult = oRegex.Match("D-040-1425")
  Debug.Print oRegResult("county") 'D
  Debug.Print oRegResult("city")   '040
  
  'And getting all the matches....
  Dim sHaystack as string: sHaystack = "D-040-1425;D-029-0055;A-100-1351"
  Debug.Print stdEnumerator.CreateFromIEnumVARIANT(oRegex.MatchAll(sHaystack)).map(stdLambda.Create("$1.item(""county"")")).join 'D,D,A
  
  'Dump regex matches to range:
  '   D,040,040-1425
  '   D,029,029-0055
  '   A,100,100-1351
  Range("A3:C6").value = oRegex.ListArr(sHaystack, Array("$county","$city","$city-$street"))
  
  'Copy some data to the clipboard:
  Range("A1").value = "Hello there"
  Range("A1").copy
  Debug.Print stdClipboard.Text 'Hello there
  stdClipboard.Text = "Hello world"
  Debug.Print stdClipboard.Text 'Hello world

  'Copy files to the clipboard.
  Dim files as collection
  set files = new collection
  files.add "C:\File1.txt"
  files.add "C:\File2.txt"
  set stdClipboard.files = files

  'Save a chart as a file
  Sheets("Sheet1").ChartObjects(1).copy
  Call stdClipboard.Picture.saveAsFile("C:\test.bmp",false,null) 'Use IPicture interface to save to disk as image
End Sub

Public Function CalcArea(ByVal radius as Double) as Double
  CalcArea = 3.14159*radius*radius
End Function

Motivation

VBA first appeared in 1993 (over 25 years ago) and the language's age is definitely noticable. VBA has a lot of specific libraries for controlling Word, Excel, Powerpoint etc. However the language massively lacks in generic modern libraries for accomplishing common programming tasks. VBA projects ultimately become a mish mash of many different technologies and programming styles. Commonly for me that means calls to Win32 DLLs, COM libraries via late-binding, calls to command line applications and calls to .NET assemblies.

Over time I have been building my own libraries and have gradually built my own layer above the simple VBA APIs.

The VBA Standard Library aims to give users a set of common libraries, maintained by the community, which aid in the building of VBA Applications.

Road Map

This project is has been majorly maintained by 1 person, so progress is generally very slow. This said, generally the road map corresponds with what I need at the time, or what irritates me. In general this means fundamental features are more likely to be complete first, more complex features will be integrated towards the end. This is not a rule, i.e. stdSharepoint is mostly complete without implementation of stdXML which it'd use. But as a general rule of thumb things will be implemented in the following order:

  • Types - stdArray, stdDictionary, stdRegex, stdDate, stdLambda, ...
  • Data - stdJSON, stdXML, stdOXML, stdCSON, stdIni, stdZip
  • File - stdShell
  • Automation - stdHTTP, stdAcc, stdWindow, stdKernel
  • Excel specific - xlFileWatcher, xlProjectBuilder, xlTimer, xlShapeEvents, xlTable
  • Runtimes - stdCLR, stdPowershell, stdJavascript, stdOfficeJSBridge

As an indicator of where my focuses have been in the past year, take a look at the following heat map:

changesHeatMap

Planned Global Objects:

Color Status Type Name Docs Description
l HOLD Debug stdError None Better error handling, including stack trace and error handling diversion and events.
l READY Type stdArray None A library designed to re-create the Javascript dynamic array object.
l READY Type stdEnumerator docs A library designed to wrap enumerable objects providing additional functionality.
l WIP Type stdDictionary None A drop in replacement for VBScript's dictionary.
l READY Type stdDate None A standard date parsing library. No more will you have to rely on Excel's interpreter. State the format, get the data.
l READY Type stdRegex None A regex library with more features than standard e.g. named capture groups and free-spaces.
l READY Type stdLambda docs Build and create in-line functions. Execute them at a later stage.
l READY Type stdCallback None Link to existing functions defined in VBA code, call them at a later stage.
l READY Type stdCOM None A wrapper around a COM object which provides Reflection (through ITypeInfo), Interface querying, Calling interface methods (via DispID) and more.
l READY Automation stdClipboard None Clipboard management library. Set text, files, images and more to the clipboard.
l HOLD Automation stdHTTP None A wrapper around Win HTTP libraries.
l READY Automation stdWindow docs A handy wrapper around Win32 Window management APIs.
l READY Automation stdProcess None Create and manage processes.
l READY Automation stdAcc docs Use Microsoft Active Accessibility framework within VBA - Very useful for automation.
l READY Automation stdWebSocket None WebSocket automation. Currently uses IE, need to move to more stable runtime. Will be useful for modern automation e.g. chrome
l WIP Excel xlTable None Better tables for VBA, e.g. Map rows etc.
l READY DevTools stdPerformance None Performance testing

The full roadmap has more detailed information than here.

Statuses

_ READY

APIs which are ready to use, and although are not fully featured are in a good enough working state.

_ WIP

APIs which are WIP are not necessarily being worked on currently but at least are recognised for their importance to the library. These will be lightly worked on/thought about continuously even if no commits are made.

As of Oct 2020, this status typically consists of:

  • data types, e.g. stdEnumerator, stdDictionary, stdTable;
  • Unit testing;
  • Tasks difficult to automate otherwise e.g. stdClipboard, stdAccessibility;

_ HOLD

APIs where progress has been temporarily halted, and/or is currently not a priority.

In the early days we'll see this more with things which do already have existing work arounds and are not critical, so projects are more likely to fit into this category.

_ UNK

APIs which have been indefinitely halted. We aren't sure whether we need these or if they really fit into the project. They are nice to haves but not necessities for the project as current. These ideas may be picked up later. All feature requests will fit into this category initially.

Structure

All modules or classes will be prefixed by std if they are generic libraries.

Application specific libraries to be prefixed with xl, wd, pp, ax representing their specific application.

Commonly implementations will use the factory class design pattern:

Class stdClass
  Private bInitialised as boolean

  'Creates an object from the given parameters
  '@constructor
  Public Function Create(...) As stdClass
    if not bInitialised then
      Set Create = New stdClass
      Call Create.init(...)
    else
      Call CriticalRaise("Constructor called on object not class")
    End If
  End Function

  'Initialises the class. This method is meant for internal use only. Use at your own risk.
  '@protected
  Public Sub init(...)
    If bInitialised Then
      Call CriticalRaise("Cannot run init() on initialised object")
    elseif Me is stdClass then
      Call CriticalRaise("Cannot run init() on static class")
    else
      'initialise with params...

      'Make sure bInitialised is set
      bInitialised=true
    End If
  End Sub

  Private Sub CriticalRaise(ByVal sMsg as string)
    if isObject(stdError) then
      stdError.Raise sMsg
    else
      Err.Raise 1, "stdClass", sMsg
    end if
  End Sub
  
  '...
End Class

With the above example, the Regex class is constructed with the Create() method, which can only be called on the stdRegex static class. We will try to keep this structure across all STD VBA classes.

Contributing

If you are looking to contribute to the VBA standard library codebase, the best place to start is the GitHub "issues" tab. This is also a great place for filing bug reports and making suggestions for ways in which we can improve the code and documentation. A list of options of different ways to contribute are listed below:

  • If you have a Feature Request - Create a new issue
  • If you have found a bug - Create a new issue
  • If you have written some code which you want to contribute see the Contributing Code section below.

Contributing Code

There are several ways to contribute code to the project:

  • Opening pull requests is the easiest way to get code intergrated with the standard library.
  • Create a new issue and providing the code in a code block - Bare in mind, it will take us a lot longer to pick this up than a standard pull request.

Please make sure code contributions follow the following guidelines:

  • stdMyClass.cls should have Attribute VB_PredeclaredId = True.
  • Attribute VB_Name should follow the STD convention e.g. "stdMyClass"
  • Follow the STD constructor convention stdMyClass.Create(...).
  • Ensure there are plenty of comments where required.
  • Ensure lines end in \r\n and not \n only. You can enable the Git filter in charge of making this automatic conversion by running git config include.path ../.gitconfig

As long as these standard conventions are met, the rest is up to you! Just try to be as general as possible! We're not necessarily looking for optimised code, at the moment we're just looking for code that works!

Note: Ensure that all code is written by you. If the code is not written by you, you will be responsible for any repercussions!

Inspiration documents

Inspiration was initially stored in this repository, however the vast swathes of examples, knowledge and data became too big for this repository, therefore it was moved to:

https://github.com/sancarn/VBA-STD-Lib-Inspiration

stdvba's People

Contributors

6diegodiego9 avatar decimalturn avatar lopperman avatar renenyffenegger avatar sancarn avatar sihlfall avatar tarvk avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

stdvba's Issues

Unable to push vbObject into Array

Description

Currently if a class is pushed to a stdArray it throws a runtime error

Expected Result

The stdArray should work as the javascript array without throwing an error

Example

Dim myClass as new MyClassModule
Dim arr as new stdArray
Set arr = stdArray.create()

arr.push(myClass)

Error

Runtime error: 438

Object does not support this property or method

Workaround

My Current Workaround for this problem is that I convert the vbObject to a vbVariant

stdProgress

stdProgress

Concept

The concept is to be able to build a Progress object, which can continuously sub-divide progress (and ultimately update a progress bar). This progress object could also be supplied as a parameter to functions which increases the ease of using it as a progress meter.

Example 1:

With stdProgress.CreateFromPercents(0.9,0.1).bindForm(myProgressBar)
  Dim allSheetNames as collection: set allSheetNames = new collection
  With .CreateFromCount(workbooks.count)
    For each wb in workbooks
      With .CreateFromCount(wb.sheets.count)
        for each sheet in wb.sheets
          allSheetNames.add sheet.name
          .step
        next
      End With
    next
  End With
  With .CreateFromCount(allSheetNames.count)
      Dim vSheetName
      for each vSheetName in allSheetNames
        debug.print vSheetName
        .step
      next
  End with
End With
  1. We divide the work into a 0.9 and 0.1 split, i.e. 90% and 10%.
  2. We subdivide the 90% into workbook.count parts. Assume 2 workbooks are open, so that's 45% each.
  3. We subdivide the 45% into wb.sheets.count parts. Assume 5 sheets per workbook so 9% per sheet.
  4. We step for every sheet name added to the workbook incrementing up the tree till the parent, which updates the form.
  5. Upon finishing we've updated 90% of the progress, 10% remains
  6. We divide this 10% into the number of sheets (10) so 1% for each sheet.
  7. Each sheet we increment by this value
  8. We unload the object as progress is completed

Example 2

With stdProgress.CreateFromCount(3)
  'do some task
  .step()
  'do some other task
  .step()
  'do some other task
  .step() 'optional, as the progress bar will unload immediately after this method in this case
End With

Example 3

Sub test(pb)
  with pb.CreateFromCount(workbooks.count)
    for each wb in workbooks
      if wb.name = sName then
        debug.print workbook.fullname
        pb.finish
        exit function
      end if
    next
  end with
End Sub

Spec

  • Constructors
    • stdProgress::CreateFromPercents(Paramarray params())
    • stdProgress#CreateFromPercents(Paramarray params())
    • stdProgress::CreateFromCount(iPartsCount as long)
    • std{rpgress#CreateFromCount(iPartsCount as long) - split progress evenly amongst N parts
    • self - Returns Me
  • Instance methods
    • bindForm(Optional Byval oUF as object, ByVal sUpdateMethodName as string) - binds a form with an update method, if no form supplied just return self
    • bindFrame(Optional ByVal fr as frame) - binds a frame, the progress object will insert labels into the frame to visualise progress. If no fram supplied just return self
    • Step - steps forward by 1 step of progress. Default is 0.01 if created from percents, otherwise it's 1 from counts.
    • Finish - call if you need to urgently finish the progress accumulated.

Security issue

Trojan detected in \sancarn\stdVBA-examples\Examples\Spreadsheet Extractor\ExtractionTemplate 1.1.xlsm

Screenshot - 02-Jun-23 , 9_02_28 AM

stdAcc initAccTree()

It seems IE has some issues with creating the Accessibility tree of applications first time. In order to create the Accessibility tree we first need to access path 4.5.4.1.1:

Sub initAccTree()
  'When accessing the accessibility model of IE it only fully generates Acc after first read. This can cause
  'Other parts of the acc tree, which werent initially visible to be visible again. 
  'typically this can be rectified by accessing 4.5.4.1.1 
  On Error Resume Next
  Call me.FromPath("4.5.4.1.1")
End Sub

`stdLambda` Syntax Improvements

Tasks

  • Lambda syntax (a,b,c)=> ...
  • Let/Set syntax let a.b = ...
    • Have a let operator a.b := ... (Is this still required?)
    • Introduce a Let/Set keyword
  • Allow column syntax [Some column] and include row property
  • Add try/catch

Allow parameter names: stdLambda.Create("(a,b,c) => a*b+c")

Not only has this been heavily requested, it would improve ease of use and it isn't too tricky to implement.

Allow vbLet | vbSet: set a.b = something, let a.c = true

One of the downfalls of lambdas so far is the inability to trigger vbLet/vbSet There are 2 options here:

Have a let operator e.g. :=

a.b := something

This is probably the easiest to implement:

if consume() = eObjectGetter and peak(2) = eObjectSetter then
  beginLetExpression()
else
  beginGetExpression()
end if

Introduce a let an set keyword:

Not entirely sure how this would work internally but this would be the typical VBA approach.

set a.b = something
let a.c = 1

Allow column syntax [Some column] and include row property

Say you are looping through rows of a csv, it would be nice if we had a good way of accessing the columns of each row. The current easiest way to do this is with dictionaries:

stdLambda.Create("$1.item(""column 1"") + $1.col2").bind(row)

'or with change 1:
stdLambda.Create("(row)=>row.col1 + row.col2").bind(row)

This change proposes the addition of a row variable on stdLambda instances.

Dim lambda as stdLambda: set lambda = stdLambda.Create("[column 1] + [col2]")
for each row in csv
  set lambda.row = row
  if lambda() > 5 then
    '...
  end if
next

Add try-catch

Say we are using stdWindow to search for ProcessIDs e.g.

set eOut = wnds.Filter(stdLambda.Create("$2.ProcessID = $1").bind(proc.id))

stdWindow is built in such a way that if the window no longer exists ProcessID will throw an error. This can be worked around with if statements to catch the error:

set eOut = wnds.Filter(stdLambda.Create("if $2.exists then $2.ProcessID = $1 else false").bind(proc.id))

but it would be nice to have a try catch also:

set eOut = wnds.Filter(stdLambda.Create("Try $2.ProcessID = $1 Catch false").bind(proc.id))

Add short circuiting out of OR and AND

  • if a or b then should immediately skip to then if a is true, without needing to check b.
  • if a and b then ... else ... should immediately skip to else if a is false.

useStdEnumerator conditional compiling.

It might be a good idea to write all declares which return a collection as the following:

#if useStdEnumerator then
Public Function Filter(Byval filter as stdICallable) as stdEnumerator
#else
Public Function Filter(Byval filter as stdICallable) as Collection
#end if



  #if useStdEnumerator then
    set Filter = stdEnumerator.CreateFromIEnumVariant(ret)
  #else
    set Filter = ret
  #end if
End Function

In this way, all stdVBA modules can be quickly ported to using stdEnumerator with the flick of a compiler switch. Similarly stdError and Err could be used based on a compiler argument.

Note: i did have another idea for stdError specifically in #10

stdVBA build process

Objectives

Build a VBA Project containing

  • stdVBA.xlsm - stdVBA as it is
  • stdVBAc.xlsm - stdVBA where all returned Collection objects are converted to stdEnumerator instead, as well as stack traces, stdError usage etc.

Related issues:

`stdCOM` - `RegisterAsActiveObject()` and `OnTimeCall()`

Implementation of 2 methods ensures something that something which previously was not possible now is, although currently not optimally, more about that towards the end.

RegisterAsActiveObject

Description

Put an active object into the Running Object Table, and register it to a specified GUID (CLSID). From here other applications can use (and drive) your VBA application.

Example

The following example registers the myObj object as GUID e3cc5a52-6f89-41e3-9714-9502146a840a. This object can then be accessed from other processes via VBA's equivalent of GetObject("{e3cc5a52-6f89-41e3-9714-9502146a840a}").

Module Test
  Dim myObj as MyClass
  sub run()
    set myObj = new MyClass
    stdCOM.Create(myObj).RegisterAsActiveObject("e3cc5a52-6f89-41e3-9714-9502146a840a")
  end sub
end class

OnTimeCall(sMethodName,iMilliseconds)

Implementation Option 1 (Short term)

  1. Call RegisterAsActiveObject on a newly created GUID.
  2. Launch a binary/PowerShell script which connects to the object (via GUID) and continually calls a method every iMilliseconds milliseconds. If the object disconnects, close powershell runtime.

Implementation Option 2 (Long Term)

  1. Create a thunk which calls a method on an object pointer every iMilliseconds milliseconds.
  2. Pass pointer into thunk
  3. Resolve any crashes that occur, e.g. when object is un-allocated.

Example

The following example registers the myObj object as GUID e3cc5a52-6f89-41e3-9714-9502146a840a. This object can then be accessed from other processes via VBA's equivalent of GetObject("{e3cc5a52-6f89-41e3-9714-9502146a840a}").

class MyClass
  Public Function OnTick()
    static i as long: i=i+1 'increase every 100ms
  End Function
end class
Module Test
  Dim myObj as MyClass
  sub run()
    set myObj = new MyClass
    stdCOM.Create(myObj).OnTimeCall("OnTick", 100)
  end sub
end class

Potential issues

Unsure but may have difficulty calling methods in Break Mode.

Odd VBA bug littered throughout stdVBA which destroys error handling

This works as expected:

Public Property Get Test() As String
  On Error GoTo CannotQuery
    Call B
    Test = "Not ok"
    Exit Property
CannotQuery:
  Test = "OK"
End Property
Public Sub B()
  Err.Raise 1, "stdCOM", "IUnknown::QueryInterface - Interface not implemented."
End Sub

This does not:

Public Property Get Test() As String
  On Error GoTo CannotQuery
    Call Me.B
    Test = "Not ok"
    Exit Property
CannotQuery:
  Test = "OK"
End Property
Public Sub B()
  Err.Raise 1, "stdCOM", "IUnknown::QueryInterface - Interface not implemented."
End Sub

As shown in the following video:

EXCEL_feflyn5z3E.mp4

Additionally, Me. error handling fails in both these cases too:

Public Property Get Test() As String
  On Error GoTo CannotQuery
    Call Me.A
    Test = "Not ok"
    Exit Property
CannotQuery:
  Test = "OK"
End Property

Public Sub A()
  Call B
End Sub

Public Sub B()
  Err.Raise 1, "stdCOM", "IUnknown::QueryInterface - Interface not implemented."
End Sub

and

Public Property Get Test() As String
  On Error GoTo CannotQuery
    Call A
    Test = "Not ok"
    Exit Property
CannotQuery:
  Test = "OK"
End Property

Public Sub A()
  Call Me.B
End Sub

Public Sub B()
  Err.Raise 1, "stdCOM", "IUnknown::QueryInterface - Interface not implemented."
End Sub

Finally you can't get around this using With Me ... End With.

Ultimately using Me. anywhere in the call stack can cause major issues when it comes to error handling, and thus it must be purged.

You can get around this using CallByName(Me, propName, vbGet) but this seems a little overkill.

stdLambda - Passing callbacks

@ws-garcia
Another thing that is on my mind is the possibility of passing arguments of UDFs functions in terms of variables and other UDFs. Suppose we have a function defined as AVG (1st arg, 2nd arg,...,nth arg) and we need to pass an argument defined as Min(x, y, z), i.e. "AVG(5, 6, sin(30*x), Min(x, y, z))" is there any way to achieve this by passing a single string to the library without using the hardcoded 5, 6, x, y and z?

If you mean "can i pass a callback" - use the lambda() function.

stdLambda.Create("Avg(5,6,lambda(""sin(30*$1)""),lambda(""Min($1,$2,$3)""))")

If you mean just passing an argument into a function in another function, then of course. stdLambda arguments are global scope:

AVG(5, 6, sin(30*$1), Min($1, $2, $3))

stdPerformance - Timer accuracy

#If Mac Then
#If MAC_OFFICE_VERSION >= 15 Then
Private Declare Function GetTickCount Lib "/Applications/Microsoft Excel.app/Contents/Frameworks/MicrosoftOffice.framework/MicrosoftOffice" () As Long
#Else
Private Declare Function GetTickCount Lib "Applications:Microsoft Office 2011:Office:MicrosoftOffice.framework:MicrosoftOffice" () As Long
#End If
#Else ' Win32 or Win64
#If VBA7 Then
Private Declare PtrSafe Function GetTickCount Lib "kernel32" () As Long
#Else
Private Declare Function GetTickCount Lib "kernel32" () As Long
#End If
#End If

The GetTickCount Function is limited to the system timer accuracy, which is usually 10-15ms. An improvement of this accuracy could be achieved by using the API for the CPU-Performancecounter and -frequency.

Example:

#If Mac Then
#ElseIf VBA7 Then
   Private Declare PtrSafe Function QueryPerformanceFrequency Lib "kernel32" (cyFrequency As Currency) As Long
   Private Declare PtrSafe Function QueryPerformanceCounter Lib "kernel32" (cyTickCount As Currency) As Long
#ElseIf VBA6 Then
   Private Declare Function QueryPerformanceFrequency Lib "kernel32" (cyFrequency As Currency) As Long
   Private Declare Function QueryPerformanceCounter Lib "kernel32" (cyTickCount As Currency) As Long
#Else
#End If

Public Function CPUPerformanceCounterMicroSecs() As Variant
   Dim Frequency As Currency, TickCount As Currency
   If QueryPerformanceFrequency(Frequency) = 0 Or QueryPerformanceCounter(TickCount) = 0 Then Exit Function
   CPUPerformanceCounterMicroSecs = (CDec(TickCount) / CDec(Frequency)) * 1000000
End Function
Public Function CPUPerformanceCounterFrequency() As Variant
   Dim Frequency As Currency
   If QueryPerformanceFrequency(Frequency) = 0 Then Exit Function
   CPUPerformanceCounterFrequency = Frequency * 1000
End Function

Private Sub TestPerformanceCounter()
  
  Dim i As Long, exp As Long, total As Double, start As Double
  
  For exp = 4 To 7
    start = CPUPerformanceCounterMicroSecs
    For i = 1 To (10 ^ exp)
      total = total + i
    Next i
    Debug.Print (10 ^ exp) & " items - total: " & total & " - Runtime: " & (CPUPerformanceCounterMicroSecs - start) & " µs"
  Next exp
  
End Sub

Running TestPerformanceCounter will give a result similar to this:
10000 items - total: 50005000 - Runtime: 256,7 µs
100000 items - total: 5050055000 - Runtime: 1506,1 µs
1000000 items - total: 505050555000 - Runtime: 14867,8 µs
10000000 items - total: 50505055555000 - Runtime: 148550,2 µs

Edit: the variables for the QueryPerformanceCounter are passed in as currency, as the API-function requires a 64-bit Integer (LongLong) pointer, which is not available in VBA6. VBA6 supports the Currency Datatype, which is a 64-bit Integer with fixed comma. From VBA7 on, the LongLong Type could be used instead.

`stdEnumerator` - Better `CreateFromIEnumVariant()`

Currently CreateFromIEnumVariant() enumerates the entire collection and converts it into an array at Create-Time. This is awful.

In reality we should call the IEnumVARIANT's Next method to obtain the next element in the collection as and when it is needed. This would allow things such as first(100) to be used effectively without lagging out.

Unfortunately this requires behaviour only made available recently in stdCOM so expect we'll have to port those over as required.

Application.Run() Alternative

I noticed in the stdCallback code there are comments indicating that an alternative to Application.Run() might exist. Is this possible? Not all VBA host applications provide an Application.Run() method, and it can be very limiting to what you can do and which libraries/code you can use.

stdUI

If only we could make whole forms, with all their events UI components which we can use to compose bigger controls and dashboards. Or maybe we can!

  • stdUIComponent implementation
  • stdUICanvas implementation ontop of stdUIComponent
  • stdUI.notify()

`stdTimer` - Cache remote workbook

Currently our code uses 1 workbook per timer, which is crazy slow. This is currently declared as "not really an issue" because requiring more than 1 timer is likely rare(?).

However if a work around was desired we could instead:

  1. Cache the workbook
  2. Use the AddTimer() method of below:
Private Type Timer
  current as long
  frequency as long
  id as string
End Type
Private Timers() as Timer
Private Const MAX_TIME as Long = 1000000
Private Const MIN_TIME as Long = 10

Sub AddTimer(ByVal iMilliseconds as long, ByVal id as string)
  if iMilliseconds > MAX_TIME then iMilliseconds = MAX_TIME
  if iMilliseconds < MIN_TIME then iMilliseconds = MIN_TIME
  On Error Resume Next: iNext = ubound(Timers)+1: On Error GoTo 0
  Redim Timers(0 to iNext)
  With Timers(iNext)
    Timers.current = 0
    Timers.frequency = iMilliseconds
    Timers.id= id
  End With
End Sub

Sub MainLoop()
  set r = Sheet1.Cells(1,1)
  While True
    For each timer in Timers
      timer.current = timer.current + 1
      if timer.current mod timer.frequency = 0 then 
        r.Value = id
        timer.current = 0 'saves having to deal with overflows
      end if
    next
    Doevents
    Sleep 1
  Wend
End Sub
  1. OnChange use the id = Target.Value and lookup id in a dictionary to find callback. Also pass id to Tick event.

This can handle multiple timers each added at different times. Unfortunately as new timers are added the whole class slows down, but perhaps this is ok.

stdError re-write

In-class implementation

Currently we have mostly been using the following declarations in classes:

Private Sub CriticalRaise(...)
  if vartype(stdError) <> "Empty" then
    stdError.Raise ...
  else
    Err.Raise 1, ...
  end if
End Sub

This feels like a dirty hack to be honest... And I'd rather this not be part of every module.

A better option would be the ability to bind an error object to every class. protErrorObject to be bindable to instances, instances get their default object from the predeclared id.

'Standard ErrorObject boiler plate
Public protErrorObject as Object
Public Sub Init()
  set protErrorObject = MyClass.protErrorObject 
End Sub
Public Sub Class_Initialize()
  set protErrorObject = Err
End Sub





Public Sub Test()
  If SomeCondition then
    '...
  else
    protErrorObject.Raise 1, ...
  end if
end sub

And in stdError:

Public Sub BindAll()
  Dim classes: classes = Array( _ 
    stdArray, _ 
    stdLambda, _ 
    stdCallback, _ 
    stdWindow _ 
  )
  dim i as long
  for i = 0 to ubound(classes)
    if isObject(classes(i)) then
      set classes(i).protErrorObject = stdError
    end if
  next
End Sub

The hope is that this makes stdError always optional, AND people can make their own implementations of stdError and substitute it, if wanted

Ability to catch errors stdError raise events.

  • IsReporting = true/false
Dim errors as Collection
with stdError.ResumeNext(errors)
   stdError.Raise "some error" 'resuming...
   stdError.Raise "another error" 'resuming...
   Debug.print "hello"   'prints
end with
stdError.Raise "some error"    'Exit here
Debug.print "hello"   'doesn't reach here

Ideally we need a stack of stacks also so we can do something along the lines of:

Dim errors1 as Collection
with stdError.ResumeNext(errors1)
   '...
   Dim errors2 as collection
   with stdError.ResumeNext(errors2) 'errors 2 now gets all errors
      '...
      stdError.Raise "some error here" 'resuming - logged to errors2
   end with
   
   'error handling returns to major handler
   stdError.Raise "some error" 'resuming - logged to errors1
   stdError.Raise "another error" 'resuming - logged to errors1
   Debug.print "hello"   'prints
end with
stdError.Raise "some error"    'Exit here
Debug.print "hello"   'doesn't get here

`stdCallback` refactor

It's time for stdCallback to get a refactor. I feel some aspects are overcomplicated and this is in part because I didn't really know what I was doing when I originally made it.

Structure:

Private Enum CallbackType
  EApplicationRun
  ECallByName
  EDispCallFunc
End Enum
Private Type TThis
  iType as CallbackType
  args() as variant
  vBoundArgs as variant
End Type
Private This as TThis


'Core constructors
Public Function CreateFromRunCommand(ByVal sRunCommand as string) as stdCallback
  set CreateFromRunCommand = new stdCallback
  CreateFromRunCommand.protInit(CallbackType.EApplicationRun, Array(sRunCommand))
End Function
Public Function CreateFromObjectMember(Byval o as object, ByVal sMemberName as string, ByVal cType as vbCallType) as stdCallback
  set CreateFromObjectMember= new stdCallback
  CreateFromObjectMember.protInit(CallbackType.ECallByName, Array(o, sMemberName, cType))
End Function
Public Function CreateFromObjectOffset(ByVal oPtr as LongPtr, ByVal iVTableOffset as LongPtr, ByVal iRetType as vbVarType, ByRef vParamTypes() as vbVarType)
  set CreateFromObjectOffset = new stdCallback
  CreateFromObjectOffset.protInit(CallbackType.EDispCallFunc, Array(oPtr, iVTableOffset, iRetType, vParamTypes)) 
End Function
Public Function Clone() as stdCallback
  set Clone = new stdCallback
  Clone.protInit(This.iType, This.args)
End Function
Public Function Bind(ParamArray vParams()) as stdCallback
  set Bind = Clone()
  Bind.protBindParams(vParams)
End Function


'More specific constructors
Public Function CreateFromModule(sModuleName as string, sMethodName as string)
  set CreateFromModule = CreateFromRunCommand(sModuleName & "." & sMethodName)
End Function
Public Function CreateFromWorkbookModule(sWorkbookPath as string, sModuleName as string, sMethodName as string)
  set CreateFromWorkbookModule = CreateFromRunCommand("'" & sWorkbookPath & "'!" & sModuleName & "." & sMethodName)
End Function
Public Function CreateFromObjectMethod(ByVal o as object, ByVal sMethodName as string)
  set CreateFromObjectMethod = CreateFromObjectMember(o, sMethodName, vbMethod
End Function
Public Function CreateFromObjectProperty(ByVal o as object, ByVal sPropertyName as string, ByVal cType as vbCallType)
  set CreateFromObjectProperty = CreateFromObjectMember(o, sPropertyName, cType)
End Function
Public Function CreateFromFunctionPointer(ByVal iPtr as LongPtr, ByVal iRetType as vbVarType, ByRef vParamTypes() as vbVarType)
  set CreateFromFunctionPointer = CreateFromObjectOffset(NULL_PTR, iPtr, iRetType, vParamTypes)
End Function



'@protected
Friend Sub protInit(ByVal iType as Long, vArgs() as variant)
  This.iType = iType
  This.args = vArgs
  This.vBoundArgs  = Null
End Sub

How to install - Compile errors

I added all the files under /src to test out the functionality, but unfortunately when pressing "compile VBAproject" (or trying to run code) I get various compile errors about user defined variables (like IUnknown) in the library files.
I opened the fullbuild.xlsm and there the files can compile without the same problem.
Should I extract the classes out of fullbuild.xlsm and import those, or what is the way to "install" these classes to use in other projects, of copying the files under /src doesn't work?

I am using excel2013 32-bit

stdCallback.CreateEvaluator()

Evaluators

Evaluators are something we have wanted for a long time, and this project shouldn't be taken lightly. In an ideal world we would build an AST for each formulae, and evaluate the AST using some data parameter. That said currently we use string manipulation and Application.Evaluate()

Solutions

1. Use Application.Evaluate()

This is the simplest version of the evaluator and our current solution. It offers a quick and dirty approach to creating a parser.

Unfortunately it does have some issues:

  • No support for objects (unless you add serialisation of all objects which is nigh on impossible in vba)
  • No access to property accessors or method execution.
  • Cannot extend the language easily (e.g. custom syntax is difficult)
  • Application.Evaluate has a 255 character limit. Application.Evaluate("=""" & Space(256) & """") won't evaluate and instead will return Error 2015, even though Excel can happily evaluate formulas this length in cells...

2. Install dependency for stdSettings. Use range from setting to evaluate formula >255 characters long.

Get a dependency on stdSettings (or request a range). Formula longer than 255 characters can be evaluated as follows:

if len(sFormula)>255 then
  with stdSettings.Create().system("Evaluator")
    .formula = sFormula
    .calculate
    result = .value2
  end with
else
  result = Application.Evaluate(sFormula)
end if

Bs & Cs

  • No more 255 char limit with Application.Evaluate().
  • If permanent switch then we can also use UDFs!
  • Very Slow
  • Increase dependency tree for stdCallback and thus all other dependencies too.

3. Create a naive interpreter

A naive interpreter is one that is built on Recursive Descent Algorithm functioning on a token list. Something of the following nature:

tokenTypes = Array(
  Array("Literal", """(""""|[^""])*?"),
  Array("Literal", "d+\.\d+"),
  Array("LBracket","\("),
  Array("RBracket","\)"),
  Array("FuncName","[a-zA-Z][a-zA-Z_0-9]+"),
  Array("FuncParam",",")
  Array("PropAcc","."),
  Array("VarAcc","\$\d+"),
  Array("Op1","\*\/"),
  Array("Op2","\+\-"),
  Array("Op3","\=")
)

tokens = tokenise()

Then we loop over the tokens, and "Recompile" the token array as we go. E.G. for

=AND($1=1,$2="Hello")

thus tokens:

[["FuncName","AND"],["LBracket","("],["VarAcc","$1"],["Op3","="],["Literal","1"],["FuncParam",","],["VarAcc","$2"],["Op3","="],["Literal","""Hello"""],["RBracket",")"]

then:

While do
  Find func name
  Segment parens for func name
  Segment expressions for func name
  Evaluate expressions (recurse)
End
While do
  Find Op1s
  Evaluate Op1s
End
While do
  Find Op2s
  Evaluate Op2s
End
While do
  Find Op3s
  Evaluate Op3s
End

Bs & Cs

  • Faster to run
  • Faster to build than compiler
  • Complex/Slow development
  • Potentially not easy to extend

4. Build a Tokeniser, Parser (to CST) and Evaluator

This is a lot more ambitious. The idea is to build a Tokeniser --> Parser. We'll run this at evaluator creation time, and at evaluation time we'll only run the evaluator which evaluates the CST with the inserted context.

And($1=1,$2="Hello")

will result in

{
  "type":"Func",
  "params": [
    {
      type:"Operator", 
      value: "=", 
      params: [
        {"type":"In", value:1},
        {"type":"Literal",value:1}
      ]
    },{
      type:"Operator",
      value:"=",
      params:[
        {"type":"In", value:2},
        {"type":"Literal",value:"Hello"}
      ]
  ]
}

This AST can be very quickly evaluated when the evaluation step comes around.

Bs & Cs

  • Very fast
  • Easier to extend (because we use a grammar)
  • Complex and very slow to develop

5. Compile to machine code

This becomes the ultimate form of Evaluator. If we could compile down to machine code for evaluation this would be very ideal. AHK has a similar system called [MCode](https://autohotkey.com/board/topic/89283-mcode-function-onlinegenerator-x86-and-x64/. This would be the late game of the evaluator. It might be architecture specific however and literally involves implementing an assembler...

Some example assemblers which may or may not be useful:

Bs & Cs

  • Fastest option
  • Potentially easy to extend? Unsure on this one.
  • Likely ludicrously complex
  • Might be platform dependent?

6. Don't do this in runtime?

In the short to long term I do want to build a VBA-VBA compiler which can allow for the addition of new rules into the VBA grammar which can be evaluated and compiled down to plane VBA code. This will be undertaken in a seperate project (see VBA-Chevrotain)

One such idea is to add a new syntax to vba ()=>{} such that we can inject evaluators into code. E.G:

Sub Main()
  stdArray.Create(1,2,3).map((int)=>int*2)
End Sub

which will compile to:

Sub Main
  stdArray.Create(1,2,3).map(stdCallback.CreateFromObject(Me,"fcb00001"))
End Sub
Public Function fcb00001(ByVal int)
  fcb00001 = int*2
End Function

This is a parallel project to the VBA STD Library. That said a runtime will always be more convenient to those who can't install their own developer environment, myself included.

Critical performance concerns

It is critical that these formulae be as performant as possible, especially since they will typically be executed over large volumes of data. Therefore there are a few watchouts which we should ensure:

  1. No object usage - Avoid use of objects where possible as property and method access are quite expensive. Use types instead.
  2. Especially no dynamic objects like Dictionary. Use types instead.
  3. Do not recreate arrays of tokens. If possible create an array and loop through it index wise.
  4. Parsing can be a nightmare for backtracking - try to avoid.
  5. Caching is vital. Do as much processing as you can at object creation, leaving as little as possible for evaluation time.

Fix `stdProcess` structure issue with ProcessEntry32

See this issue for quotation:

twinbasic/twinbasic#1031 (comment)

Ultimately pcPriClassBase should be a Single not a Long type

Private Type PROCESSENTRY32
    dwSize As Long
    cntUsage As Long
    th32ProcessID As Long
    'pack1 As Long
    th32DefaultHeapID As LongPtr
    th32ModuleID As Long
    cntThreads As Long
    th32ParentProcessID As Long
    pcPriClassBase As Single         'Bugfix
    dwFlags As Long
    szExeFile As String * 260
    'pack2 As Long
End Type

stdCallback constructor improvements.

Would be nice to include the following constructors:

  • stdCallback::CreateFromForeignFunction(wb as workbook, moduleName as string, methodName as string)
  • stdCallback::CreateFromDLLFunction(dllName as string, functionName as string, types...)

stdProcess

stdProcess

Used to create processes and

Constructors

  • CreateProcess( sCmd as string, winStyle As VbAppWinStyle = VbAppWinStyle.vbHide)
  • CreateProcess(sCmd as string, winstyle?, startupInfo?, creationInfo?) - see #18
  • CreateProcessAsUser() - expect usage to be low
  • CreateProcessWithLogon() - expect usage to be low
  • CreateProcessWithToken() - expect usage to be low
  • CreateFromProcessId(pID) as stdProcess
  • CreateFromQuery(callable) as stdProcess
  • CreateManyFromQuery(callable) as Collection<stdProcess>
  • CreateAll()

Instance Properties

  • R id - Process ID
  • R ModuleID
  • R sid - expect usage to be low
  • R threadid? - expect usage to be low
  • R name
  • R path
  • R Winmgmt
  • R CommandLine
  • R isRunning
  • R isCritical
  • R Priority
  • W Priority
  • R TimeCreated
  • R TimeQuit
  • R TimeKernel
  • R TimeUser
  • R ExitCode
  • W STDIN
  • R STDOUT
  • R STDERR

Instance Methods

  • forceQuit()
  • waitTilClose()
  • stdInPush()
  • stdInEnd()

Protected additions

  • protProcessHandleCreate(access) - Open the process with specific rights
  • protProcessHandleRelease() - Close the open process handle.
  • protProcessHandle - Get the process handle with the rights created from protProcessHandleCreate

Rewrite stdWebSocket using WebSocket API provided by Websocket.dll

WebSocketCreateClientHandle
WebSocketBeginClientHandshake
WebSocketEndClientHandshake
WebSocketCreateServerHandle
WebSocketBeginServerHandshake
WebSocketEndServerHandshake
WebSocketSend
WebSocketReceive
WebSocketGetAction
WebSocketCompleteAction
WebSocketAbortHandle
WebSocketDeleteHandle
WebSocketGetGlobalProperty

Mac OS X support

Mac OS X support

Currently the majority of the classes in this repository will work on Windows OS only. In reality office for Mac also exists, and thus e.g. Regular expressions will have to be implemented differently to use AppleScript or JSX instead of COM.

Some libraries are also basically meaningless, e.g. the COM library is utterly meaningless on Mac OS X. It'd be advisable to have a Signal API for Mac OS X though.

In this post we should discuss:

  • Which of the classes work and which classes need porting.
  • General design choices for code porting.
  • This issue should link to other issues where a specific class shall be ported, and specific design choices can be identified.

This post should not discuss:

  • Specifics of porting/creating a specific class - These should be in an own issue.

Classes that need porting

Below is a list of classes which are ready to port to Mac OS X. I'll try to identify more as and when they arise:

Class Brief description of issues Status
stdCallback Usage of Win32 DLLs 0%
stdArray Usage of Win32 DLLs 0%
stdLambda Usage of Win32 DLLs 0%
stdEnumerator Usage of Win32 DLLs 0%
stdRegex Usage of Win32 DLLs 0%
stdWindow Usage of Win32 DLLs 0%
stdAcc Usage of Win32 DLLs 0%

StringBuilder performance tweaks

This is a great project! These classes save a lot of work.

Maybe my tweaking of StringBuilder is of interest to someone.

The StringBuilder class relies on string concatenation (repeated &), which results in a lot of string creation and copying. This can be overcome by using an exponentially growing buffer. Since I needed such a StringBuilder, I created one (https://github.com/sihlfall/vba-stringbuilder/).

I have been able to apply the ideas used there to stdVBA's StringBuilder class. In addition, I have modified StringBuilder to create the variable substitution dictionary lazily (so it is only created when it is actually used).

The modified class can be found here:

https://github.com/sihlfall/vba-stringbuilder/blob/master/comparison/stdvba-ssb/stdStringBuilderSsb.cls

It should, after renaming, be a drop-in replacement for the existing class.

Performance improvements are considerable. For instance, consider appending n single-character strings to an initial string of length 20,000:

Initial string size: 20,000
   n             stdStringBuilder     stdStringBuilderSsb (= modified version)
 100                 0.86ms                     0.01ms
 200                 1.41ms                     0.02ms
 400                 2.52ms                     0.03ms
 800                 5.84ms                     0.06ms
1600                10.82ms                     0.12ms

For a larger initial string:

Initial string size: 200,000
   n             stdStringBuilder     stdStringBuilderSsb (= modified version)
 100                13.72ms                     0.20ms
 200                26.84ms                     0.21ms
 400                51.00ms                     0.22ms
 800               104.26ms                     0.25ms
1600               206.13ms                     0.30ms

stdProcess - massive slow downs due to deprecated code

stdProcess.CreateAll() method is incredibly slow. The root cause of the slow down comes from getModuleIDByPid function, which always returns 0 anyway... Comment out the line in protInitFromProcessId:

Friend Sub protInitFromProcessId(ByVal argID As Long, Optional ByVal argName As String = "", Optional ByVal argPath As String = "", Optional ByVal argModuleID As Long = 0)
  pProcessId = argID
  pName = argName
  pPath = argPath
  'If argModuleID = 0 Then pModuleID = getModuleIDByPid(argID)   '<------ commented
  If Len(argName) = 0 Then pName = getProcessNameById(argID)
  If Len(argPath) = 0 Then pPath = getProcessImageName(argID)
  pQueryInfoHandle = OpenProcess(PROCESS_QUERY_INFORMATION, False, argID)
End Sub

stdEnumerator - Other constructors

implement

  • CreateFromICallable 'Only half implemented
  • CreateFromArray

To manage IEnumVARIANT use this approach

Public Function ForEach(Optional ByVal cb As stdICallable, Optional ByVal WithIndex as boolean = false) As stdEnumerator
    Dim bLoopInitialised as boolean: bLoopInitialised = false
    Dim ExitLoop as Boolean: ExitLoop = false
    Dim v as variant, i as long: i=0
    Do while true
        'Increment index
        i=i+1

        'Obtain loop var from mode
        select case mode
            case EnumeratorType.FromCallable
                if bLoopInitialised then
                    v = pCallback.run(v,i)
                else
                    v = pCallback.Run(null,i)
                    bLoopInitialised=true
                end if
                if isNull(v) then ExitLoop = true
            case EnumeratorType.FromIEnumVariant
                if bLoopInitialised then
                    GoSub NextItem
                else
                    GoSub InitIEnumVARIANT
                    bLoopInitialised = True
                end if
            case EnumeratorType.FromIEnumVariant
                if bLoopInitialised then
                    if lb+i-1 <= ub then
                        v = vArr(lb+i-1)
                    else
                        ExitLoop = true
                    end if
                else
                    Dim lb as long: lb = lbound(vArr,1)
                    Dim ub as long: ub = ubound(vArr,1)
                    if ub-lb+1 > 0 then
                        v = vArr(lb+i-1)
                    else
                        v = null
                    end if
                end if
        end select
        if ExitLoop then Exit Do

        if withIndex then
            Call cb.Run(i,v)
        else
            Call cb.Run(v)
        end if
    Loop

    set ForEach = me
    Exit Function

'=========================================================================================================================
' IEnumVARIANT Iterator - Don't change the code below
'=========================================================================================================================
InitIEnumVARIANT:
    For Each v In pEnumObject
        Return
NextItem:
    Next
    ExitLoop = true
    Return
End Function

FullBuild.xlam disaligned with srcWindow.cls

In FullBuild.xlam I get an error on the line "RectClient = rect" because RectClient is read-only (true, since it has no letter proc.)

Public Property Let RectByType(Optional iClWnd As EWndRectType, ByVal rect As Variant)
  Select Case iClWnd
    Case EWndRectType.RectTypeClient
      RectClient = rect
    Case EWndRectType.RectTypeWindow
      RectWindow = rect
  End Select
End Property

However, src\stdWindow.cls has "RectWindow = rect" in its place.

I'm new to your repository. Should I always take the src files as the latest version?

`stdLambda` performance re-visited

stdLambda Performance

It has come to my attention that there exists another library a lot like stdLambda, named clsMathParser. This library apparently existed since 2004(?!), so totally unsure how this skipped passed my radar! It's not a very intuitive library but essentially functions as follows:

With new clsMathParser
  .storeExpression("x+1")
  For i = 1 to 100
    variable("x") = i
    Debug.Print .eval()
  next
End With

This library is, according to my testing, 5x vaster than stdLambda. Test set:

Sub test()
  Dim math As New clsMathParser
  Dim lambda As stdLambda: Set lambda = stdLambda.Create("$1^2+$1+2")
  
  If math.StoreExpression("x^2+x+2") Then
    With stdPerformance.Measure("clsMathParser", 10000)
      Dim i As Double, l As Double
      For i = 1 To 1000000
        math.variable("x") = i / 1000
        l = math.Eval()
      Next
    End With
    
    With stdPerformance.Measure("stdLambda", 10000)
      For i = 1 To 1000000
        l = lambda.Run(i / 1000)
      Next
    End With
  End If
End Sub

Comparrison of algorithm

  • clsMathParser's stack is pre-populated with values paired with operators. This ultimately means less operations done in VBA side:
    • clsMathParser splits x^2+x+2 into 3 operations:
      • ET(0) = completion op
      • ET(1) = ^ prevArg + varX^2 (???)
      • ET(2) = + prevArg + varX
      • ET(3) = + 0+2
    • stdLambda splits$1^2+$1+2 into 7 operations
      • ops(0) = Arg 1
      • ops(1) = Push 2
      • ops(2) = Op Pow
      • ops(3) = Arg 1
      • ops(4) = Op Add
      • ops(5) = Push 2
      • ops(6) = Op Add
      • ops(7) = Completion
  • stdLambda's starts with an empty stack and alocates it with values at runtime. Though we're only copying a few bytes I guess this consumes a lot of time compared to having all that data pre-prepared. A pre-populated stack is something we should likely look into.
  • stdLambda used to be copying argument location from a string at runtime, which is slow! I changed this and it saves some 30µs per operation. Fixed in 2a9b2e0
  • clsMathParser isn't stand alone sadly, and doesn't provide object access. stdLambda does provide object access and is standalone by comparrison (although technically requires stdICallable - easily removed by comparrison)
  • Cleverly clsMathParser uses a ByRef param for the return type. That shaves off some time, although not significant in my tests. But still clever fosho :)

stdWindow - shell windows

Would be nice to be able to obtain the shell windows representing a window

Obtainable by:

Property Get shellWindows() as Collection
  set shellWindows = new Collection
  Dim shell as object: set shell = CreateObject("Shell.Application")
  Dim shWin as object
  For each shWin in shell.windows
    if shWin = handle then
      call shellWindows.add(shWin)
    end if
  next
end property

`stdLambda` `stack` issue

Currently in evaluate:

            Case iType.oAccess
                Select Case op.subType
                    Case ISubType.argument
                        Dim iArgIndex As Long: iArgIndex = val(mid(op.value, 2)) + LBound(vLastArgs) - 1
                        If iArgIndex <= UBound(vLastArgs) Then
                            Call pushV(stack, stackPtr, vLastArgs(iArgIndex))
                        Else
                            Call Throw("Argument " & iArgIndex & " not supplied to Lambda.")
                        End If
                    Case Else
                        Call pushV(stack, stackPtr, stack(stackPtr - op.value))

Call pushV(stack, stackPtr, stack(stackPtr - op.value)) can sometimes error when the variable at stack(stackPtr - op.value) is an object. See explanation for details. As a temporary work around we can do the following:

 '@FIX bug where if stack(stackPtr-op.value) was an object, then array will become locked. Array locking occurs by compiler to try to protect
'instances when re-allocation would move the array, and thus corrupt the pointers. By copying the variant we redivert the compiler's efforts,
'but we might actually open ourselves to errors...
Dim vAccessVar: Call CopyVariant(vAccessVar, stack(stackPtr - op.value))
Call pushV(stack, stackPtr, vAccessVar)

This may still break so we need to consider a better solution. Our suggestion is to use a Collection (i.e. linked list) instead of a dynamic array. This should have fewer issues but might sacrifice on some performance.


Explanation:

Minimal examples where error occurs:

Sub Example()
    'Initialize the array with 3 elements
    Dim myArray() As string
    ReDim myArray(0)
    Set myArray(0) = "yop"
    Call Push(myArray, myArray(0))
End Sub

Sub Push(ByRef myArray() As string, ByRef element As string)
    'Lock the array to prevent deallocation of memory for the reference parameter
    ReDim Preserve myArray(UBound(myArray) + 1)
    If IsObject(element) Then
      Set myArray(UBound(myArray)) = element
    Else
      myArray(UBound(myArray)) = element
    End If
End Sub

The issue is that element, in this case is a reference to an element within myArray... The solution to above is change element to ByVal:

Sub Push(ByRef myArray() As string, ByVal element As string)

When extended to the variant case however:

Sub Push(ByRef myArray() As variant, ByVal element As variant)

Objects are also pointers... so

Sub Example()
    'Initialize the array with 3 elements
    Dim myArray() As Variant
    ReDim myArray(0)
    Set myArray(0) = CreateObject("scripting.dictionary")
    Call Push(myArray, myArray(0))
End Sub

Sub Push(ByRef myArray() As Variant, ByVal element As Variant)
    'Lock the array to prevent deallocation of memory for the reference parameter
    ReDim Preserve myArray(UBound(myArray) + 1)
    If IsObject(element) Then
      Set myArray(UBound(myArray)) = element
    Else
      myArray(UBound(myArray)) = element
    End If
End Sub

Also errors because we have a pointer directly to myArray(0) object. So the real work around is

Sub Example()
    'Initialize the array with 3 elements
    Dim myArray() As Variant
    ReDim myArray(0)
    Set myArray(0) = CreateObject("scripting.dictionary")
    Dim v: CopyVariant(v, myArray(0))
    Call Push(myArray, v)
End Sub

Parsing arithmetic operators in StdLambda

Hello, I probably found an issue in how StdLambda parses expressions involving * and / operators.
Consider the following:
Set lambda = stdLambda.Create("$1 * $2 / $3 * $4")

If I evaluate lambda(1,2,2,2), the result is 0.5 instead of 2.

Thank you

stdProcess - Use CreateProcess() instead of Shell()

Currently we are using Shell() for simplicity. We have built some (bodge) 64-bit code as follows:

Private Type SECURITY_ATTRIBUTES
    nLength              As Long
    lpSecurityDescriptor As LongPtr
    bInheritHandle       As Long
End Type

Private Type STARTUPINFO
    cb              As Long
    lpReserved      As String
    lpDesktop       As String
    lpTitle         As String
    dwX             As Long
    dwY             As Long
    dwXSize         As Long
    dwYSize         As Long
    dwXCountChars   As Long
    dwYCountChars   As Long
    dwFillAttribute As Long
    dwFlags         As Long
    wShowWindow     As Integer
    cbReserved2     As Integer
    lpReserved2     As LongPtr
    hStdInput       As LongPtr
    hStdOutput      As LongPtr
    hStdError       As LongPtr
End Type

Private Type PROCESS_INFORMATION
    hProcess    As LongPtr
    hThread     As LongPtr
    dwProcessId As Long
    dwThreadId  As Long
End Type

Public Enum ECreationFlags
    CREATE_BREAKAWAY_FROM_JOB = &H1000000
    CREATE_DEFAULT_ERROR_MODE = &H4000000
    CREATE_NEW_CONSOLE = &H10&
    CREATE_NEW_PROCESS_GROUP = &H200&
    CREATE_NO_WINDOW = &H8000000
    CREATE_PROTECTED_PROCESS = &H40000
    CREATE_PRESERVE_CODE_AUTHZ_LEVEL = &H2000000
    CREATE_SECURE_PROCESS = &H400000
    CREATE_SEPARATE_WOW_VDM = &H800&
    CREATE_SHARED_WOW_VDM = &H1000&
    CREATE_SUSPENDED = &H4&
    CREATE_UNICODE_ENVIRONMENT = &H400&
    DEBUG_ONLY_THIS_PROCESS = &H2&
    DEBUG_PROCESS = &H1&
    DETACHED_PROCESS = &H8&
    EXTENDED_tStartupInfo_PRESENT = &H80000
    INHERIT_PARENT_AFFINITY = &H10000
    PRIORITY_CLASS_NORMAL = &H20&
    PRIORITY_CLASS_IDLE = &H40&
    PRIORITY_CLASS_HIGH = &H80&
End Enum

Public Enum EStartupFlags
    STARTF_FORCEONFEEDBACK = &H40&
    STARTF_FORCEOFFFEEDBACK = &H80&
    STARTF_PREVENTPINNING = &H2000&
    STARTF_RUNFULLSCREEN = &H20&
    STARTF_TITLEISAPPID = &H1000&
    STARTF_TITLEISLINKNAME = &H800&
    STARTF_UNTRUSTEDSOURCE = &H8000&
    STARTF_USECOUNTCHARS = &H8&
    STARTF_USEFILLATTRIBUTE = &H10&
    STARTF_USEHOTKEY = &H200&
    STARTF_USEPOSITION = &H4&
    STARTF_USESHOWWINDOW = &H1&
    STARTF_USESIZE = &H2&
    STARTF_USESTDHANDLES = &H100&
End Enum

Private Declare PtrSafe Function CreateProcess Lib "kernel32" Alias "CreateProcessA" ( _
       ByVal lpApplicationName As String, _
       ByVal lpCommandLine As String, _
       ByRef lpProcessAttributes As SECURITY_ATTRIBUTES, _
       ByRef lpThreadAttributes As SECURITY_ATTRIBUTES, _
       ByVal bInheritHandles As Long, _
       ByVal dwCreationFlags As Long, _
       ByRef lpEnvironment As Any, _
       ByVal lpCurrentDirectory As String, _
       ByRef lpStartupInfo As STARTUPINFO, _
       ByRef lpProcessInformation As PROCESS_INFORMATION) As Long

Private Declare PtrSafe Function SuspendThread Lib "kernel32" (ByVal hThread As LongPtr) As Long
Private Declare PtrSafe Function ResumeThread Lib "kernel32" (ByVal hThread As LongPtr) As Long
Private Declare PtrSafe Function CloseHandle Lib "kernel32.dll" (ByVal hObject As LongPtr) As Long
Private Declare PtrSafe Function DebugActiveProcess Lib "kernel32" (ByVal dwProcessId As Long) As Long
Private Declare PtrSafe Function DebugActiveProcessStop Lib "kernel32" (ByVal dwProcessId As Long) As Long
Private Declare PtrSafe Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)


Public Function WinApi_CreateProcess(strCommandLine As String, Optional strCurrentDirectory As String = vbNullString) As Long
    If strCurrentDirectory = vbNullString Then
        strCurrentDirectory = ThisWorkbook.Path
    End If
    Dim sap As SECURITY_ATTRIBUTES: sap.nLength = Len(sap)
    Dim sat As SECURITY_ATTRIBUTES: sat.nLength = Len(sat)
    Dim si As STARTUPINFO: si.cb = Len(si)
    si.wShowWindow = STARTF_USESHOWWINDOW
    Dim pi As PROCESS_INFORMATION
    Debug.Print Err.LastDllError ' 0 => ERROR_SUCCESS
    Dim dwResult As Long: dwResult = CreateProcess(vbNullString, strCommandLine, sap, sat, 0, PRIORITY_CLASS_NORMAL, 0, strCurrentDirectory, si, pi)
    WinApi_CreateProcess = pi.dwProcessId
End Function

which appears to be able to create cmd.exe processes correctly, however any application with UI like mspaint.exe, explorer.exe and calc.exe are all unopenable using this function...

Need to figure out what we are missing and integrate this into stdProcess

Edit: Actually, notepad.exe works fine! Still not sure about the others...

stdCOM

stdCOM

A class for creating and manipulating objects, and extracting/controlling properties of them.

Constructors

  • Create(ByRef obj as IUnknown) as stdCOM
  • CreateFromPtr(ByVal ptr as LongPtr) as stdCOM
  • CreateFromActiveObjects() as Collection - See #34
  • CreateFromLResult(Lresult, sIID or iKnownID, wParam as long) as stdCOM(?)

Properties

  • Get Object as Object
  • Get Pointer() as LongPtr/Long
  • Get TypeInfo() as stdCOM - inherited from IDispatch
  • Get InterfaceInfo() as Collection<Variant()<string, vbCallType, long, long>> - uses TypeInfo
  • Get Methods() as Collection - uses InterfaceInfo()
  • Get Properties as Collection - uses InterfaceInfo()

Methods

  • unkQueryInterface(sIID) as stdCOM - Inherited from IUnknown
  • unkAddRef() - Inherited from IUnknown
  • unkRelease() - Inherited from IUnknown
  • QueryKnownInterface(interface as EKnownInterfaces) as stdCOM - uses unkQueryInterface
  • CallVT(iOffset as long, ReturnType as VbVarType, params()) as variant
  • RegisterAsActiveObject() - See #50
  • OnTimeCall() - See #50

stdWindow

stdWindow

A class for creating and obtaining window objects, and extracting/controlling properties of them.

Constructors

  • Create(sClassName,sCaption,dwStyle, x, y, Width, Height, hWndParent, hMenu, hInstance, lpParam) as stdWindow
  • CreateHighlightRect(x, y, Width, Height, BorderWidth, BorderColor) as stdWindow
  • CreateFromDesktop() as stdWindow
  • CreateFromHwnd(hwnd) as stdWindow
  • CreateFromPoint(x, y) as stdWindow
  • CreateFromEvent() as stdWindow
  • CreateFromIUnknown(obj) as stdWindow
  • CreateFromContextMenu() as stdWindow 'Class == "#32768"
  • CreateFromShellWindows() as Collection i.e. shell.application.windows. See #22

Properties

  • Get handle() as LongPtr
  • Get hDC() as LongPtr
  • Get Exists as Boolean
  • Get/Let Visible() as Boolean
  • Get/Let State() as EWndState 'Normal,Minimised,Maximised
  • Get IsFrozen() as Boolean
  • Get/Let Caption() as string
  • Get Class() as string
  • Get RectClient() as Long()
  • Get/Let RectWindow() as Long()
  • Get/Let X() as Long
  • Get/Let Y() as Long
  • Get/Let Width() as Long
  • Get/Let Height() as Long
  • Get ProcessID() as long
  • Get ProcessName() as string
  • Get/Set Parent() as stdWindow
  • Get AncestralRoot() as stdWindow
  • Get/Let Style() as Long
  • Get/Let StyleEx() as Long
  • Get/Let UserData() as LongPtr
  • Get/Let WndProc() as LongPtr
  • Get/Let Resizable() as Boolean
  • Get Children() as Collection
  • Get Descendents() as Collection
  • Get ThreadID() as Long
  • Get/Let AlwaysOnTop
  • Get HTMLObject as Object Removed feature requirement, RE: IE deprecation.
  • #86 Add to taskbar

Methods

  • SetHook(idHook, hook, hInstance, dwThreadID) as LongPtr
  • AttatchThread(wnd as stdWindow, bAttatch as boolean)
  • Redraw()
  • SendMessage(wMsg, wParam, lParam)
  • PostMessage(wMsg, wParam, lParam)
  • SendMessageTimeout(wMsg, wParam, lParam, TimeoutMilliseconds)
  • ClickInput(x?, y?, Button?)
  • ClickEvent(x?, y?, Button?, isDoubleClick?, wParam?)
  • SendKeysInput(sKeys, bRaw?, keyDelay?)
  • SendKeysEvent(sKeys, bRaw?, keyDelay?)
  • Activate()
  • Close()
  • FindFirst(query)
  • FindAll(query)
  • Screenshot() Removed requirement, see stdImage

General

  • Unit Tests
  • Documentation
  • Mac compatibility

stdArray starts at 1 instead of 0

Description

In the current implementation of the stdArray it starts at 1 instead of 0, however in vba and javascript the array starts at 0 is this a planned design decision or is it designed to be that way?

Example

    Dim arr As stdArray
    Set arr = stdArray.Create(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
    Debug.Print (arr(0)) 'throws an error
    Debug.Print (arr(1))

Error

Runtime error: 9
Index out of range

Expectation

stdArray starts at 0

Bug/Unexpected Behaviour when using stdArray.Reduce

The stdArray.Reduce method exhibits the following unexpected behaviour because the intitialValue Parameter is always predeclared as zero:

  1. minimum value of an array of positive integers always returns zero, even if it was not included in the original array
  2. maximum value of an array of negative integers always returns zero, even if not in the original array.

The following calls would both print a zero, while the expected answer would be 1 and -1 respectively:
Debug.Print stdArray.Create(1, 2, 3, 4, 5).reduce(stdLambda.Create("If $1 < $2 Then $1 else $2"))
Debug.Print stdArray.Create(-1, -2, -3, -4, -5).reduce(stdLambda.Create("If $1 > $2 Then $1 else $2"))

In order to get the correct answer one would have to set initialValue as follows:
to a value >= the minimum to calculate the minimum
to a value <= the maximum to calculate the maximum
but both of these require that the answer is already known.

Standard behaviour of JS/Java would be to have initialValue as an optional parameter and start with the first element of the array if initialValue is not passed in.

Public Function Reduce(ByVal cb As stdICallable, Optional ByVal initialValue As Variant=0) As Variant

`stdLambda` #performance

I have been playing with stdLambda and a doubt is gnawing my head. Why is the evaluation of a function like sin(x) so slow compared to numeric evaluation in this class module, given the fact it is a single operation?

Runtime Error - Overflow

pLength is defined as Long in global variables
Array Access with Integer will cause overflow or not all elements to be accessible

Public Property Get item(ByVal i As Integer) As Variant

stdVBA/src/stdArray.cls

Lines 507 to 512 in 835bf4b

Public Property Set item(ByVal i As Integer, ByVal item As Object)
set pArr(i) = item
End Property
Public Property Let item(ByVal i As Integer, ByVal item As Variant)
pArr(i) = item
End Property

stdVBA/src/stdArray.cls

Lines 517 to 519 in 835bf4b

Public Sub PutItem(ByVal i As Integer, ByRef item As Variant)
CopyVariant pArr(i), item
End Sub

Public Function indexOf(ByVal el As Variant, Optional ByVal start As Integer = 1) as integer

Public Function includes(ByVal el As Variant, Optional ByVal startFrom As Integer = 1) As Boolean

stdVBA/src/stdArray.cls

Lines 629 to 630 in 835bf4b

Dim i As Integer
For i = 1 To pLength

stdVBA/src/stdArray.cls

Lines 647 to 648 in 835bf4b

Dim i As Integer
For i = 1 To pLength

stdVBA/src/stdArray.cls

Lines 661 to 662 in 835bf4b

Dim i As Integer
For i = 1 To pLength

stdVBA/src/stdArray.cls

Lines 708 to 709 in 835bf4b

Dim i As Integer
For i = 1 To pLength

stdLambda - Enable results cache

Ability to have a results cache on lambda expressions which could be used as an optimisation (perhaps with options like “only remember last n results” would be good too.

This could greatly increase the performance of stdLambda expressions across large but consistent datasets.

`stdEnumerator` compile error in 32 bit

Private Declare PtrSafe Function createEmptyArrayLongLong Lib "oleaut32" Alias "SafeArrayCreateVector" (Optional ByVal vt As VbVarType = vbLongLong, Optional ByVal lLow As Long = 0, Optional ByVal lCount As Long = 0) As LongLong()

Will raise a compiler error in 32 bit.

Fix, change to:

#If Win64 Then
    Private Declare PtrSafe Function createEmptyArrayLongLong Lib "oleaut32" Alias "SafeArrayCreateVector" (Optional ByVal vt As VbVarType = vbLongLong, Optional ByVal lLow As Long = 0, Optional ByVal lCount As Long = 0) As LongLong()
#End If

Additionally

CreateFromListObject needs a test as it failed on first try. v needs to be vData

stdWindow.SendKeysEvent() - Await key received

Use SetWindowHookEx() to create a KeyboardHook to ensure all keys sent by keybd_event() are received.

Pseudo-code:

Function KeybdHook(key, ...)
  For iKey = 1 to len(keys)
    if not keys(iKey).received then
      if key.code = keys(iKey).code then
        key.received = true
        exit for
      end if
      exit for
    end if
  next
  KeybdHook = CallNextHookEx(...)
End Function

Function SendKeysEvent(..., Optional bWaitForRetrieval as boolean)
  if bWaitForRetrieval then iHook = SetWindowHookEx(ActiveWnd, WH_KEYBOARD, AddressOf KeybdHook)
  For iKey = 1 to len(keys)
    keybd_event(keys(iKey))
  next 
  if bWaitForRetrieval then
    While not keys(len(keys)).received
      DoEvents
    Wend
    Call ClearWindowHookEx(iHook)
  end if
  
End Function

See also:

stdEnumerator

GENERAL FEATURES

  • Collection/Dictionary Key preservation
  • Better IEnumVARIANT handling

CONSTRUCTORS

  • CreateFromIEnumVariant #54
  • CreateFromICallable #26
  • CreateFromArray #26
  • CreateFromDictionary
  • CreateEmpty
  • Create(obj, iFromType=IEnumVariant)
  • init #PROTECTED

INSTANCE PROPERTIES

  • Get/Let/Set item
  • Get length

INSTANCE METHODS

Many methods were inspired by those in Ruby's Enumerable: https://ruby-doc.org/core-2.7.2/Enumerable.html

  • asCollection()
  • asArray(iType as vbVarType)
  • asDictionary()
  • Sort()
  • Reverse()
  • ForEach
  • Map
  • Unique
  • Filter
  • Concat
  • Join
  • indexOf
  • lastIndexOf
  • includes
  • reduce
  • countBy
  • groupBy
  • max(cb)
  • min(cb)
  • sum(cb)
  • Flatten
  • cycle
  • findFirst
  • checkAll
  • checkAny
  • checkNone
  • checkOnlyOne
  • each_cons [1,2,3,4,5].each_cons(2,cb) ==> cb([1,2]) ==> cb([2,3]) ==> cb([3,4]) ==> cb([4,5])
  • each_slice [1,2,3,4,5].each_slice(2,cb) ==> cb([1,2]) ==> cb([3,4]) ==> cb([5])
  • partition [1,2,3,4,5,6].partition(a=>a%2=0) ==> [[2,4,6],[1,3,5]]
  • zip [1,2,3].zip([4,5,6]) ==> [[1,4],[2,5],[3,6]] | [1,2,3].zip([1,2]) ==> [[1,1],[2,2],[3,null]]

WHAT WE WON'T DO:

  • with_index - this can't be easily done, so instead implement methods like `forEach(cb,withIndex?=false)
  • with_object - this can be done with cb.Bind()

Things we can't do (yet)

  • take <-- can't do this unless we implement IEnumVARIANT and call Next() method
  • tally <-- Would like to do this but can't until we have stdDictionary ["a","b","c","b","a","b"].tally ==> {a:2, b:3, c:1}
  • to_dict <-- requires stdDictioanry ["a",1,"b",2].to_dict ==> {a:1, b:2}
  • groupBy <-- requires stdDictionary - this has been implemented currently using Scripting.Dictionary

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.