Comparing Best Times not always working correctly and not sure why - string

This is for VB.NET 2017. I am creating a program and keeping score with the best time. The timer in the program runs like a stopwatch. I would like at the end to compare the best record from a past game with the current time of a new game. If the new game has a faster time then I would like to replace it on the data file. I can do that just fine but sometimes it will put a slower time ahead of the fastest time. I have tried multiple scenarios and cannot get it consistent. If anyone could help that would be appreciated very much. I have some message boxes so I can see some outcomes. They will be commented out later when it is working properly.
If TotalBalls = 2 And SelectPoison = 2 Then
tmrTime.Enabled = False
CurrentScore = lblTime.Text
MsgBox("You win.")
'''''''''''''''''''''''''''''''''''''''''''''''''''
' CurrentScore = lblTime.Text
MsgBox("Current Time is " & CurrentScore)
NewScoreCheck = String.Compare(CurrentScore, RecordHighScore)
MsgBox(NEwScoreCheck)
'NewScoreCheck will be less than 0 if CurrentScore is less (alphabetically) than RecordhighScore
'NewScoreCheck will be greater than 0 if RecordHighSchore is greater than CurrentScore
If NewScoreCheck < 0 Then
MsgBox(CurrentScore)
Try
MsgBox("In the try statement. Writing new time")
Dim FileWrite As System.IO.StreamWriter
FileWrite = New System.IO.StreamWriter("PoisonHighScore.TXT", False)
FileWrite.WriteLine(CurrentScore)
FileWrite.Close()
Catch
MsgBox("Saving error")
End Try
Else
MsgBox("Not the fastest time.")
End If
Else
MsgBox("You lose.")
End If
strExit = MsgBox("Do you want to play again?", vbYesNo)
If strExit = vbYes Then
Application.Restart()
End If
End Sub
Edit 1: I am using some variables as TimeSpan which is why I have the values CurrentScore and RecordHighScore as Strings. When I am using TimeSpan it will not store as an Integer and will return an error. I am looking for a way to compare two times but need to store them in such a way that they can be compared which is why I used the compare string method mentioned above. I understand after looking at the solution below as to why I cannot. My question now becomes how do I store them since it cannot be stored as double, single, or integer?
To make it a little more clear think of two racers who finish with two different times and those times being unpredictable. The fastest time would win and we would write to the text file (which I know how to do) the time of the winner.
P.S. I have also tried the CInt(CurrentScore) < CInt(RecordHighScore) but that just returns an error too. Any help again would be greatly appreciated and thank you for taking the time to help me with this.

Right off the bat it looks like you're doing some implicit type conversions such as:
CurrentScore = lblTime.Text
Presumably CurrentScore is a numeric data type (like an Integer or Double), but you're setting the value equal to a String. To correct those errors, turn Option Strict on. Looking even deeper, this appears to be your problem because you use the String.Compare method to compare the scores alphabetically. To give you an example, String.Compare returns -1 when you pass 1112 and 121 as your current score and high score respectively, but obviously 121 is quicker than 1112.
What you need to do is convert all numeric values as numeric data types and then compare them using the appropriate comparison operator.

If you want the Timer to behave like a stopwatch then why not use a StopWatch? If you use a StopWatch, you can get the ElapsedMilliseconds which returns a long. The Stop method only pauses the timer; you need to call the Reset method to reset the StopWatch to zero. Call this after you collect the ElapseMilliseconds into a variable.
NewScoreCheck = String.Compare(CurrentScore, RecordHighScore) Strings are not compared in the same way numbers are.
Dim a As String = "72"
Dim b As String = "100"
If String.Compare(a, b) < 0 Then
MessageBox.Show("a comes first")
Else
MessageBox.Show("b comes First")
End If
Result b comes first!
Using a MsgBox to check values is not a great idea. Visual Studio has all sorts of great debugging tools. Inevidibly you will forget to remove a MsgBox; I have :-). Use Debug.Print which will not be in the release version.
Dim sw As New Stopwatch()
Private Sub BeginGame()
sw.Start()
End Sub
Private Sub OPCode2()
Dim TimeInMilliseconds As Long = sw.ElapsedMilliseconds
Dim TotalBalls As Integer = 2
Dim SelectPoison As Integer = 2
Dim RecordHighScore As Long
Dim CurrentScore As Long
If TotalBalls = 2 And SelectPoison = 2 Then
sw.Stop()
CurrentScore = sw.ElapsedMilliseconds
sw.Reset() 'So you can play again and get a new time
MsgBox("You win.")
'''''''''''''''''''''''''''''''''''''''''''''''''''
Debug.Print($"Current Time is {CurrentScore}")
If CurrentScore > RecordHighScore Then
Try
Debug.Print("In the try statement. Writing new time")
Dim FileWrite As System.IO.StreamWriter
FileWrite = New System.IO.StreamWriter("PoisonHighScore.TXT", False)
FileWrite.WriteLine(CurrentScore.ToString)
FileWrite.Close()
Catch
MsgBox("Saving error")
End Try
Else
MsgBox("Not the fastest time.")
End If
Else
MsgBox("You lose.")
End If
Dim strExit As MsgBoxResult
strExit = MsgBox("Do you want to play again?", vbYesNo)
If strExit = vbYes Then
'Not a good way to do this, clear your variables and UI
Application.Restart()
End If
End Sub

