Does CreateTextFile create a race condition over OpenTextFile? [closed] - file-io

Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 6 years ago.
Improve this question
My script iterates over a large amount of data in a .xlsx file and, depending on whether or not the file exists yet, creates/opens a file with respect to the data set it's currently working on.
The code that handled determining whether the FileSystemObject should create/open the text file ended up causing a portion of the data to be repeated in the beginning of the text file. Once changed to working with a single file there were no repeats meaning the logic which handled extraction of data is fine.
Set excel = CreateObject("Excel.Application")
Set excelWorkbook = excel.Workbooks.Open("C:\data.xlsx")
Set excelSheet = excelWorkbook.Worksheets(1)
Set fso = CreateObject("Scripting.FileSystemObject")
id = ""
sum = 0
row = 2
Do While row < 40500
'identifier located in column c
id = excelSheet.Range("C" & row).value
'followed by numbers in column h
'column h is empty on the row of an id
Do While Len(excelSheet.Range("H" & row+1).value) > 0
sum = excelSheet.Range("H" & row+1).value + sum
row = row + 1
Loop
WriteToText id,row,sum
sum = 0
row = row + 1
Loop
Sub WriteToText(x, y, z)
fileName = "C:\file" & x & ".txt"
If fso.FileExists(fileName) Then
Set file = fso.OpenTextFile(fileName, 8)
file.WriteLine x & " " & y " " & z
file.Close
Else
Set file = fso.CreateTextFile(fileName)
file.WriteLine x & " " & y " " & z
file.Close
End If
End Sub
Is this a result of not freeing the memory space once the text file is created, processed, and closed?
Set file = Nothing
...causing a buffer created with CreateTextFile to persist through memory once OpenTextFile is called?

Without seeing where your data comes from and how you pull it there's no telling where duplicate data might come from. I don't see an inherent race condition in your code, since VBScript isn't multi-threaded in the first place. However, you can avoid any issues pertaining to the distinction whether or not the file already exists by simply using the OpenTextFile method with the third parameter set to True. That will automatically create a missing file and otherwise append to it.
Sub WriteToText(x, y, z)
fileName = "C:\file" & x & ".txt"
fso.OpenTextFile(fileName, 8, True).WriteLine x & " " & y " " & z
End Sub
Setting objects to Nothing inside functions is almost never required, because VBScript usually handles that automatically by itself.

Related

Changing text in a contentcontrol is very slow

I have a big table in ms-word that contains 85 contentcontrols (combo boxes). I want to change the content using a vba loop (see below). It takes longer than one minute for it to complete...
Are there other options?
Private Sub Btn_Clear1_Click()
Dim a
Dim c As ContentControl
a = FindTable(ActiveDocument.Name, "myTableName")(1) 'returns an array(Long) with number of table found
For Each c In ActiveDocument.Tables(a).Range.ContentControls
c.Range.text = "MY CHANGED TEXT"
Next c
End Sub
Thanks in advance for any hint!
Here, turning off screenupdating reduces the time from about 6 seconds to less than 1 second. e.g.
On Error Goto turnscreenon
Application.Screenupdating = False
For Each c In ActiveDocument.Tables(a).Range.ContentControls
c.Range.text = "MY CHANGED TEXT"
Next c
turnscreenon:
Application.Screenupdating = True
That may only work on the Windows version of Word.
If you know exactly how many combo boxes there are going to be, you could consider creating a custom xml part containing an array of XML Elements to contain the values. Map each content control to one of those elements. Then instead of writing the values to the content control ranges, write them to the XML Part and let Word do the work. That works almost instantaneously here.
e.g. in a simple scenario where you just have those 85 content controls in the table, you could set up the Custom XML Part like this (I leave you to write any code that you need to delete old versions). You should only need to run this once.
Sub createCxpAndLink()
' You should choose your own Uri
Const myNamespaceUri As String = "mycbcs"
Dim a
Dim i As Long
Dim s As String
Dim cxp As Office.CustomXMLPart
With ActiveDocument
a = FindTable(.Name, "myTableName")(1)
s = ""
s = s & "<?xml version='1.0' encoding='UTF-8'?>" & vbCrLf
s = s & "<cbcs xmlns='" & myNamespaceUri & "'>" & vbCrLf
For i = 1 To .Tables(a).Range.ContentControls.Count
s = s & " <cbc/>" & vbCrLf
Next
s = s & "</cbcs>"
Set cxp = .CustomXMLParts.Add(s)
With .Tables(a).Range.ContentControls
For i = 1 To .Count
.Item(i).XMLMapping.SetMapping "/x:cbcs[1]/x:cbc[" & Trim(CStr(i)) & "]", "xmlns:x='" & myNamespaceUri & "'", cxp
Next
End With
Set cxp = Nothing
End With
End Sub
Then to update the contents you need something like this
Sub testsetxml()
Const myNamespaceUri As String = "mycbcs"
Dim i As Long
'our start time...
Debug.Print Now
With ActiveDocument.CustomXMLParts.SelectByNamespace(myNamespaceUri)(1)
For i = 1 To 85
.SelectNodes("/ns0:cbcs[1]/ns0:cbc[" & Trim(CStr(i)) & "]")(1).Text = "my changed text "
' or if you want to put different texts in different controls, you can test using e.g.
.SelectNodes("/ns0:cbcs[1]/ns0:cbc[" & Trim(CStr(i)) & "]")(1).Text = "my changed text " & Cstr(i)
Next
End With
'our end time...
Debug.Print Now
End Sub
(NB you cannot do it by mapping all the controls to a single XML element because then all the dropdowns will all be updated to the same value whenever you change the value of one of them.)
Apologies for any typos - I've changed the code to be more in line with what you have already and have not tested the changes.

