Russian Peasant Multiplication

The Daily WTF posted a challenge to code the Russian Peasant Multiplication. Here’s mine:

Function RussianPeasant(lFirst As Long, lSecond As Long) As Double
   
    Dim lDiv As Long
    Dim lMult As Long
    Dim lMod As Long
   
    lDiv = lFirst
    lMult = lSecond
   
    Do Until lDiv = 1
        Debug.Print lDiv, “x”, lMult
        If lDiv Mod 2 = 1 Then
            <strike>lMod = lMult</strike>
            lMod = lMod + lMult
        End If
       
        lDiv = lDiv 2
        lMult = lMult * 2
    Loop
   
    Debug.Print lMult, “+”, lMod
   
    RussianPeasant = lMult + lMod
   
End Function

The backward division sign means integer divison. I checked the first BASIC entry in the comments and it was done in five lines. Clearly I have some variable declarations, debugs, and white space that he doesn’t, but reading his code caused me to think about the problem in a slightly different way. He just aggregates lMult every time lDiv is odd, whereas I do the same thing just not as concisely.

Make Office Better

JP posted about Make Office Better, so I checked it out. First, I have to say that I love the design of the site. I didn’t know anyone at MS knew how to design clutter-free websites anymore. Maybe that could add a few more suggestions per page and get rid of the huge “about” section, but generally I’m impressed.

It’s fun to read what people want to change in the various Office programs. And it’s interesting to note how out of touch I am with the rest of the world. As I read the top 20, I found myself saying “I don’t care about that” or “That’s stupid” often. I did happen upon this little gem

And shortly thereafter, this one

There’s probably a bunch of suggestions that are all this suggestion and need to be combined. I did see one that piqued my interest, but I certainly wouldn’t waste any of my brain cycles on it because I’m convinced it will never happen.

Do something with the VBE
The VBE hasn’t changed much for 10 years. I’d like to see some commitment to VBA programmers – your treatment of us is a disgrace. If you’re going to phase out VBA, give us a USABLE alternative. Give us a roadmap at least.

Go have a look, make some suggestions, or cast some votes.

Quick Find Regular Expressions

I thought I’d add regular expressions to my Quick Find utility. I’ve used regular expressions about a dozen times in my life, so why not clutter up a simple tool with it. Now if I start my search string with rx: it will evaluate the rest as a regular expression.

