I wonder about how I can store a part of code in a string field of an object and convert it at run-time in executable code.
Let's say I have my class:
Public Class Car
Public m_IDCar As String
Public m_Brand As String
Public m_Description As String
Public m_Condition As String ' => here I need to store an If, or an If condition as a String, that will be executed at run-time.
End Class
Then the code that happens when CommandButton1.Click():
Dim carList As List(Of Car)
Dim parameter as String
' here I create carList with data from a database, so for each car In the database I create its relative object, with its .m_IDCar, .m_Brand, .m_Description and .m_Condition, and add it to carList
parameter = TextBox1.Text ' => given in input by the user
For each car As Car in carList
If (car.m_Condition = True) then ' => here there must be something to do cause, as things are now, in car.m_Condition is stored a String, but I need to parse it in code that returns a boolean value and, if this value would be True, the code will enter in the If statement.
'do something
End If
Next
Example of car.m_Condition could be:
car.m_Condition = "(car.m_Name=""BMW"" AND car.m_Description.Contains(parameter)) OR car.m_Brand=""AUDI"""
I need some tips on how to implement this approach, if someone would help me.
TY!
EDIT:
Thanks David, I have seen that question and seems a lot similar. I've seen that the user asked how store the entire If in a string:
Dim code As String = "IIf(1 = 2, True, False)"
(the users that asked that question used IIf)
The best way for me would be to store the condition to evaluate in a string and, maybe, the value to with compare the result in another one. So, for example:
Dim condition As String = "(car.m_Name=""BMW"" AND car.m_Description.Contains(parameter)) OR car.m_Brand=""AUDI"""
Dim valueToCompareWith as String = "True"
and the following If
If (car.m_Name=""BMW"" AND car.m_Description.Contains(parameter)) OR car.m_Brand=""AUDI"" = True) Then
'do something
End If
will become:
If (condition = valueToCompareWith) Then '(conceptually, because in this form it's simply a String comparison that returns always False)
'do something
End If
EDIT2:
Thanks Plutonix, I've read about your hint on Getters and Setters, I've not specified it, but the context I'm working on is a lot more complex then this. I've made a very simple example just for focus the problem, but I'm working with a good amount of objects, with various fields to compare and for every comparison there are different conditions in the If, with different logics and different types of data to compare.
Related
I am developing an app that focuses around manipulating strings in various ways. One of which is removing any duplicate items while combining files.
I have attempted to use this:
Private Sub run()
For Each filePath As String In ListBox1.Items
For Each Line In IO.File.ReadAllLines(filePath)
Dim founds() As String = Line.Split(":")
Dim hash As String = founds(0)
Dim word As String = founds(1)
foundList.Add(word)
Dim result As List(Of String) = foundList.Distinct().ToList
Label1.Text = result.Count
For Each addstring In result
ListBox2.Items.Add(addstring)
Next
Next
Next
End Sub
Distinct was very slow in this fashion, so I tried using:
Private Sub run()
For Each filePath As String In ListBox1.Items
For Each Line In IO.File.ReadAllLines(filePath)
Dim founds() As String = Line.Split(":")
Dim hash As String = founds(0)
Dim word As String = founds(1)
If Not foundList.Contains(word) Then
foundList.Add(word)
Label1.Text = foundList.Count
End If
Next
Next
For Each found In foundList
ListBox2.Items.Add(found)
Next
End Sub
This was much faster however still performs slower than what should be possible without using OpenCL or similar. I can write in C# if there is anything different available but this in .NET.
Can anyone suggest a faster or more effective method?
This can't possibly be it, surely I am missing something.
Fabio got to the obvious answer before I finished this. I've put some more detail in, but skip to the bottom for another idea.
The obvious speed issue is in the string comparison:
If Not foundList.Contains(word) Then
Comparing strings is a fairly expensive operation, and here you're comparing strings against a successively larger list of other strings. For a short list that might be OK, but when you're dealing with big lists it's going slow down somewhat.
The better option is to hash each string once then compare the hashes. There's some finesse required to handle hash collisions, but when the bulk of the data is unique and the hash function is good then you'll get a huge improvement in the speed.
In .NET the HashSet(Of T) class implements hash-based storage and lookup.
Being a Set, HashSet will only hold one of any particular value, which handles the duplication issue. It makes no guarantees about retaining the order of its contents, but in practice if you are adding only and never removing items then order is preserved.
Lookups in HashSet are very fast due to the way the hash values are stored and indexed internally. Testing to see if a value exists in the set is almost unaffected by the number of items in the list. I get lookup times on the order of ~50ns for lookups in lists from 1,000 to 1,000,000 strings with a simple test (100,000,000 lookups).
For your purposes the usage would be something like (in C#):
Private Sub Run()
' shortcut the file reads...
Dim items = ListBox1.Items.OfType(Of String)()
.SelectMany(Function(fn) File.ReadAllLines(fn))
.Select(Function(i) i.Split(":"c)(1))
Dim hash = New HashSet(Of String)(items)
ListBox2.Items.Clear()
ListBox2.Items.AddRange(hash.ToArray())
End Sub
(Sorry, VB is not my native language.)
Question is, will this actually speed things up? You'll need to do some testing of your own, but I suspect that the answer might be: not much.
Yes, it's faster than using Distinct and ToArray to get an array of sorted values. Almost twice as fast by my simple test. ~180ms vs ~275ms for a million distinct 36-character strings (yes, they're Guids) in an array. Not much of an increase. YMMV, but if the operation is taking significantly more time than that then the Distinct is probably not your biggest problem.
Do some profiling and find the actual pain point. I suspect that you'll find that ListBox2 has the Sorted flag set. Try this (again in C#, sorry):
Private Sub Run()
{
Dim items = ListBox1.Items.OfType(Of String)().
SelectMany(Function(fn) File.ReadAllLines(fn)).
Select(Function(i) i.Split(":"c)(1))
Dim hash = HashSet<string>(items)
ListBox2.Items.Clear()
Dim sorted = ListBox2.Sorted
ListBox2.Sorted = false
ListBox2.Items.AddRange(hash.ToArray())
ListBox2.Sorted = sorted
}
If that's a lot faster then the problem isn't in the Distinct it's in the sort-on-insert which is painfully slow and almost always the worst option for sorted lists.
If it's not then the problem might be
Get all values before using Distinct and adding them (the slowest part is still updating the control):
ListBox2.DataSource = (From line In IO.File.ReadLines(filePath)
Select line.Split(":"c)(1) Distinct).ToList
Use HashSet(Of String)
Dim lines = IO.File.ReadAllLines(filePath)
Dim uniqueLines = New HashSet(Of String)(values)
After initialization HashSet will contains unique values.
You can use HashSet(Of T).Add(value) method - which return true if value was added to the set and false if value already exists in the set
Dim isAdded As Boolean = uniqueLines.Add("someValue")
If isAdded Then
' Do something if added
Else
' Do something if already exists
End if
HashSet have method Contains which algorithm is O(1) - use fixed amount of operations, where for example List.Contains method will iterate whole list until find given value (O(N) - amount of operation is equal amount of items in worth case)
So your function can be re-written as below
Private Sub run()
' get data
Dim allItems = ListBox1.Items.
SelectMany(Function(path) IO.File.ReadAllLines(path)).
SelectMany(Function(line) Line.Split(":"))
Dim uniqueItems = New HashSet(Of String)(allItems)
' update controls
Label1.Text = uniqueItems.Count.ToString()
ListBox2.Items.AddRange(uniqueItems.ToArray())
End Sub
Notice that items added to the ListBox2 by using .AddRange method. This method will add items in one operation and re-draw control only one time. Where when you adding items one by one (using .Add method) you control re-drawing itself for every added item, which can be "heavy" for big amount of items.
I have an access database and I'm attempting to write some VBA to increase automation.
I have a module I've entitled Global Variables which I've successfully used to define global constants (file paths etc) and a module ReportCode which has two main subrouties, one to run a query with ADODB (scraping form params where needed - returning a recordset), and a second which takes this record set and writes the data out to an excel template.
Given I may want to have multiple queries write to multiple tabs I thought the best way was to define a ExportDocument object to contain common parameters and a OrgReport object, containing query and tab specific parameters - then gather multiple OrgReport objects in a collection.
I'd hope to then pass just these two parameters into the main subroutine. This turns out to be a pain in VBA (or at least compared to ruby!).
Here you can see how I've defined by custom objects
Option Private Module
' Define Custom Doc Object
Public Type ExportDocument
TeamName As String
TemplatePath As String
SaveName As String
SavePath As String
End Type
' Define Custom Report Object
Public Type OrgReport
Query As String
Fields As Variant
Sheet As String
StartCol As Integer
StartRow As Integer
Headers As Boolean
End Type
And here is the code in my form which then called an additional module which does the heavy lifting - I know that part works because it did before I tried to go all OOP on this...
Private Sub my_report_from_form_Click()
' Prep Query Inputs
Dim TeamX_Report As OrgReport
TeamX_Report.Query = "qry_TeamReporting Query"
TeamX_Report.Sheet = "RawData"
TeamX_Report.StartCol = 1
TeamX_Report.StartRow = 2
TeamX_Report.Headers = True
TeamX_Report.Fields = Nothing
' Prep Document Inputs
Dim Teamx_Doc As ExportDocument
Teamx_Doc.TeamName = "MyTeam"
Teamx_Doc.TemplatePath = strReportTemplatePath & "MyTeam.xltm"
Teamx_Doc.SaveName = ""
Teamx_Doc.SavePath = strReportSavePath & Teamx_Doc.TeamName
' Init and set collection for CHAIN reports
Dim TeamReports As New Collection
TeamReports .Add Item:=TeamX_Report, Key:=TeamX_Report.Query
Call export_data_dump(Teamx_Doc, TeamReports)
End Sub
This gives me the issue of:
Only public user defined types defined in public object modules can be
used as parameters or return types for public procedures of class
modules or as fields of public user defined types
Following advice here I changed
Dim Teamx_Doc As ExportDocument
to
Teamx_Doc = CreateObject("ExportDocument")
But alas now i get
Run-time error '429': ActiveX component can't create object VBA
All references to this problem seem to be related to calling code from the Word., Excel. or Outlook. codebases, so perhaps I'm just missing a prefix for my own module stored within my database?
Best lead I've found is this one, which seems to suggest there's deeper issues with what i'm trying to do, or that i may get around parts by calling Friend, though I'm lost to where and how.
Is there a way I can late bind my UDT Objects within my form code, stash one in a collection then pass both to a subroutine that will be able to grab params from the first 'Doc' object and then iterate through the second 'report' object?
VBA >_<
There's no reason I can see why this doesn't work:
Dim Teamx_Doc As ExportDocument
Especially if you're not getting an error on line
Dim TeamX_Report As OrgReport
I've used custom Public Types before - no need for CreateObject
Though the docs seem to say it's just fine, can you try removing the
Option Private Module
The error message is kinda misleading. You simply can't put variables with an User-defined Type into a collection.
Option 1: Use an array instead. This actually sounds like it would work well for what you want to do.
Option 2: Create a class module instead of an UDT for OrgReport. Then instantiate objects of that class, those you can add to a collection.
See
Excel VBA Collections and Custom Data Types
and
http://www.mrexcel.com/forum/excel-questions/16849-adding-user-defined-types-collection.html
Our Business Accounts in Acumatica have 13 custom Attributes for our main Business Account Class. I've been able to save values to the Attributes successfully, based on Acumatica's example "Adding Records to the Business Accounts and Opportunities Forms". But I have not been able to figure out how to retrieve the values with an Export.
First, I tried using a format similar to how the field was specified when saving them.
Public Function GetCustomerAttributes(ByVal customerID As String) As String()()
Dim customer As CR303000Content = m_context.CR303000GetSchema()
m_context.CR303000Clear()
Dim idFilter As Filter = New Filter()
idFilter.Field = customer.AccountSummary.BusinessAccount
idFilter.Condition = FilterCondition.Equals
idFilter.Value = customerID
' SIMILAR TO EXAMPLE FOR SAVING
Dim awdField As Field = New Field()
awdField.ObjectName = customer.Attributes.Attribute.ObjectName
awdField.FieldName = "AWD Number"
Dim searchfilters() As Filter = {idFilter}
Dim searchCommands() As Command = {awdField}
Dim searchResult As String()() = m_context.CR303000Export(searchCommands, searchfilters, 0, False, False)
Return searchResult
End Function
I thought this would return one result with the value for our attribute named "AWD Number". Instead, it returned 13 results, one for each attribute, and the value of each one was blank. I changed the FieldName to customer.Attributes.Attribute.FieldName and then it started returning the name of each attribute. So I thought if I added another field for the value, then I might get the name and value in separate results, like this:
Public Function GetCustomerAttributes(ByVal customerID As String) As String()()
Dim customer As CR303000Content = m_context.CR303000GetSchema()
m_context.CR303000Clear()
Dim idFilter As Filter = New Filter()
idFilter.Field = customer.AccountSummary.BusinessAccount
idFilter.Condition = FilterCondition.Equals
idFilter.Value = customerID
Dim awdField As Field = New Field()
awdField.ObjectName = customer.Attributes.Attribute.ObjectName
awdField.FieldName = customer.Attributes.Attribute.FieldName
Dim awdValue As Field = New Field()
awdValue.ObjectName = customer.Attributes.Attribute.ObjectName
awdValue.FieldName = customer.Attributes.Attribute.Value
Dim searchfilters() As Filter = {idFilter}
Dim searchCommands() As Command = {awdField, awdValue}
Dim searchResult As String()() = m_context.CR303000Export(searchCommands, searchfilters, 0, False, False)
Return searchResult
End Function
I did get a 2-item array back for each of the 13 results, but the value in the second field was still blank.
Does anyone know how I can get the values? I don't really care if I have to get them one at a time, but I'd prefer to get them all at once with their names or codes so that I don't have to rely on the indices always staying the same. Below are images of the debugger running on my second example and view in Acumatica. Thanks!
Your first attempt is correct, however you're not using the right object name and field name. The system will dynamically add fields to the primary object (view) of the screen, in this case the object name represented by customer.AccountSummary.BusinessAccount.ObjectName variable (I suggest you use the debugger to see what this value equals too - good learning exercise).
The attribute field name will use the same naming convention as used in How To Retrieve An Attribute Field In StockItems In Acumatica API?. The naming convention is _Attributes. The attribute ID is not the attribute name; I don't see your configuration but I doubt in your case the Attribute ID is "AWD Number". To summarize, the code will look like:
Dim awdField As Field = New Field()
awdField.ObjectName = customer.AccountSummary.BusinessAccount.ObjectName
awdField.FieldName = "AWDNumber_Attributes"
In your example, by putting the Attributes.Attribute.ObjectName object, the system will iterate through all values inside this table, and then return for every row the fields you want. I'm not exactly sure why you're not seeing all the attribute values in this case, but I think you should be fine with the example above.
I started learning VBA for my job at the end of last summer, and I can proudly say this is the first time I haven't be able to find the answer on Google. I started teaching myself about Classes this week, and I have come across a situation where I would like to be able to identify an "indexed property" for my class.
Since that probably isn't the clearest explanation, here is a hypothetical example:
The class which I have created for my super awesome sandwich shop (clsSASS) contains properties for Calories, Weight in Grams, Price, and Ingredients. The first three are variables with very straight forward let and get statements. I.E.:
Public pCal As Integer
Public Property Get Calories() As Integer
Calories= pCal
End Property
Public Property Let Calories(Value As Integer)
pCal = Value
End Property
Ingredients however is designed to contain, in order of entry, the list of ingredients. My initial instinct was to do something like this:
Public pIngd As Collection
Public Property Get Ingredients(Value As Integer) As Collection
Ingredients = pIngd(Value)
End Property
Public Property Set Ingredients(Object As Collection)
Set pIngd = Object
End Property
So if Bacon were the first ingredient in the list (and let's be honest it always would be), something like clsNewSandwich.Ingredients(1) would return the string 'Bacon'.
The problem arose when I added a container property to a class, and then couldn't figure out how to identify the individual items in the container. So this may just be a simple syntax issue that has nothing to do with classes whatsoever.
Many Thanks!
*edited for clarity/continuity
OK - I will retract my advice about always naming let/set and Get the same, since in this case you cannot, since the "input" and "output" types are not the same. So, in the sample below I've named the property which just returns one ingredient as Ingredient
Class "clsSASS":
Dim pIngd As Collection
Property Set Ingredients(c As Collection)
Set pIngd = c
End Property
Property Get Ingredient(v As Integer) As String
Ingredient = pIngd(v)
End Property
Regular module:
Sub Tester()
Dim c As New Collection
Dim s As New clsSASS
c.Add "bacon"
c.Add "lettuce"
c.Add "tomato"
Set s.Ingredients = c
Debug.Print s.Ingredient(1) 'bacon
Debug.Print s.Ingredient(2) 'lettuce
Debug.Print s.Ingredient(3) 'tomato
End Sub
Every time i try to attribute any type of string to this i get Object reference not set to an instance of an object. I have tried every combination of possible way to handle the string, convert it to a string again and all the fuzz. It's very frustrating and i guess it's some kind of base principle of the structure/class usage and the string array or whatnot (which is also very dumb)
Private Class movie
Public name As String
Public actors As String
Public year As Integer
Public country As String
Public votes As Integer
End Class
Private movies() As movie
If File.Exists(OpenFileDialog1.FileName) Then
lblPath.Text = OpenFileDialog1.FileName
Dim iFile As New StreamReader(lblPath.Text)
While Not iFile.EndOfStream
current = iFile.ReadLine
movies(i).name = "sasasasa"
lbMovies.Items.Add(movies(i).name)
i = i + 1
End While
End If
these are the code parts where i use it
You are creating an empty array of movie objects, as was pointed out previously. Consequently movies(i) is Nothing. When you try to access a member (movies(i).name) the appropriate exception is generated. Note that your code does not even reach the assignment operator = but fails prior to that. In other words, this has nothing to do with strings altogether; you will get the same error if you write movies(i).votes = 42 instead. To fix your code you will first have to create a movie object, populate it and append it to your array.