Related

Issue with using the String.Contains()

I am relatively new to programming, and am starting off with VB.net using Microsoft VB Studio 2019. I usually use Python, and therefore take heavy advantage of the
> If String in("y","yes","YES"):
statement so I don't have to compare the string to every item individually.
I've been trying to do this on Virtual Basic for some time now, but have not even managed to get 1 value to compare to the string to work. I've tried 2 different methods, the first just being a basic String.Contains() command, which I've set out as such:
Dim UserSelection As String
Console.Write("Play again? ")
UserSelection = Console.Read()
If UserSelection.Contains("n") = True Then
UserPlaying = False
End If
My thought process here was that the computer would look at UserSelection, and if it contained the letter 'n' at any point then it would result as being True (eg: if UserSelection = 'no', 'nope', 'n' ext ext) However, every time I've ran this code, the result always comes back as false, no matter what UserSelection is.
I've also tried using the IndexOf command (which makes the search case insensitive) to see if it would work then, but again something seems to be up with it:
Dim UserSelection As String
Console.Write("Play again? ")
UserSelection = Console.Read()
Dim subtxt As String = "n"
Dim comp As StringComparison = StringComparison.OrdinalIgnoreCase
Dim result As Boolean = If(UserSelection.IndexOf(subtxt, comp) > 0, True, False)
If result = True Then
UserPlaying = False
End If
My indentation appears correct in both blocks of code, and I cannot for the life of me figure out what it wrong here.
If someone could help me with this (especially if you could adjust the code so that it could work with multiple comparisons) then that would be more than appreciated.
Thanks so much,
Alfie :)
Console.Read returns an Integer (documentation), so naturally it would return false. The method you're looking for is Console.ReadLine (documentation), which returns a string.
Take a look at this example:
Console.Write("Play again? ")
Dim input = Console.ReadLine()
Dim stopPlaying = input.IndexOf("n", StringComparison.OrdinalIgnoreCase) > -1
If (stopPlaying) Then
' the user replied with "n", "no", "nope", etc.
Else
' the user replied with something that did not contain the letter "n"
End If
Fiddle: https://dotnetfiddle.net/Fihq02
I just added .ToLower to the UserSelection string before the Contains so N or n would be recognized.
Private UserPlaying As Boolean
Sub Main()
Console.Write("Play again? ")
Dim UserSelection = Console.ReadLine()
If UserSelection.ToLower.Contains("n") = True Then
Debug.Print("It contains n")
UserPlaying = False
Else
Debug.Print("No n")
UserPlaying = True
End If
Console.ReadKey()
End Sub

Rock Paper Scissors Simulation with Markov Chain Keeping Score