VBA: Reading a list of filenames taking various amounts of time

I have been tasked with creating and updating a series of VBA based excel add-on programs by my superiors at work. One of the programs is a utility that compares the contents of two folders and gives a list of what files are different. Most of the program works very well, but I am having issues with one section of the code; namely, the section that is tasked with gathering all the filenames of the files to be checked.
The section itself does function, most of the time with no issue, but on occasion, it will take inordinate amounts of time. I have been running the tests on the same set of data for the entire development of the utility, so I know that the issue is not the number of files being searched (which is in the hundreds and will eventually be nearly the thousands). My issue is that the section of code is wildly inconsistent with its timing.
The section of code in question is here:
Sub GetFileList(ByRef FileSpec() As String, FileArray() As FileInfo, FoldIndex As Integer)
'FileSpec - an array of strings that correspond to the filtered list of file extensions to be searched
'FileArray - an array of strings that will end up holding the complete list of relevant file names
'FoldIndex - an integer that corresponds to which folder is being searched (1 or 2)
'Returns an array of filenames that match FileSpec
'If no matching files are found, returns an error messagebox
'Arbitrarly takes inordinate amount of time, sometimes upwards of 20 seconds, to finish running.
'Usually when the filtering has been changed.
Dim FileCount As Integer
Dim FileName As String
On Error GoTo NoFilesFound
FileCount = 0
For i = LBound(FileSpec) + 1 To UBound(FileSpec)
FileName = Dir(FileSpec(i))
'Loop until no more matching files are found
Do While FileName <> ""
FileCount = FileCount + 1
ReDim Preserve FileArray(1 To FileCount)
FileArray(FileCount).FileName = FileName
FileName = Dir()
Select Case FoldIndex
Case 1
Call FormFunctionality.UpdateResults(FileCount & ": " & FileArray(FileCount).FileName & vbCrLf, "")
Case 2
Call FormFunctionality.UpdateResults("", FileCount & ": " & FileArray(FileCount).FileName & vbCrLf)
End Select
Loop
Next i
If FileCount = 0 Then GoTo NoFilesFound
Exit Sub
'Error handler
NoFilesFound:
ReDim FileArray(1)
FileArray(1).FileName = "Error"
MsgBox ("Error: No files found of requested type" & vbCrLf & "Please review folders and requested file types.")
End
End Sub
Sub UpdateResults(Str1 As String, Str2 As String)
'Prints strings to the results window text boxes
RbtUtilResultScreen.Folder1Results.Text = RbtUtilResultScreen.Folder1Results.Text & Str1
RbtUtilResultScreen.Folder2Results.Text = RbtUtilResultScreen.Folder2Results.Text & Str2
RbtUtilResultScreen.Folder1Results.SetFocus
RbtUtilResultScreen.Folder2Results.SetFocus
End Sub
The Time inconsistency varies wildly. For ~350 files being searched, the average time to generate the list of files is about 2 seconds. Sometimes, that time shoots up to 10 or 20 seconds, which is frankly unacceptable. It gets even worse with more files being searched, and I have had it take up to a minute and thirty seconds for ~800 files (where the average is still something like 3 seconds).
My question is this: Is there something obvious that I am doing wrong, or is there a better way to handle reading files in that I have overlooked? What could be causing this inconsistency within the program?
If more in-depth timing information or other sections of the code are needed, I will provide. I do not believe that I can provide access to the data that I have been running the tests on, though.
A reason is not clear from your code. However, you can optimize some part and maybe that reduces the time. Namely, you ReDim on each iteration and this can cause memory management overhead. Instead, ReDim a fixed number of items, for example:
Dim nElms As Integer
...
nElms = 0
FileCount = 0
Do While FileName <> ""
FileCount = FileCount + 1
If (FileCount > nElms) Then
nElms = nElms + 250
ReDim Preserve FileArray(1 To nElms)
EndIf
Paul has suggested in his response that you need to use "fixed step" to re-dimension the array which seems to be one issue.
The other issue seems to be updating the form text continuously to show progress. If it is not too critical then you can think of changing it to something like below.
Declare dictionary object at the beginning of code before Loop.
Dim objDict As Object
objDict = CreateObject("Scripting.Dictionary")
And then modified block would be like shown below.
For i = LBound(FileSpec) + 1 To UBound(FileSpec)
FileName = Dir(FileSpec(i))
objDict.RemoveAll
Do While FileName <> ""
If Not objDict.Exists(FileName) Then objDict.Add FileName, FileName
FileName = Dir()
Loop
Select Case FoldIndex
Case 1
Call FormFunctionality.UpdateResults(objDict.Count & ": " & FileName & vbCrLf, "")
Case 2
Call FormFunctionality.UpdateResults("", objDict.Count & ": " & FileName & vbCrLf)
End Select
Next I
Test it on a backup!

