I've been working to create a random pairing algorithm for my bass club. The idea is to pair a value in Column A (Boaters) with a value in Column B (non-boaters). If there are no more non-boaters, any remaining boaters should be paired unless only one unpaired boater remains.
I found some VBA code online, which works fine in Excel, but all of my club's stuff is in Google Sheets and I'd like to have the same pairing function in GAS.
I've tried my best to convert the VBA code to GAS, but honestly, I have some experience with VBA and I'm still a relative novice at GAS, although I'm learning.
I've pasted the two VBA functions below, followed by the GAS conversion I've been working on. The comments show the areas where I'm having trouble, particularly with the called sorting function (vSortM), although I'd welcome a second set of eyes to make sure I haven't incorrectly coded something else.
Can anyone advise if I am performing the conversion from VBA to GAS correctly?
Pairing Algorithm in VBA that I found online:
Option Explicit
Sub test()
Dim Boters(), NonBoters(), i As Long, x As Long
Boters = Range("a1", Range("a" & Rows.Count).End(xlUp)).Value
Redim Preserve Boters(1 To UBound(Boters), 1 To 2)
NonBoters = Range("b1", Range("b" & Rows.Count).End(xlUp)).Value
Redim Preserve NonBoters(1 To UBound(NonBoters), 1 To 2)
Randomize
For i = 1 To UBound(Boters)
Boters(i, 2) = Rnd
Next
For i = 1 To UBound(NonBoters)
NonBoters(i, 2) = Rnd
Next
VSortM Boters, 1, UBound(Boters), 2
VSortM NonBoters, 1, UBound(NonBoters), 2
x = Application.Min(UBound(Boters), UBound(NonBoters))
With Cells(1, 4).Resize(x, 2)
.CurrentRegion.ClearContents
.Columns(1).Value = Boters
.Columns(2).Value = NonBoters
End With
If x < UBound(Boters) Then
For i = x + 1 To UBound(Boters) Step 2
If i + 1 > UBound(Boters) Then Exit For
Cells(i, 4).Value = Boters(i, 1)
Cells(i, 5).Value = Boters(i + 1, 1)
Next
End If
End Sub
Private Sub VSortM(ary, LB, UB, ref)
Dim M As Variant, i As Long, ii As Long, iii As Long, temp
i = UB: ii = LB
M = ary(Int((LB + UB) / 2), ref)
Do While ii <= i
Do While ary(ii, ref) < M
ii = ii + 1
Loop
Do While ary(i, ref) > M
i = i - 1
Loop
If ii <= i Then
For iii = LBound(ary, 2) To UBound(ary, 2)
temp = ary(ii, iii)
ary(ii, iii) = ary(i, iii): ary(i, iii) = temp
Next
ii = ii + 1: i = i - 1
End If
Loop
If LB < i Then VSortM ary, LB, i, ref
If ii < UB Then VSortM ary, ii, UB, ref
End Sub
My attempt at converting the Pairing Algorithm in GAS
function test() {
//Get values for Column A and Column B starting at Row 5
var ssMatch = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Pairings');
var bRange = ssMatch.getRange("A5:A").getValues();
var nBRange = ssMatch.getRange("B5:B").getValues();
//Determine length with data to exclude blansk
var bLast = bRange.filter(String).length;
var nBLast = nBRange.filter(String).length;
//Get values for boaters & nBoaters without blanks
var boaters = ssMatch.getRange(5,1,bLast).getValues();
var nBoaters = ssMatch.getRange(5,2,nBLast).getValues();
// Populate boaters & nBoaters arrays using random numbers
for (var i = 0; i < bLast; i++) {
boaters[i][1] = Math.random();
Logger.log(boaters);
}
for (var j = 0; j<nBLast; j++) {
nBoaters[j][1] = Math.random();
Logger.log(nBoaters);
}
vSortM (boaters, 1, bLast, 1);
vSortM (nBoaters, 1, nBLast, 1);
//Determine whether there are more boaters or non-boaters
var x = Min(bLast, nBLast);
//Write boater & nBoater values in Columns
//NEED SOME HELP HERE: Certain this isn't correct for GAS
Cells(1,4).Resize(x, 2);
Cells.CurrentRegion.ClearContents;
Cells.Columns(1).setValues(boaters);
Cells.Columns(2).setValues(nBoaters);
//If no more nBoaters, pair remaining unpaired boaters
if (x < bLast) {
for (var i = x + 1; i<bLast; i = i + 2) {
if (i + 1 > bLast) { break;}
else {
//THINK I DID THIS RIGHT, BUT NOT SURE
ssMatch.getRange(i,4).setValue(boaters[i][0]);
ssMatch.getRange(i,5).setValue(boaters[i+1][0]);
}
}
}
}
//Having some trouble converting this from VBA to GAS
// not sure how to deal with the ary parameter and m statement
function vSortM(ary, lB, uB, ref) {
var temp = 0;
var i = uB;
var ii = lB;
var m = [parseInt((lB + uB) / 2), ref];
while (ii <= i);{
while ([ii, ref] < m); {
ii++;
while ([i, ref] > m); {
i--;
}
if (ii <= i); {
for (var iii = 0; i<=(ary, 2);) {
temp = [ii, iii];
[ii, iii] = [i, iii];
[i, iii] = temp;
}
ii++;
i--;
}
}
if (lB < i) {
vSortM(ary, lB, i, ref);
}
if (ii < uB) {
vSortM(ary, ii, uB, ref);
}
}
}
First off, I have to agree with the others in the comments. The question is too broad. In https://stackoverflow.com/help/dont-ask it says that questions should be reasonably scoped.
From what I see in your code that you ask for help.
//NEED SOME HELP HERE: Certain this isn't correct for GAS
Cells(1,4).Resize(x, 2);
Cells.CurrentRegion.ClearContents;
Cells.Columns(1).setValues(boaters);
Cells.Columns(2).setValues(nBoaters);
This section is easy to figure out by using the very well written documentation for GAS (always refer to it first before going anywhere else). In GAS you are essentially working with classes (or objects if you will). Here you will want the sheet class to resize the row and column (2 seperate functions) and then a range class (which is retrieved from the sheet class) to clear and set values.
//THINK I DID THIS RIGHT, BUT NOT SURE
ssMatch.getRange(i,4).setValue(boaters[i][0]);
ssMatch.getRange(i,5).setValue(boaters[i+1][0]);
Depends on what you want to accomplish. The syntax here is correct, you set a value for a single cell. Keep in mind that in google sheets it's best to try and batch such calls. So instead of setting a value on a cell by cell basis, you would get a range from A1 to B20 and set all values at once with a 2D array.
Finally you need to clarify what it is the second functions has to do and what it doesn't do right. Perhaps share a minimal example sheet (read here about Minimal, Complete, and Verifiable example)?
Related
I have a set which has an unknown number of objects. I want to associate a label to each one of these objects. Instead of labeling each object with a number I want to label them with letters.
For example the first object would be labeled A the second B and so on.
When I get to Z, the next object would be labeled AA
AZ? then BA, BB, BC.
ZZ? then AAA, AAB, AAC and so on.
I'm working using Mapbasic (similar to VBA), but I can't seem to wrap my head around a dynamic solution. My solution assumes that there will be a max number of objects that the set may or may not exceed.
label = pos1 & pos2
Once pos2 reaches ASCII "Z" then pos1 will be "A" and pos2 will be "A". However, if there is another object after "ZZ" this will fail.
How do I overcome this static solution?
Basically what I needed was a Base 26 Counter. The function takes a parameter like "A" or "AAA" and determines the next letter in the sequence.
Function IncrementAlpha(ByVal alpha As String) As String
Dim N As Integer
Dim num As Integer
Dim str As String
Do While Len(alpha)
num = num * 26 + (Asc(alpha) - Asc("A") + 1)
alpha = Mid$(alpha, 2,1)
Loop
N = num + 1
Do While N > 0
str = Chr$(Asc("A") + (N - 1) Mod 26) & str
N = (N - 1) \ 26
Loop
IncrementAlpha = str
End Function
If we need to convert numbers to a "letter format" where:
1 = A
26 = Z
27 = AA
702 = ZZ
703 = AAA etc
...and it needs to be in Excel VBA, then we're in luck. Excel's columns are "numbered" the same way!
Function numToLetters(num As Integer) As String
numToLetters = Split(Cells(1, num).Address(, 0), "$")(0)
End Function
Pass this function a number between 1 and 16384 and it will return a string between A and XFD.
Edit:
I guess I misread; you're not using Excel. If you're using VBA you should still be able to do this will the help of an reference to an Excel Object Library.
This should get you going in terms of the logic. Haven't tested it completely, but you should be able to work from here.
Public Function GenerateLabel(ByVal Number As Long) As String
Const TOKENS As String = "ZABCDEFGHIJKLMNOPQRSTUVWXY"
Dim i As Long
Dim j As Long
Dim Prev As String
j = 1
Prev = ""
Do While Number > 0
i = (Number Mod 26) + 1
GenerateLabel = Prev & Mid(TOKENS, i, 1)
Number = Number - 26
If j > 0 Then Prev = Mid(TOKENS, j + 1, 1)
j = j + Abs(Number Mod 26 = 0)
Loop
End Function
I am banging my head against the wall for a while now trying different techniques.
None of them are working well.
I have two strings.
I need to compare them and get an exact percentage of match,
ie. "four score and seven years ago" TO "for scor and sevn yeres ago"
Well, I first started by comparing every word to every word, tracking every hit, and percentage = count \ numOfWords. Nope, didn't take into account misspelled words.
("four" <> "for" even though it is close)
Then I started by trying to compare every char in each char, incrementing the string char if not a match (to count for misspellings). But, I would get false hits because the first string could have every char in the second but not in the exact order of the second. ("stuff avail" <> "stu vail" (but it would come back as such, low percentage, but a hit. 9 \ 11 = 81%))
SO, I then tried comparing PAIRS of chars in each string. If string1[i] = string2[k] AND string1[i+1] = string2[k+1], increment the count, and increment the "k" when it doesn't match (to track mispellings. "for" and "four" should come back with a 75% hit.) That doesn't seem to work either. It is getting closer, but even with an exact match it is only returns 94%. And then it really gets screwed up when something is really misspelled. (Code at the bottom)
Any ideas or directions to go?
Code
count = 0
j = 0
k = 0
While j < strTempName.Length - 2 And k < strTempFile.Length - 2
' To ignore non letters or digits '
If Not strTempName(j).IsLetter(strTempName(j)) Then
j += 1
End If
' To ignore non letters or digits '
If Not strTempFile(k).IsLetter(strTempFile(k)) Then
k += 1
End If
' compare pair of chars '
While (strTempName(j) <> strTempFile(k) And _
strTempName(j + 1) <> strTempFile(k + 1) And _
k < strTempFile.Length - 2)
k += 1
End While
count += 1
j += 1
k += 1
End While
perc = count / (strTempName.Length - 1)
Edit: I have been doing some research and I think I initially found the code from here and translated it to vbnet years ago. It uses the Levenshtein string matching algorithm.
Here is the code I use for that, hope it helps:
Sub Main()
Dim string1 As String = "four score and seven years ago"
Dim string2 As String = "for scor and sevn yeres ago"
Dim similarity As Single =
GetSimilarity(string1, string2)
' RESULT : 0.8
End Sub
Public Function GetSimilarity(string1 As String, string2 As String) As Single
Dim dis As Single = ComputeDistance(string1, string2)
Dim maxLen As Single = string1.Length
If maxLen < string2.Length Then
maxLen = string2.Length
End If
If maxLen = 0.0F Then
Return 1.0F
Else
Return 1.0F - dis / maxLen
End If
End Function
Private Function ComputeDistance(s As String, t As String) As Integer
Dim n As Integer = s.Length
Dim m As Integer = t.Length
Dim distance As Integer(,) = New Integer(n, m) {}
' matrix
Dim cost As Integer = 0
If n = 0 Then
Return m
End If
If m = 0 Then
Return n
End If
'init1
Dim i As Integer = 0
While i <= n
distance(i, 0) = System.Math.Max(System.Threading.Interlocked.Increment(i), i - 1)
End While
Dim j As Integer = 0
While j <= m
distance(0, j) = System.Math.Max(System.Threading.Interlocked.Increment(j), j - 1)
End While
'find min distance
For i = 1 To n
For j = 1 To m
cost = (If(t.Substring(j - 1, 1) = s.Substring(i - 1, 1), 0, 1))
distance(i, j) = Math.Min(distance(i - 1, j) + 1, Math.Min(distance(i, j - 1) + 1, distance(i - 1, j - 1) + cost))
Next
Next
Return distance(n, m)
End Function
Did not work for me unless one (or both) of following are done:
1) use option compare statement "Option Compare Text" before any Import declarations and before Class definition (i.e. the very, very first line)
2) convert both strings to lowercase using .tolower
Xavier's code must be correct to:
While i <= n
distance(i, 0) = System.Math.Min(System.Threading.Interlocked.Increment(i), i - 1)
End While
Dim j As Integer = 0
While j <= m
distance(0, j) = System.Math.Min(System.Threading.Interlocked.Increment(j), j - 1)
End While
I have the following code as a sub in Excel 2010:
i = 2
For j = 1 To num_scenarios
Dim probdiff As Double
Dim OCS_Spend As Double
n = 0
For k = 1 To num_yrs
' These are the calculations and potentially not relevant to my question but here for context
For Each cell In rng
x = Rnd()
'Debug.Print Format(x, "0.00000%")
If cell.Value >= x Then
'Populate the result sheet
Sheets("Event Occurs").Cells(i, 1) = mywksht.Cells(cell.Row, 1)
Sheets("Event Occurs").Cells(i, 2) = mywksht.Cells(cell.Row, 2)
Sheets("Event Occurs").Cells(i, 3) = mywksht.Cells(cell.Row, 3)
Sheets("Event Occurs").Cells(i, 4) = mywksht.Cells(cell.Row, 4)
Sheets("Event Occurs").Cells(i, 5) = mywksht.Cells(cell.Row, 5)
Sheets("Event Occurs").Cells(i, 6) = mywksht.Cells(cell.Row, 6)
Sheets("Event Occurs").Cells(i, 10) = "Event Occurs"
Sheets("Event Occurs").Cells(i, 11) = mywksht.Cells(cell.Row, 11)
Sheets("Event Occurs").Cells(i, 9) = x
Sheets("Event Occurs").Cells(i, 7) = k
Sheets("Event Occurs").Cells(i, 8) = j
Sheets("Event Occurs").Cells(i, 14) = (cell.Value - x) ^ (2)
event_max = Sheets("Event Occurs").Cells(i, 11)
probdiff = probdiff + (cell.Value - x) ^ (2)
If Round(cell / x, 0) >= event_max Then
Sheets("Event Occurs").Cells(i, 12) = event_max
Else
Sheets("Event Occurs").Cells(i, 12) = Round(cell / x, 0)
End If
Duration = Sheets("Event Occurs").Cells(i, 4)
Num_Event = Sheets("Event Occurs").Cells(i, 12)
Spend = Sheets("Event Occurs").Cells(i, 5)
Sheets("Event Occurs").Cells(i, 13) = Num_Event * Spend / Duration
OCS_Spend = OCS_Spend + Num_Event * Spend / Duration
n = n + 1
i = i + 1
End If
Next cell
' End calculations
Next k
Debug.Print j, probdiff / n
probdiff = 0
OCS_Spend = 0
Next j
The output to the immediate window looks like this:
J: MSE:
1 0.194236476623154
2 0.157939130921924
3 0.19825548826238
4 0.384990330451172
5 0.267128221022187
The first column is j (the outer for loop) and represents a scenario. The second column is the mean square error of the data generated by each iteration of the outer j loop. So 1 is the first time the loop runs,2 is the second etc.. The smaller the number in column MSE, the more likely the scenario is to occur.
I want people to be able to limit the number of scenarios (j's) they see to only the most likely in the event they want to run 100 scenarios. So I need a way of sorting the table above to something like this
j: MSE
2 0.157939130921924
1 0.194236476623154
3 0.19825548826238
5 0.267128221022187
4 0.384990330451172
And if someone wanted to see only the top three results, it would be this:
j: MSE
2 0.157939130921924
1 0.194236476623154
3 0.19825548826238
So basically the three most likely out of 5 possible scenarios. I have tried collections and arrays but not dicitonaries (I am still learning how to use these and not sure if they exists in Excel VBA).
Chip Pearson provides a number of very useful functions which can sort arrays, collections, and dictionaries, which are available here:
http://www.cpearson.com/Excel/SortingArrays.aspx
There is too much code there to reproduce here. What I typically do when the need arises is to create a separate module in my VBProject which contains these array helper functions. I have used these extensively in PowerPoint and they worked in that environment with minimal modifications. For Excel, they should work out-of-the-box.
Once you have put the data in an array (I don't see any arrays in your code, so let's assume you have something like Dim MyArray As Variant), and sorted it using those functions, you can do something like this to cut the array down to include only the first x results:
'where "x" is a long/integer represents some user-input or _
limit to the number of results:
ReDim Preserve MyArray(x - 1)
I would use arrays rather than collections or dictionaries.
Why not Collections? Collections are useful and would arguably do the job, here. However, whereas we can "resize" the array in a single ReDim Preserve statement, you cannot do that with a Collection object; you would instead have to use iteration. While this is not overly complicated, it does seem a bit clunkier. (You could of course do some tests on performance, but unless you are dealing with very large sets of data, I would not expect a noticeable gain either way).
Sub testCollection()
Dim coll As New Collection
Dim i As Integer
For i = 1 To 10
coll.Add i
Next
Dim x As Integer 'The maximum number of results you want to return:
x = 4
Do Until coll.Count = x
coll.Remove (coll.Count)
Loop
End Sub
Why not dictionaries? While a dictionary's .Keys returns a one-dimensional array of values, in order to avoid iteration (like in the collection object) you would still need to transfer these to an array:
MyArray = dict.Keys()
ReDim Preserve MyArray(x-1)
Further, the dictionary object holds unique key values, so these are not good to use if you anticipate that there may be duplicate values that you need to store.
One option is to use a System.Collections.ArrayList since this object directly supports a Sort method. The Object is "borrowed" from VB.NET.
EDIT#1
Here is a sample:
Sub SortDemo()
s = Array("Larry", "Moe", "Curley", "Manny", "Zack", "Jack")
L = LBound(s)
U = UBound(s)
With CreateObject("System.Collections.ArrayList")
For k = L To U
.Add s(k)
Next k
.Sort
s = .toarray
End With
msg = ""
For k = L To U
msg = msg & s(k) & vbCrLf
Next k
MsgBox msg
End Sub
and here are the references in place:
For more information see:
Ozgrid Material
I have written code to implement an algorithm I found on string permutations. What I have is an arraylist of words ( up to 200) and I need to permutate the list in levels of 5. Basically group the string words in fives and permutated them. What I have takes the first 5 words generates the permutations and ignores the rest of the arraylist?
Any ideas appreciated.
Private Function permute(ByVal chunks As ArrayList, ByVal k As Long) As ArrayList
ReDim ItemUsed(k)
pno = 0
Permutate(k, 1)
Return chunks
End Function
Private Shared Sub Permutate(ByVal K As Long, ByVal pLevel As Long)
Dim i As Long, Perm As String
Perm = pString ' Save the current Perm
' for each value currently available
For i = 1 To K
If Not ItemUsed(i) Then
If pLevel = 1 Then
pString = chunks.Item(i)
'pString = inChars(i)
Else
pString = pString & chunks.Item(i)
'pString += inChars(i)
End If
If pLevel = K Then 'got next Perm
pno = pno + 1
SyncLock outfile
outfile.WriteLine(pno & " = " & pString & vbCrLf)
End SyncLock
outfile.Flush()
Exit Sub
End If
' Mark this item unavailable
ItemUsed(i) = True
' gen all Perms at next level
Permutate(K, pLevel + 1)
' Mark this item free again
ItemUsed(i) = False
' Restore the current Perm
pString = Perm
End If
Next
K above is = to 5 for the number of words in one permutation but when I change the for loop to the arraylist size I get an error of index out of bounds
Index out of bounds error usually happens when you start the loop from 1 to length. The the for loop as following.
For i = 0 to array.length - 1
You will get this error.
When you do
For i = 1 To K
The last value of i will be the size of your array.
chunks.Item(i)
Will crash when i equals the size of the array since the index starts at 0.
I would suggest you change your for loop to
For i = 0 To K - 1
Or you change the way you access the values in your arrays to
chunks.Item(i-1)
C++ Permutation
#include <stdio.h>
void print(const int *v, const int size)
{
if (v != 0)
{
for (int i = 0; i < size; i++)
{
printf("%4d", v[i] );
}
printf("\n");
}
} // print
void permute(int *v, const int start, const int n)
{
if (start == n-1) {
print(v, n);
}
else {
for (int i = start; i < n; i++) {
int tmp = v[i];
v[i] = v[start];
v[start] = tmp;
permute(v, start+1, n);
v[start] = v[i];
v[i] = tmp;
}
}
}
main()
{
int v[] = {1, 2, 3, 4};
permute(v, 0, sizeof(v)/sizeof(int));
}
Project Euler's problem 16:
2^(15) = 32768 and the sum of its digits is 3 + 2 + 7 + 6 + 8 = 26.
What is the sum of the digits of the number 2^(1000)?
I have been trying to do this problem for a few days now and i just can't figure out how to get vb.net 2008 to recognize anywhere near that large a number. I have seen in other posts that some software like java has the integer type BigNumber or BigInteger but i cant find anything like that in visual basic. I'm running into this problem a lot using Visual Basic. I also can't seem to find any of the standard upper level math features in visual basic such as factorials and a few others that i can't remember but couldn't find under the math feature. any suggestions? (Sorry let me rephrase, any suggestions on how to do this stuff without switching to a different programming language.)
here's the function I wrote, it's not so difficult to do your own implementation of a BigInteger purely for this purpose (very difficult to make it efficient and versatile however, but that's what libraries are for)
Public Shared Function Problem16(ByVal power As Integer) As String
Dim digits As Integer = CInt(Int(power * Log10(2)))
Dim number(digits) As Byte
number(digits) = 1
For i As Integer = 1 To power
Dim carry As Byte = 0
For j As Integer = digits To 0 Step -1
number(j) <<= 1
number(j) += carry
If number(j) > 9 Then
carry = number(j) \ CByte(10)
number(j) -= CByte(10)
Else
carry = 0
End If
Next
Next
Dim result As Integer
For i As Integer = 0 To digits
result += number(i)
Next
Return result.ToString
End Function
There are several BigInteger libraries that are freely available which you can use.
http://msdn.microsoft.com/en-us/magazine/cc163696.aspx
http://www.codeproject.com/KB/cs/biginteger.aspx?df=100&forumid=4524&exp=0&fr=26
In this case the limitations are not necessarily the language. Visual Basic, outside of basic math operations, largely depends on the BCL for functionality. This is true of most languages which run on the CLR (including C#). In most cases though, there are libraries available which you can use to augment the functionality of the framework.
I haven't tried it but there seems to be a Big Integer construct for Visual Basic.
public static void main(String[] args) {
int m = 2, ci = 1, n = 1000, i;
int[] arr = new int[n + 1];
arr[1] = 1;
for (i = 1; i <= n; i++) {
int carry = 0;
for (int j = 1; j <= ci; j++) {
arr[j] = arr[j] * m + carry;
carry = arr[j] / 10;
arr[j] = arr[j] % 10;
}
if (carry > 0) {
while (carry > 0) {
ci++;
arr[ci] = carry % 10;
carry = carry / 10;
}
}
}
int sum = 0;
System.out.println(ci + "\n \n ");
for (int j = ci; j > 0; j--) {
System.out.print(arr[j]);
sum = sum + arr[j];
}
System.out.println("\n \n " + sum);
}
Answer:
Number of digits in 2^1000: 302
2^1000=
10715086071862673209484250490600018105614048117055336074437503883703510511249361224931983788156958581275946729175531468251871452856923140435984577574698574803934567774824230985421074605062371141877954182153046474983581941267398767559165543946077062914571196477686542167660429831652624386837205668069376
sum of the digits: 1366