My problem is when the pattern is invalid, like while I’m typing it in. For instance, when I type rx:([, the start of the above regular expression, it returns every cell in the workbook because I’ve ignored errors in the pattern matching. I suppose I could make it show no results instead of everything. Or maybe people who type in regular expressions will just expect crappy results until they’re done.

The above regular expression is supposed to match US phone numbers that are formatted like (999) 999-9999. Here’s how I think it works:

( matches an open paren. Since it’s a special character, it needs the backslash to escape its specialness
[2-9] match any digit 2 through 9
d{2} d means digit and {2} means 2, so match two consecutive digits
) Same as the open paren
space match a space
[2-9]d{2} same as above – match three digits where the first is between 2 and 9
match a dash
d{4} match four consecutive digits

I’m going to set up a special page for utilities and I’ll post the update add-in next week.

P.S. I realize I got the phone number wrong, but it wasn’t worth a new screen shot.

Euler Problem 205

Euler Problem 205 asks:

Peter has nine four-sided (pyramidal) dice, each with faces numbered 1, 2, 3, 4. Colin has six six-sided (cubic) dice, each with faces numbered 1, 2, 3, 4, 5, 6.

Peter and Colin roll their dice and compare totals: the highest total wins. The result is a draw if the totals are equal.

What is the probability that Pyramidal Pete beats Cubic Colin? Give your answer rounded to seven decimal places in the form 0.abcdefg

As a quick review, if we roll 2 of Colin’s dice, we expect 62 different outcomes. Rolling 6 dice will have 66 outcomes, or 46,656 different rolls.

Peter has 49 different outcomes, or 262,144 different rolls. Peter’s least roll (nine 1’s) will best the one way Colin can roll a 6 (six 1’s), the six ways he can roll a 7 (five 1’s and a 2 six times) and the twenty-one ways he can roll an 8 (a 3 and five 1’s six times, or two 2’s and four 1’s fifteen times). Peter’s meager 9 wins over 28 of Colin’s possible rolls. Peter’s 10, which he can roll 9 ways, bests 84 of Colin’s rolls.

VBA does not have a CEILING function, and I needed one for this problem. We could use Application.Worksheetfunction.Ceiling, but there is a quicker way execution-wise by a factor of 5. The INT function always rounds down. When the argument to INT is negative, INT rounds down or away from zero. INT(-3.14159) is -4, and -INT(-3.14159) is 4, rounding pi() up! Very useful when you need more area in your circles. It works this way in both the VBA and the spreadsheet implementations.

Easier than developing the usage for Problem 205, I’ll show it and explain how it works. The code we want to use for Colin is “-INT(-N/6^C) Mod 6” for C from zero to five, where N is the number of the roll (1 to 66), and when Mod 6 = zero, substitute 6. In a spreadsheet, this would be =IF(MOD(-INT(-N/6^C),6), MOD(-INT(-N/6^C),6), 6)

Remembering that 60 is 1, and 61 is 6, this is how the first four of Colin’s dice (C=0,1,2,3) look on Roll 66, N = 66, -N = -66.

  1. C = 0, Die 1:
    • INT(-66/6^0) = INT(-66/1) = -66
    • –66 = 66
    • 66 Mod 6 = 0, Return 6
  2. C = 1, Die 2:
    • INT(-66/6^1) = INT(-66/6) = -11
    • –11 = 11
    • 11 Mod 6 = 5, Return 5
  3. C = 2, Die 3:
    • INT(-66/6^2) = INT(-66/36) = INT(-1.83333) = -2
    • –2 = 2
    • 2 Mod 6 = 2, Return 2
  4. C = 3, Die 4:
    • INT(-66/6^3) = INT(-66/216) = INT(-0.30555) = -1
    • –1 = 1
    • 1 Mod 6 = 1, Return 1

Dice 5 and 6 (with C of 4 and 5) also return 1. Colin’s 66th roll is {6,5,2,1,1,1}. We do the same thing for Peter, where the code is “-INT(-N/4^P) Mod 4” for P from zero to eight, returning 4 when Mod 4 is zero. Peter’s 66th roll is {2,1,1,2,1,1,1,1,1}, summing 11. Peter gets 11 forty-five ways, on which he beats the 210 of Colin’s rolls (but not Colin’s #66) that sum 10 or below.

If we aggregate the number of times Colin sums from 6 to 36 in his 46,656 possible rolls, and the number of times Peter gets a particular sum from 9 to 36 in his 262,144 different rolls, we can then loop through Peter’s aggregation and see how many of Colin’s rolls lose to that number. If we then multiply that discovery by the number of ways Peter achives that aggregation, keep a grand sum of winners, and then divide by the product of (66)*(49),we will have our percentage of Peter’s winning. Format the answer to 7 decimals to the right. Format() will take care of the necessary rounding.

This is the code that does this. It runs in about 6ths of a second.

Sub Problem_205()
   Dim N As Long, TEMP As Long, Sum As Long
   Dim Answer As Double, T As Single, Count As Double
   Dim PP(1 To 36) As Long, P As Long  ‘Pyramidal Pete
  Dim CC(1 To 36) As Long, C As Long ‘Cubic Colin
  Dim LosersToPete As Long
 
   T = Timer
 
   For N = 1 To 6 ^ 6 ‘Cubic Colin
    Sum = 0
     For C = 0 To 5
        TEMP = -Int(-N / 6 ^ C) Mod 6
        If TEMP = 0 Then TEMP = 6
        Sum = Sum + TEMP
      Next C
      CC(Sum) = CC(Sum) + 1  ‘Incrementing Colin’s ways this value can happen
  Next N
 
   For N = 1 To 4 ^ 9 ‘Pyramidal Pete
     Sum = 0
      For P = 0 To 8
         TEMP = -Int(-N / 4 ^ P) Mod 4
         If TEMP = 0 Then TEMP = 4
         Sum = Sum + TEMP
      Next P
      PP(Sum) = PP(Sum) + 1 ‘Incrementing Pete’s ways this value can happen
  Next N
 
   For P = 9 To 36 ‘ Pete’s rolls
     LosersToPete = 0
      For C = 6 To P – 1
         LosersToPete = LosersToPete + CC(C) ‘ Num Colin’s rolls (all losses) below Pete’s roll
     Next C
      Count = Count + (LosersToPete * PP(P))  
      ‘Incrementing the winning Count by the # of ways Colin’s roll can lose to Pete
  Next P
 
   Answer = Count / (CDbl(4 ^ 9) * CDbl(6 ^ 6))
 
   Debug.Print Format(Answer, “0.0000000”);”  Time:”; Timer – T; Count
End Sub

If, instead of -INT, I use Application.Worksheetfunction.Ceiling as:

  • TEMP = Application.WorksheetFunction.Ceiling(N / 6 ^ C, 1) Mod 6 and
  • TEMP = Application.WorksheetFunction.Ceiling(N / 4 ^ P, 1) Mod 4

the runtime is 3.5 seconds! Using ROUNDUP() is even slower. The really wrong way to do this problem is to match each of Peter’s rolls with each of Colin’s, or something like this, where larger PP() and CC() now hold each roll and not the occurrances of each sum.

For P = 1 to 4^9
   For C = 1 to 6^6
      If PP(P) &gt; CC(C) then Count = Count + 1
   Next C
Next P

That’s 12,230,590,464 loops. Been there, did that. Takes 6 and a half minutes. No tee shirt.

…mrt

Quick Move

In my continuing series of tools to save myself literally seconds every day, I present Quick Move. See also QuickTTC, QuickPivot, and QuickFind.

Even though I’m a keyboard guy, I tend to move or copy sheets within a workbook using the mouse. I generally only use Edit – Move or Copy Sheets when I want to move to another workbook. So there’s no option here to move within the same workbook. Other non-features:

  • Select different workbook without navigating to drop down
  • Always puts sheet at the end
  • If it can’t move, it copies with no message
  • Copy is a button rather than a checkbox

You can download QuickMove.xla.zip. Access the dialog box via Edit – Quick Move.

As to my moving sheets quandry, I settled on this:

Private Sub MoveSheet(ByVal bNewWorkbook As Boolean, Optional ByRef shAfter As Object)
       
        Dim sh As Object
       
        On Error Resume Next
            ActiveSheet.Move
            Select Case Err.Number
                Case 1004
                    If LCase(Right$(ActiveSheet.Parent.Name, 4)) = “.csv” Or _
                        LCase(Right$(ActiveSheet.Parent.Name, 4)) = “.txt” Or _
                        Len(ActiveSheet.Parent.Path) = 0 Then
                       
                        Set sh = ActiveSheet
                        If bNewWorkbook Then
                            ActiveSheet.Copy
                        Else
                            ActiveSheet.Copy shAfter
                        End If
                        sh.Parent.Close False
                    Else
                        If bNewWorkbook Then
                            ActiveSheet.Copy
                        Else
                            ActiveSheet.Copy shAfter
                        End If
                    End If
                Case Else
                    MsgBox Err.Description
            End Select
        On Error GoTo 0
       
End Sub

If moving creates an error, it checks the file extension. CSV, text, and unsaved files are closed once the sheet is copied. For all other files, the sheet is just copied. A little less elegant than I hoped, but it’s tailored for exactly what I want it to do.

Moving Sheets

I’m working on a utility that requires that I move a worksheet. I thought it would be pretty easy, but now I’m rethinking it. Maybe it is easy and I’m just dense. I’ll walk you through my process and you can tell me what you think.

Step 1

Activesheet.Move

What could be simpler? Oh, there’s one little problem. When I first tested this code, the workbook containing the code was active, unsaved, and contained only one sheet. If you haven’t figured it out yet, I lost all of my code because the only worksheet was moved to another workbook and the unsaved original workbook was gone. I better save if that’s the case.

Step 2

If Not ActiveSheet.Parent.Saved And ActiveSheet.Parent.Sheets.Count = 1 Then
    ActiveSheet.Parent.Save
End If
   
ActiveSheet.Move

If it’s not saved and it’s one sheet, save it. Nice, but I don’t always want to save it. Maybe I should ask.

Step 3

Dim lResp As Long
 
If Not ActiveSheet.Parent.Saved And ActiveSheet.Parent.Sheets.Count = 1 Then
    lResp = MsgBox(“Save sheet first”, vbYesNoCancel)
    If lResp = vbYes Then
        ActiveSheet.Parent.Save
        ActiveSheet.Move
    ElseIf lResp = vbNo Then
        ActiveSheet.Move
    End If
End If

A nice clean utility is turning into a piece of crap. If it’s a previously unsaved workbook, this will save it in the current directly with the current name and in the default file format. I need to Save As if it’s previously unsaved. That means I need to present a save dialog and further muck things up.

Step 4

Dim lResp As Long
 
If Not ActiveSheet.Parent.Saved And ActiveSheet.Parent.Sheets.Count = 1 Then
    lResp = MsgBox(“Save sheet first”, vbYesNoCancel)
    If lResp = vbYes Then
        Application.CommandBars.FindControl(, 3).Execute
        ActiveSheet.Move
    ElseIf lResp = vbNo Then
        ActiveSheet.Move
    End If
End If

Executing the commandbarbutton (id 3 is the save button) will Save or SaveAs as appropriate. If I save it and then try to Move the only sheet, I get an error. Every saved workbook has to have at least one sheet. I’m doing this in a userform, so instead of all this trouble, I’m just going to disable the Move button when it’s illegal to move.

When is it illegal to move? If you have only one sheet in a workbook that’s been saved at some point. If it’s never been saved, you can move that one sheet and the original workbook goes away. So I’ll test for a Path (meaning it’s been saved) and I’ll count the sheets. Except that if it’s a CSV or a text file, I can move a single sheet and the original “workbook” simply closes. Maybe it’s more than just CSV and TXT, I don’t know.

At this point, I’m so far from my vision of a simple Move macro that it’s time to backup and rethink. I settle on:

Step 5

On Error Resume Next
    ActiveSheet.Move
    Select Case Err.Number
        Case 1004
            MsgBox “Can’t move last sheet in workbook”
        Case Else
            MsgBox Err.Description
    End Select
On Error GoTo 0

I stopped trying to anticipate the problem and just reported it. Much simpler.

The Big Bang Servo Diet

From Philip Greenspun

You draw a line from the current weight/date to the desired weight/date. Every morning you weigh yourself and plot the result. If the point is below the line, you eat whatever you want all day. If the point is above the line, you eat nothing but broccoli or some other low-calorie food.

The thing that strikes me about this is the use of graph paper. Had I written this post a few weeks ago, I might have remarked “Do they still make graph paper?” But I was in Dayton a few weeks ago surrounded by engineers. I asked for a legal pad and they gave me that tilted head, quizzical look. You know, like a dog might give you as if he understands you sometimes but not this time. All they had was Office Depot quad paper – 4 squares per inch on the front, 5 on the back. I didn’t mind taking notes on graph paper, except that the binding sucks and the pages all fell out.

Oh well, if you want to try the Steve Ward Diet for yourself, you can download WeightChart.xls.zip.

Click to embiggen

I wouldn’t eat broccoli on a bet. That’s not true, I recently did eat broccoli on a bet and it was terrible. I wouldn’t eat broccoli on a bet again, so I’d have to have some other low calorie food, like gin.

I suppose I could mix some Excel stuff into this post. Column C contains the formula =IF(A8=$B$2,$B$4,IF(A8=$B$3,$B$5,NA())) which puts the starting and ending weights on the proper dates. The rest of the dates get the NA() function because Excel dutifully ignores #N/A errors when charting. To simulate the actual recorded wieghts, I first added 1 to the starting weight in B9. This helps to ensure that some of the points get above the line. Then starting in B10, I use =IF(B9>B8,B9-1,B9+RANDBETWEEN(-1,1)/4) which adds or subtracts 1/4 pound every day, except that it forces a downward trend by subtracting 1 anytime it rises. Yeah, pretty hacky I know.

I’m a little unsure on what to do when the dot is touching the line.