Global Variable Lookup

Hopefully, I'll make this question as precise and understandable as possible - but you'll tell me if i don't ! Thanks in advance.
Firstly, a little background and what I've found that DOES work, then on to a small change that I cannot get to work. Rather than use the whole code, I've used snippets that should give enough information for you to understand that I have initiated things correctly first.
I use a menu system (a turbo-charged version of the original MS one) that has additional fields to store information needed to make changes depending upon what wording the user wants to use, so I may name a field Product, whereas a user may want to call it Goods or Items or Stuff or whatever he desires! So I store the user preferences in a separate table (we'll call that tblWORDS). When the menu is populated (remember it operates in a similar fashion to the standard SwitchBoard) with the data from fields: ItemText, Command & Argument the menu normally displays the text from ItemText, which I use. But, I have added a NEW field called CAPTION in SWITCHBOARD table because the VBA code does not allow for formatting the labels as I want them. So, when the VBA code reads ItemText for the label, from the recordset, and it encounters a | (pipe), my added VBA code then looks to the field Caption for a string. Hopefully, enough background info!!??
[SwitchBoard].[Caption] originally contained the following: (EVERYTHING INCLUDED)
"" & dlookup("fldProduct","tblWORDS") & ""
This worked perfectly!! But...
Instead of performing a lookup every time I need the WORD, I decided to create GLOBAL VARIABLES, so I have a Global Variable of glProduct, which obtains the word from the tblWORDS table correctly and retains that just fine. This is then available throughout the session anywhere.
I have substituted the string above to read the Global Variable instead of performing a lookup each time, to: (again, EVERYTHING INCLUDED)
"" & glProduct & ""
So, my code is as follows:
While (Not (rs.EOF))
Me("Option" & rs![ItemNumber]).Visible = True
'MY PIPE DEVIATION
If Left(Trim(rs![ItemText] & ""), 1) = "|" Then
'THIS LINE WORKS JUST FINE
'DISPLAYS CORRECTLY (rs!Caption = "" & dlookup("fldProduct","tblWORDS") & "")
szTemp = DLookup(rs![Caption], "tblWORDS")
'AS DOES THIS (but, I'm explicitly naming the variable in code! Only entered this line to show that the variable is working!)
szTemp = "" & glbProduct & ""
'THIS DOES TOO (Just a BYREF function for testing)
szTemp = fnGetValue(DLookup(rs![Caption], "tblWORDS"))
Me("Option" & rs![ItemNumber]).Caption = szTemp
Else
If rs![ItemNumber] = 0 Then
Me("OptionLabel" & rs![ItemNumber]).Caption = VBA.Trim(rs![ItemText] & " (" & rs![SwitchboardID] & ")")
Me("OptionLabel" & rs![ItemNumber]).Visible = True
Me("Option" & rs![ItemNumber]).Visible = False
Else
Me("Option" & rs![ItemNumber]).Caption = VBA.Trim(rs![ItemText] & "")
End If
End If
rs.MoveNext
Wend
REVISED TO:
While (Not (rs.EOF))
Me("Option" & rs![ItemNumber]).Visible = True
'MY PIPE DEVIATION
If Left(Trim(rs![ItemText] & ""), 1) = "|" Then
'THIS WORKS (but, I'm explicitly naming the variable in code! Only entered this line to show that the variable is working!)
szTemp = "" & glbProduct & ""
'THIS DOES NOT (FYI: rs!Caption = "" & glbProduct & "")
szTemp = rs!Caption
'NOR DOES THIS (Just a BYREF function for testing)
szTemp = fnGetValue(rs!Caption)
Me("Option" & rs![ItemNumber]).Caption = szTemp
Else
If rs![ItemNumber] = 0 Then
Me("OptionLabel" & rs![ItemNumber]).Caption = VBA.Trim(rs![ItemText] & " (" & rs![SwitchboardID] & ")")
Me("OptionLabel" & rs![ItemNumber]).Visible = True
Me("Option" & rs![ItemNumber]).Visible = False
Else
Me("Option" & rs![ItemNumber]).Caption = VBA.Trim(rs![ItemText] & "")
End If
End If
rs.MoveNext
Wend
What IS displayed is the literal string as entered ("" _glProduct "") not Product as it was prior to my revision!
So... the first problem is that you're confusing everybody with what you're doing it and where it's happening - where did rs![Caption] come from???
The second problem is you can't access global variables in your form or control properties.
You need to set the caption via code. I'm going to make some big guesses as to what you're using and what each element is, so let's start here:
First
Replace the Caption property with something else - just "Caption" for example.
Second
Add this code after your loop - I think that will work - hard to really know because you don't show where this code is and when it runs
[SwitchBoard].Caption = "" & glProduct & ""