For a class project my partner and I have created a Rock Paper Scissors simulation using Markov Chain. We have the input for what the computer does, but we don't know how we can keep track of the score.
How can we use VBA or maybe a function to get the score after each round?
We've tried things in VBA we tried different functions. But there is no data to summarize it.
Sub Score()
Dim sVariable As String
Dim iNumber As Integer
Dim iPC As Variant
Dim iPlayer As Variant
sVariable = Sheets("Model").Range("D10")
iPC = Sheets("Model").Range("E6") + 1
iPlayer = Sheets("Model").Range("F6") + 1
iNumber = 1
If sVariable = "PC Winner!" Then
Sheets("Model").Range("E6") = iPC
ElseIf sVariable = "Player Winner!" Then
Sheets("Model").Range("F6") = iPlayer
End If
End Sub
That code is the closest we have gotten and we added a button to make it run since it doesn't do it automatically. But now every time we add the score the move changes for the PC because of the random function we have for the Markov data. We want to keep the score and reset it everytime the game is over.
Probably easiest way is to create a global variable and increment the score upon individual wins and then Call a procedure after each round to update the scores.
Note: Depending on your implementation a global variable may not even be necessary and could be easily passed via an argument. It's just hard to tell without further details provided
Public playerScore as Integer
Public pcScore as Integer
Private Sub update_score()
Sheets("Model").Range("E6") = pcScore
Sheets("Model").Range("F6") = playerScore
End Sub
Private Sub Score()
' ... your code here ...'
If sVariable = "PC Winner!" Then
pcScore = pcScore + 1
Else
playerScore = playerScore + 1
End If
update_score
End Sub
and upon new game you re-initate the score
Private Sub new_game()
pcScore = 0
playerScore = 0
' ... your code here ...'
End Sub
I'm not exactly sure, if I've gotten your question right, but this should work.
In your future questions, it would be welcome, if you did bit of a
better job explaining what data you're working with and how your
desired result should look like, as per Minimal, Complete and
Verifiable Example, because from
your current question it's not clear:
when exactly is the game over
where exactly you want to update your score
on which condition should exactly the score increment
which procedures you are calling upon aforementioned events
So I had to do a lot of guess-work in your question. Either way, should be more than enough to guide you to the right path :)

Limit text to allowed characters only - (not by enumerating the wrong characters) | VBA

I would like to limit certain textboxes to accept only [A-Za-z]
I hope, a counterpart to Like exists.
With Like I would have to make a long list of not allowed characters to be able to filter.
Not MyString like [?;!°%/=....]
I can think of a solution in the form of:
For Counter = 1 To Len(MyString)
if Mid(MyString, Counter, 1) Like "*[a-z]*" = false then
MsgBox "String contains bad characters"
exit sub
end if
next
... but is there a more sophisticated 1liner solution ?
Until then, I have created a function to make it "Oneliner":
Function isPureString(myText As String) As Boolean
Dim i As Integer
isPureString = True
For i = 1 To Len(myText)
If Mid(myText, i, 1) Like "*[a-zA-Z_íéáűúőöüóÓÜÖÚŐŰÁÉÍ]*" = False Then
isPureString = False
End If
Next
End Function
If i add 1 more parameter, its also possible to define the allowed characters upon calling the function.
Ok, it seems my question was a bit of a duplicate, even though that did not pop in my search results.
So credits for #QHarr for posting the link.
The solution I can forge from that idea for my "oneliner" is:
If myText Like WorksheetFunction.Rept("[a-zA-Z]", Len(myText))=false then 'do something.
Using .rept is inspiringly clever and elegant in my oppinion.
So what is does: Multiplies the search criteria for each charater instead of looping through the characters.
EDIT:
In an overaboundance of nice and elegant solutions, the most recent leader is:
If not myText Like "*[!A-Za-z]*" then '... do something
Statistics update:
I have tested the last 3 solutions' performance:
I have pasted # in the below text strin at the beginning, at the end or nowhere.
The criteria were: "*[a-zA-Z \S.,]*"
For 100000 repetitions
text = "This will be a very Long text, with one unwanted in the middle, to be able to test the difference in performance of the approaches."
1.) Using the [!...] -> 30ms with error, 80ms if no error
2.) Using .Rept -> around 1800ms for all cases
3.) Using characterLoop+Mid -> around 3000ms if no error / 40-80ms ms if early error

Performance alternative over Scripting.Dictionary