Trying to use VBA to write query in Access

I am getting a type mismatch with the following syntax in my Access VBA. I am trying to update my table named "Billing" by seeing if any records have a date that looks at a string value in my "Certs" table like "2012-07-01" corresponding to my form's billYear textbox e.g. 2012 and my billMonth textbox e.g. 07. Is there a better way to write the VBA or see an error - many thanks:
Dim sRecert As String
Dim byear As Integer
Dim bmonth As Integer
byear = Me.billYear
bmonth = Me.billMonth
sRecert = "Update Billing set recertifying = -1 where (select certificationExpireDate from certs where Left((certificationExpireDate),4) = " & byear
& " and Mid((certificationExpireDate),6,2) = " & bmonth & ")"
DoCmd.RunSQL sRecert
I may not have explained it well. I created a real Query called from my form:
DoCmd.OpenQuery "updateRecert"
I set up my SQL below as a test on a real date I’m working with. It is in SQL Server (ODBC linked)
My dbo_certs table and my dbo_billing table share only one joinable field peopleID:
UPDATE dbo_Billing AS a INNER JOIN dbo_certs AS b ON a.peopleid = b.peopleid
SET a.recertifying = -1
WHERE b.certificationExpireDate = '2015-08-31 00:00:00.000';
The above gave a data mismatch error.
My bottom line is I have two text boxes on my form to pass in data preferably into my VBA code:
billMonth which in this case is 8 because it is an integer so that is
a problem
billYear is 2015
so I need to update my dbo_billing table’s ‘recertifying’ field with -1 if the dbo_cert’s field ‘certificationExpireDate’ is '2015-08-31 00:00:00.000' but only if that can be gotten from the form.
Is there a better way to write the VBA or see an Error?
Yes. You need Error Handling
I don't think the issue is in the code, I think it's in the SQL.
To troubleshoot your code, wrap it in an good error handler.
Public Sub MyRoutine()
On Error GoTo EH
'put your code here
GoTo FINISH
EH:
With Err
MsgBox "Error" & vbTab & .Number & vbCrLf _
& "Source" & vbTab & .Source & vbCrLf & vbCrLf _
& .Description
End With
'for use during debugging
Debug.Assert 0
GoTo FINISH
Resume
FINISH:
'any cleanup code here
End Sub
When the msgbox shows the error, make note of the Source. This should help you determine where the error comes from.
The lines following 'for use during debugging are helpful. Here's how to use them:
execution will stop on the Debug.Assert 0 line
drag the yellow arrow (which determines which line to run next) to the Resume line
hit {F8} on the keyboard (or use the menu Debug > Step Into)
This will go to the line where the error occurred. In your case, it will probably be the last line of your code.
Error in SQL... but!! Are you sure that certificationExpireDate is string and all the time equal to yyyy-mm-dd pattern?? It's dangerouse to have relation with "not certain" key like you have. I think this is not a good db design.
But, after all, for your case:
VBA:
sRecert = "UPDATE Billing a inner join certs b " & _
"on format(a.imaginary_date_field, """yyyy-mm-dd""") = b.certificationExpireDate " & _
"set a.recertifying = -1 " & _
"where CInt(Left((b.certificationExpireDate),4)) = " & byear & " and CInt(Mid((b.certificationExpireDate),6,2)) = " & bmonth
QueryDef:
PARAMETERS Forms!your_form!byear Short, Forms!your_form!bmonth Short;
UPDATE Billing a inner join certs b
on format(a.imaginary_date_field, "yyyy-mm-dd") = b.certificationExpireDate
set a.recertifying = -1
where CInt(Left((b.certificationExpireDate),4)) = Forms!your_form!byear and CInt(Mid((b.certificationExpireDate),6,2)) = Forms!your_form!bmonth
UPDATED
mismatch error
You get error probable because you have date/time field, not a string. Date in MS Access queries write with # symbol. WHERE b.certificationExpireDate = #2015-08-31 00:00:00.000#;
In your case:
PARAMETERS Forms!your_form!byear Short, Forms!your_form!bmonth Short;
UPDATE dbo_Billing AS a INNER JOIN dbo_certs AS b ON a.peopleid = b.peopleid
SET a.recertifying = -1
WHERE year(b.certificationExpireDate) = Forms!your_form!byear and Month(b.certificationExpireDate) = Forms!your_form!bmonth;
For more info follow this link

Excel VBA make a script asynchronous

I have a script that can ping a list of computers and change their background color depending after the result it gets.
My problem is, that it blocks the entire excel file while it runs.
So my question is, how can I make it to run async?
Here is the code:
'ping
Function sPing(sHost) As String
Dim oPing As Object, oRetStatus As Object
Set oPing = GetObject("winmgmts:{impersonationLevel=impersonate}").ExecQuery _
("select * from Win32_PingStatus where address = '" & sHost & "'")
For Each oRetStatus In oPing
If IsNull(oRetStatus.StatusCode) Or oRetStatus.StatusCode <> 0 Then
sPing = "timeout" 'oRetStatus.StatusCode <- error code
Else
sPing = sPing & vbTab & oRetStatus.ResponseTime & Chr(10)
End If
Next
End Function
Sub pingall_Click()
Dim c As Range
Dim p As String
Application.ScreenUpdating = True
For Each c In ActiveSheet.Range("A1:N50")
If Left(c, 7) = "172.21." Then
p = sPing(c)
If p = "timeout" Then
c.Interior.ColorIndex = "3"
ElseIf p < 16 And p > -1 Then
c.Interior.ColorIndex = "4"
ElseIf p > 15 And p < 51 Then
c.Interior.ColorIndex = "6"
ElseIf p > 50 And p < 4000 Then
c.Interior.ColorIndex = "45"
Else
c.Interior.ColorIndex = "15"
End If
End If
Next c
Application.ScreenUpdating = False
You can't do too much about this unfortunately since VBA runs in a single thread.
You can however introduce a degree of responsiveness by putting
VBA.DoEvents()
in various places in your code, ideally in the tight loops. In your case, put them just after the lines containing For. This pauses the VBA and flushes the event queue which will have the effect of making Excel responsive.
(Toggling the screen updating is a bad idea since you might leave things in a bad state if the function terminates unexpectedly. I'd remove the lines that do that if I were you.)
Excel can calculate "asynchronously". Call sPing as a function.
I'm not sure why your range is A1:N50. I assume one of the columns is the IP address, which I will assume as A. So your formula in column M will look like =sPing(A1).
As for the color coding, you can use conditional formatting.
While strictly speaking you cannot make Excel to behave as you need, there is a trick to work around it. The solution is to create another Excel instance. Then you can run the macro in one of the instances and work independently in the other one.
You can open another Excel instance from the Run prompt (press Windows + R) and then type Excel /x, then Enter