I am coding a Manager in Excel-VBA with several buttons.
One of them is to generate a tab using another Excel file (let me call it T) as input.
Some properties of T:
~90MB size
~350K lines
Contains sales data of the last 14 months (unordered).
Relevant columns:
year/month
total-money
seller-name
family-product
client-name
There is not id columns (like: cod-client, cod-vendor, etc.)
Main relation:
Sellers sells many Products to many Clients
I am generating a new Excel tab with data from T of the last year/month grouped by Seller.
Important notes:
T is the only available input/source.
If two or more Sellers sells the same Product to the same Client, the total-money should be counted to all of those Sellers.
This is enough, now you know what I have already coded.
My code works, but, it takes about 4 minutes of runtime.
I have already coded some other buttons using smaller sources (not greater than 2MB) which runs in 5 seconds.
Considering T size, 4 minutes runtime could be acceptable.
But I'm not proud of it, at least not yet.
My code is mainly based on Scripting.Dictionary to map data from T, and then I use for each key in obj ... next key to set the grouped data to the new created tab.
I'm not sure, but here are my thoughts:
If N is the total keys in a Scripting.Dictionary, and I need to check for obj.Exists(str) before aggregating total-money. It will run N string compares to return false.
Similarly it will run maximun N string compares when I do Set seller = obj(seller_name).
I want to be wrong with my thoughts. But if I'm not wrong, my next step (and last hope) to reduce the runtime of this function is to code my own class object with Tries.
I will only start coding tomorrow, what I want is just some confirmation if I am in the right way, or some advices if I am in the wrong way of doing it.
Do you have any suggestions? Thanks in advance.
Memory Limit Exceeded
In short:
The main problem was because I used a dynamic programming approach of storing information (preprocessing) to make the execution time faster.
My code now runs in ~ 13 seconds.
There are things we learn the hard way. But I'm glad I found the answer.
Using the Task Manager I was able to see my code reaching 100% memory usage.
The DP approach I mentioned above using Scripting.Dictionary reached 100% really faster.
The DP approach I mentioned above using my own cls_trie implementation also reached 100%, but later than the first.
This explains the ~4-5 min compared to ~2-3 min total runtime of above attempts.
In the Task Manager I could also see that the CPU usage never hited 2%.
Solution was simple, I had to balance CPU and Memory usages.
I changed some DP approaches to simple for-loops with if-conditions.
The CPU usage now hits ~15%.
The Memory usage now hits ~65%.
I know this is relative to the CPU and Memory capacity of each machine. But in the client machine it is also running in no more than 15 seconds now.
I created one GitHub repository with my cls_trie implementation and added one excel file with an example usage.
I'm new to the excel-vba world (4 months working with it right now). There might probably have some ways to improve my cls_trie implementation, I'm openned to suggestions:
Option Explicit
Public Keys As Collection
Public Children As Variant
Public IsLeaf As Boolean
Public tObject As Variant
Public tValue As Variant
Public Sub Init()
Set Keys = New Collection
ReDim Children(0 To 255) As cls_trie
IsLeaf = False
Set tObject = Nothing
tValue = 0
End Sub
Public Function GetNodeAt(index As Integer) As cls_trie
Set GetNodeAt = Children(index)
End Function
Public Sub CreateNodeAt(index As Integer)
Set Children(index) = New cls_trie
Children(index).Init
End Sub
'''
'Following function will retrieve node for a given key,
'creating a entire new branch if necessary
'''
Public Function GetNode(ByRef key As Variant) As cls_trie
Dim node As cls_trie
Dim b() As Byte
Dim i As Integer
Dim pos As Integer
b = CStr(key)
Set node = Me
For i = 0 To UBound(b) Step 2
pos = b(i) Mod 256
If (node.GetNodeAt(pos) Is Nothing) Then
node.CreateNodeAt pos
End If
Set node = node.GetNodeAt(pos)
Next
If (node.IsLeaf) Then
'already existed
Else
node.IsLeaf = True
Keys.Add key
End If
Set GetNode = node
End Function
'''
'Following function will get the value for a given key
'Creating the key if necessary
'''
Public Function GetValue(ByRef key As Variant) As Variant
Dim node As cls_trie
Set node = GetNode(key)
GetValue = node.tValue
End Function
'''
'Following sub will set a value to a given key
'Creating the key if necessary
'''
Public Sub SetValue(ByRef key As Variant, value As Variant)
Dim node As cls_trie
Set node = GetNode(key)
node.tValue = value
End Sub
'''
'Following sub will sum up a value for a given key
'Creating the key if necessary
'''
Public Sub SumValue(ByRef key As Variant, value As Variant)
Dim node As cls_trie
Set node = GetNode(key)
node.tValue = node.tValue + value
End Sub
'''
'Following function will validate if given key exists
'''
Public Function Exists(ByRef key As Variant) As Boolean
Dim node As cls_trie
Dim b() As Byte
Dim i As Integer
b = CStr(key)
Set node = Me
For i = 0 To UBound(b) Step 2
Set node = node.GetNodeAt(b(i) Mod 256)
If (node Is Nothing) Then
Exists = False
Exit Function
End If
Next
Exists = node.IsLeaf
End Function
'''
'Following function will get another Trie from given key
'Creating both key and trie if necessary
'''
Public Function GetTrie(ByRef key As Variant) As cls_trie
Dim node As cls_trie
Set node = GetNode(key)
If (node.tObject Is Nothing) Then
Set node.tObject = New cls_trie
node.tObject.Init
End If
Set GetTrie = node.tObject
End Function
You can see in the above code:
I hadn't implemented any delete method because I didn't need it till now. But it would be easy to implement.
I limited myself to 256 children because in this project the text I'm working on is basically lowercase and uppercase [a-z] letters and numbers, and the probability that two text get mapped to the same branch node tends zero.
as a great coder said, everyone likes his own code even if other's code is too beautiful to be disliked [1]
My conclusion
I will probably never more use Scripting.Dictionary, even if it is proven that somehow it could be better than my cls_trie implementation.
Thank you all for the help.
I'm convinced that you've already found the right solution because there wasn't any update for last two years.
Anyhow, I want to mention (maybe it will help someone else) that your bottleneck isn't the Dictionary or Binary Tree. Even with millions of rows the processing in memory is blazingly fast if you have sufficient amount of RAM.
The botlleneck is usually the reading of data from worksheet and writing it back to the worksheet. Here the arrays come very userfull.
Just read the data from worksheet into the Variant Array.
You don't have to work with that array right away. If it is more comfortable for you to work with dictionary, just transfer all the data from array into dictionary and work with it. Since this process is entirely made in memory, don't worry about the performance penalisation.
When you are finished with data processing in dictionary, put all data from dictionary back to the array and write that array into a new worksheet at one shot.
Worksheets("New Sheet").Range("A1").Value = MyArray
I'm pretty sure it will take only few seconds

cancel or hide input box in vba

I have a code which asks for an input between 1-3 using an InputBox which then runs a code depending on what input it is given. The problem i have is that i don't need that InputBox anymore, as the work i intend to do only uses one input which is 3.I tried removing the input box and just inserting 3 there but then the code doesnt do anything. I then tried to insert a default value in the box, but it still appears and needs me to click enter which is what i am trying to avoid. Pls how should i go about this problem.
My Code
Function GetTypeFile() As Integer
Dim strInput As String, strMsg As String
Dim Default
choice = 0
While (choice < 1 Or choice > 3)
Default = "3"
strMsg = "Type in the kind of entities to create (1 for points, 2 for points and splines, 3 for points, splines and loft):"
strInput = InputBox(Prompt:=strMsg, _
Title:="User Info", Default:=3, XPos:=2000, YPos:=2000)
'Validation of the choice
choice = CInt(strInput)
If (choice < 1 Or choice > 3) Then
MsgBox "Invalid value: must be 1, 2 or 3"
End If
Wend
GetTypeFile = choice
End Function
Your function returns the value to wherever it's called, so you could just use:
Function GetTypeFile() As Integer
GetTypeFile = 3
End Function
or just replace any calls to the function with the number 3.
So rather than something like:
Sub Test()
ThisWorkbook.SaveAs "MyFileName", GetTypeFile
End Sub
You'd have:
Sub Test()
ThisWorkbook.SaveAs "MyFileName", 3
End
Thank you all for your reply. I just understood what to do, by setting the variable which is dependent on the value in the inputbox to 3 instead of it being TypeDocument=GetTypeFile it is now TypeDocument=3 directly..I think that is what you all had been trying to say all the while. Thank you again.

Resources