TableMaker 2.0

Dick very recently put up a function that takes an Excel range and whips up some HTML to create a table. I loved that idea, and I asked if I could flesh it out. With Dick’s green light, I created a Sub() that captures each cell’s font family, font style, and font color, as well as the cell’s alignment and background color. It retains Dick’s option to use headers or not.

The sub spits the table to the clipboard. To get it there, in the VBE use Tools/References and check the Microsoft Forms 2.0 Object Library.

The table is a mixture of HTML and deprecated HTML (I’m not a purist.) I played with capturing the font size, but never liked how it came out so I commented it out. In creating HTML or CSS, many times you need to uses double-quotes (“) around the parameters. You can get by without it if the parameters are a single word, Arial for instance, but not for Times New Roman. Getting double-quotes in a text string requires you to escape them with another set of double quotes, creating double double-quotes (“”) and my eyes start to cross. I trick I use is to define a string*1 constant DQ equal to double double double-quotes (“”””). And then where I want quotes to appear in the HTML or CSS, I just concatenate in DQ. I used it throughout the Sub().

Public Sub MakeHTMLTable()

   Const DQ    As String * 1 = “”””   ‘double double double-quotes
   Dim DataObj As New MSForms.DataObject
   ‘Check VBE Tools/References Microsoft Forms 2.0 Object Library
   Dim rInput  As Range
   Dim rRow    As Range
   Dim rCell   As Range
   Dim sReturn As String
   Dim TextAlign As String
   Dim VertAlign As String
   Dim BgColor As String
   Dim FontColor As String
   Dim FontFace As String
   Dim CellContents As String
   Dim UseHeaders As Long
   Dim FontSize As Long
   Dim R As Long, C As Long
   Dim Red     As String
   Dim Green   As String
   Dim Blue    As String
   Dim TEMP    As Variant

   Set rInput = Selection
   R = rInput.Rows.Count
   C = rInput.Columns.Count

   UseHeaders = MsgBox(“Use Table Headers for your ” & R & “-row by ” & C & “-column table?”, _
                       vbYesNoCancel + vbQuestion, “DK’s Table Maker”)
   If UseHeaders = vbCancel Then Exit Sub

   sReturn = “.LT.table border=1 rules=all cellpadding=” & DQ & “5” & DQ & “.GT.”

   If UseHeaders = vbYes Then
      sReturn = sReturn & “.LT.tr.GT..LT.th bgcolor = #0055e5.GT. .LT./th.GT.”

      For Each rCell In rInput.Rows(1).Cells
         sReturn = sReturn & “.LT.th bgcolor = #0055e5 align=” & _
                   DQ & “center” & DQ & “.GT.” & “.LT.font face=” & _
                   DQ & “Arial” & DQ & “.GT.” & Chr$(rCell.Column + 64) & _
                   “.LT./font.GT..LT./th.GT.”
      Next rCell

      sReturn = sReturn & “.LT./tr.GT.” & vbNewLine
   End If

   For Each rRow In rInput.Rows
      sReturn = sReturn & “.LT.tr.GT.”

      If UseHeaders = vbYes Then
         sReturn = sReturn & “.LT.th bgcolor = #0055e5 align=” & _
                   DQ & “center” & DQ & “.GT.” & “.LT.font face=” & _
                   DQ & “Arial” & DQ & “.GT.” & rRow.Row & “.LT./font.GT..LT./th.GT.”
      End If

      For Each rCell In rRow.Cells

         CellContents = rCell.Text
         If Len(CellContents) = 0 Then CellContents = “ ”

         Select Case rCell.HorizontalAlignment
            Case xlGeneral
               TextAlign = “left”
               If IsNumeric(rCell.Value) Then TextAlign = “right”
               If IsError(rCell.Value) Then TextAlign = “center”
            Case xlLeft
               TextAlign = “left”
            Case xlCenter
               TextAlign = “center”
            Case xlRight
               TextAlign = “right”
            Case xlJustify
               TextAlign = “center”
         End Select

         FontFace = DQ & rCell.Font.Name & DQ
         ‘FontSize = rCell.Font.Size
         ‘If FontSize .LT. 12 Then FontSize = 12

         TEMP = rCell.Font.Color
         Red = Hex(TEMP And 255)
         Green = Hex(TEMP 256 And 255)
         Blue = Hex(TEMP 256 ^ 2 And 255)
         If Len(Red) = 1 Then Red = “0” & Red
         If Len(Green) = 1 Then Green = “0” & Green
         If Len(Blue) = 1 Then Blue = “0” & Blue
         FontColor = “#” & Red & Green & Blue

         TEMP = rCell.Interior.Color
         Red = Hex(TEMP And 255)
         Green = Hex(TEMP 256 And 255)
         Blue = Hex(TEMP 256 ^ 2 And 255)
         If Len(Red) = 1 Then Red = “0” & Red
         If Len(Green) = 1 Then Green = “0” & Green
         If Len(Blue) = 1 Then Blue = “0” & Blue
         BgColor = “#” & Red & Green & Blue

         sReturn = sReturn & “.LT.td align=” & TextAlign & _
                   ” bgcolor=” & BgColor & “.GT.”
         sReturn = sReturn & “.LT.font face=” & FontFace & _
                   ” color=” & FontColor & “.GT.”

         With rCell.Font
            If .Italic Then sReturn = sReturn & “.LT.i.GT.”
            If .Bold Then sReturn = sReturn & “.LT.b.GT.”
            If .Underline .LT..GT. xlNone Then sReturn = sReturn & “.LT.u.GT.”
            If .Strikethrough Then sReturn = sReturn & “.LT.strike.GT.”
            If .Subscript Then sReturn = sReturn & “.LT.sub.GT.”
            If .Superscript Then sReturn = sReturn & “.LT.sup.GT.”
         End With

         sReturn = sReturn & CellContents

         With rCell.Font   ‘in reverse order
            If .Superscript Then sReturn = sReturn & “.LT./sup.GT.”
            If .Subscript Then sReturn = sReturn & “.LT./sub.GT.”
            If .Strikethrough Then sReturn = sReturn & “.LT./strike.GT.”
            If .Underline .LT..GT. xlNone Then sReturn = sReturn & “.LT./u.GT.”
            If .Bold Then sReturn = sReturn & “.LT./b.GT.”
            If .Italic Then sReturn = sReturn & “.LT./i.GT.”
         End With

         sReturn = sReturn & “.LT./font.GT..LT./td.GT.”
      Next rCell
      sReturn = sReturn & “.LT./tr.GT.” & vbNewLine
   Next rRow

   sReturn = sReturn & “.LT./table.GT.”

   DataObj.SetText sReturn
   DataObj.PutInClipboard

End Sub

 
In the above replace the ampersand-amp-semicolon with an ampersand, the .LT. with < (35 times) and the .GT. with > (34 times.) The macro accurately reproduced a very ugly selected range as this double-ugly table:

  A B C D E F G H I
1 1 2 3 4 5 6 7 8 9
2 2 4 6 8 10 12 14 16 18
3 3 #N/A 9 12 15 18 21 24 27
4 4 8.00E+00 12 16 20 24 28.000 32 36
5 The quick brown fox jumps over the lazy dog!
6 6 12 18 24 30 36 42 48  

That table uses every color in the default Excel palette, and the fonts use most of them. The fonts in Row 5, from left to right, are:

  • Courier new
  • Times New Roman
  • Verdana
  • Comic Sans MS
  • Georgia
  • Tahoma
  • Trebuchet MS
  • Arial Black
  • Impact

There are assorted alignments and number formats sprinkled throughout. If the spreadsheet cell is empty, the macro puts a non-breaking space in the table as a placeholder. There is a considerable amount of bloat in the output, as it’s all done at the cell level. v3.0 will swap out the message-box interaction with a form that allows you to pick only what you are interested in. It’ll be out someday. Seeing how long it took MS to get Excel to edit at the character level, that’ll never be out.

Somethings I learned about WordPress: WordPress prefers the colors in #HEX format. When I used RBG, WordPress would wrap the RGB(r,g,b) in double quotes, and then not honor it! The fix was to use the HEX() function and a leading octothorpe. It turns out, WordPress wraps all the parameters, anyway. I don’t know how to capture the heading colors (I suspect it takes an API) so for now, whatever is the DDoE WordPress default (looks like a pale beige to me) is what you get here. (The sub’s code actually sets Windows Classic for the headers, but it gets overridden by CSS. Just as well–doesn’t really look like a window.) And I don’t think WordPress does subscript well when there’s no text to subordinate to. Cell C2 is specifed as superscript and E3 as subscript/strike-through. All I can say is that it’s clear on the spreadsheet.

…mrt:roll:

MatLab vs. Excel

The engineer who attempted teaching me MatLab used a short script to demonstrate its matrix manipulation prowess. MatLab (shortened from Matrix Laboratory) is built from the ground up to do matrix mathematics. I wanted to translate his script to VBA, to see, 1. If it’s possible, and 2. How Excel compares. This is the original MatLab script:

% fern.m        
%
% This is an exercise with MatLab Graphics and Flow Control.  The results
% should be a graphic drawn by an Iterated Function System.
%
% It takes about 2-3 minutes to complete the drawing
%
figure          % Establish a graphic window
pause(5.);      % An opportunity to re-size the Figure window
hold on         % Points accumulate in the window as an animation
x=[-5 0 5];     % 3 Initial points size the output axes
y=[0 1 10];
plot(x,y,’.g’,’MarkerSize’,4)
z = [x(2);y(2)];
%
pause(3.);     %  Pause before drawing
%
%  4 Matrices are key to the IFS Fractal description
%
fernM1 = [0 0
    0 0.16];
fernM2 = [0.2 -0.26
    0.23 0.22];
fernM3 = [-0.15 0.28
    0.26 0.24];
fernM4 = [0.85 0.04
    -0.04 0.85];
%
% Here is the fern
% We use tic/toc timing to measure execution time
%
tic
for kk = 1:20000
    dd = rand();
    if dd LTE 0.025
        z = fernM1 * z;
        plot(z(1),z(2),’.g’,’MarkerSize’,4)
        pause(0.01);        % Pause values less than 10ms makes no difference
    elseif (dd GT 0.025) &amp; (dd LTE 0.125)
        z = fernM2 * z + [0 ; 1.6];
        plot(z(1),z(2),’.g’,’MarkerSize’,4)
    elseif (dd GT 0.125) &amp; (dd LTE 0.225)
        z = fernM3 * z + [0 ; 0.44];
        plot(z(1),z(2),’.g’,’MarkerSize’,4)
    else
        z = fernM4 * z + [0 ; 1.6];
        plot(z(1),z(2),’.g’,’MarkerSize’,4)
    end
       
end
display(toc)
tic
for kk = 1:50000
    dd = rand();
    if dd LTE 0.025
        z = fernM1 * z;
        plot(z(1),z(2),’.g’,’MarkerSize’,4)
    elseif (dd GT 0.025) &amp; (dd LTE 0.125)
        z = fernM2 * z + [0 ; 1.6];
        plot(z(1),z(2),’.g’,’MarkerSize’,4)
    elseif (dd GT 0.125) &amp; (dd LTE 0.225)
        z = fernM3 * z + [0 ; 0.44];
        plot(z(1),z(2),’.g’,’MarkerSize’,4)
    else
        z = fernM4 * z + [0 ; 1.6];
        plot(z(1),z(2),’.g’,’MarkerSize’,4)
    end

end
display(toc)
display(‘The fern is done’)

 
Above, GT is “Greater Than”, LTE is “Less Than or Equals” and &amp is an ampersand. Here is my translation.

Sub Fern()
   Dim x(1 To 1, 1 To 3) As Double
   Dim y(1 To 1, 1 To 3) As Double
   Dim z()     As Variant
   Dim FernM1(1 To 2, 1 To 2) As Double
   Dim FernM2(1 To 2, 1 To 2) As Double
   Dim FernM3(1 To 2, 1 To 2) As Double
   Dim FernM4(1 To 2, 1 To 2) As Double
   Dim kk As Long, dd As Double
   Dim ZXArray(1 To 32000, 1 To 1) As Double
   Dim ZYArray(1 To 32000, 1 To 1) As Double
   Dim Fern As New Chart

   ReDim z(1 To 2, 1 To 1) As Variant
   x(1, 1) = -5: x(1, 2) = 0: x(1, 3) = 5
   y(1, 1) = 0: y(1, 2) = 1: y(1, 3) = 10
   z(1, 1) = x(1, 2)
   z(2, 1) = y(1, 2)

   FernM1(1, 1) = 0: FernM1(1, 2) = 0
   FernM1(2, 1) = 0: FernM1(2, 2) = 0.16

   FernM2(1, 1) = 0.2: FernM2(1, 2) = -0.26
   FernM2(2, 1) = 0.23: FernM2(2, 2) = 0.22

   FernM3(1, 1) = -0.15: FernM3(1, 2) = 0.28
   FernM3(2, 1) = 0.26: FernM3(2, 2) = 0.24

   FernM4(1, 1) = 0.85: FernM4(1, 2) = 0.04
   FernM4(2, 1) = -0.04: FernM4(2, 2) = 0.85

   For kk = 1 To 32000
      dd = Rnd()
      If dd LTE 0.025 Then
         z = Application.WorksheetFunction.MMult(FernM1, z)
      ElseIf dd GT 0.025 And dd LTE 0.125 Then
         z = Application.WorksheetFunction.MMult(FernM2, z)
         z(2, 1) = z(2, 1) + 1.6
      Elseif dd GT 0.125 And dd LTE 0.225 Then
         z = Application.WorksheetFunction.MMult(FernM3, z)
         z(2, 1) = z(2, 1) + 0.44      
      Else
         z = Application.WorksheetFunction.MMult(FernM4, z)
         z(2, 1) = z(2, 1) + 1.6
      End If
      ZXArray(kk, 1) = z(1, 1)
      ZYArray(kk, 1) = z(2, 1)
   Next kk
   Worksheets(“Sheet1”).Range(“A1:A32000”) = ZXArray
   Worksheets(“Sheet1”).Range(“B1:B32000”) = ZYArray

   Application.ScreenUpdating = False
   Set Fern = Charts.Add
   With Fern
      .ChartType = xlXYScatter
      .SetSourceData Source:=Sheets(“Sheet1”).Range(“A1:B32000”), PlotBy:=xlColumns
      .Location Where:=xlLocationAsNewSheet, Name:=”Fern”
      .Legend.Delete
      With .SeriesCollection(1)
         .MarkerBackgroundColorIndex = 10
         .MarkerForegroundColorIndex = 10
         .MarkerStyle = xlDiamond
         .Smooth = False
         .MarkerSize = 2
         .Shadow = False
      End With
      With .PlotArea
         .Border.LineStyle = xlNone
         With .Interior
            .ColorIndex = 2
            .PatternColorIndex = 1
            .Pattern = xlSolid
         End With
      End With
      With .Axes(xlCategory)
         .HasMajorGridlines = False
         .HasMinorGridlines = False
         .MajorTickMark = xlNone
         .MinorTickMark = xlNone
         .TickLabelPosition = xlNone
      End With
      With .Axes(xlValue)
         .HasMajorGridlines = False
         .HasMinorGridlines = False
         .MajorTickMark = xlNone
         .MinorTickMark = xlNone
         .TickLabelPosition = xlNone
         .MinimumScale = 0
         .MaximumScale = 10
         .MinorUnitIsAuto = True
         .MajorUnit = 2
         .Crosses = xlAutomatic
         .ReversePlotOrder = False
         .ScaleType = xlLinear
         .DisplayUnit = xlNone
         .Border.LineStyle = xlNone
      End With
   End With
   Application.ScreenUpdating = True
   
End Sub

 
Again, GT is “Greater Than” and LTE is “Less Than or Equals”. Make the substitutions after you paste in the code to a new workbook. We have to do this or the WordPress parser treats them as HTML tags and doesn’t present the complete information, trying as it does to helpfully shorten everyone’s code.

Some observations: You don’t dimension variables in MatLab; and case matters, with x being a different variable from X. MatLab can plot at least 70,000 points, while Excel is limited to 32,000. The Excel limit is supposedly a series limit, but I could never plot two series totaling over 32K points. MatLab doesn’t need a spreadsheet to plot, so if there’s a way in Excel to plot values without going through a spreadsheet, would someone please so describe. I don’t think you can. As expected, the Sheet1 ranges are the same dimensions as the ZX and ZY arrays, which are at the series limits, and allows copying the arrays to the spreadsheet.

Matrix Multiplication in VBA requires dimensioning the VB arrays as if they were cell arrays so that the worksheet function MMULT() can be used. Z() is a variant re-dimed to the dimensions of the matrix multiplication, meeting the specifications MMULT() requires in a spreadsheet–two rows by one column. I can’t make the code run on a Mac, where we’re stuck at VBA5. The Mac won’t accept the assignment of the array.

And finally, while it’s less than half the points, it’s much less than half the time to run. It’s almost instantaneous on my machine. That may mean the MatLab script is not optimum. I’ll pass on along any observations MatLab veterans may make.

Give it a try. You’ll see why it was chosen as a demo. It’s worth the trouble. A picture is worth one kilo-word, and you’ll have it in a blink.

…mrt

Left Looking Lookups

OR: The problem with VLOOKUP. Wikipedia gives us a table of birthstones, and I think just because it’s Wikipedia, there’s a trailing space after every entry. Pasted into a spreadsheet, the table looks like this:

  A B C D E
1 Month  Traditional Birthstone(s)  Modern Birthstone(s)  Mystical Birthstone  Ayurvedic Birthstone 
2 January  Garnet  Garnet  Emerald  Garnet 
3 February  Amethyst  Amethyst  Bloodstone  Amethyst 
4 March  Bloodstone, Jasper  Aquamarine  Jade  Bloodstone 
5 April  Diamond, Sapphire  Diamond  Opal  Diamond 
6 May  Emerald, Agate  Emerald  Sapphire  Agate 
7 June  Alexandrite, Emerald  Moonstone, Pearl  Moonstone  Pearl 
8 July  Ruby, Onyx  Ruby  Ruby  Ruby 
9 August  Sardonyx, Carnelian  Peridot  Diamond  Sapphire 
10 September  Sapphire, Peridot  Sapphire  Agate  Moonstone 
11 October  Tourmaline, Aquamarine  Opal, Tourmaline  Jasper  Opal 
12 November  Citrine, Topaz  Topaz, Citrine  Pearl  Topaz 
13 December  Zircon, Ruby  Turquoise, Blue Topaz  Onyx  Ruby 

 
(Only there for completeness, the mystical stones have Tibetan origin, and the Ayurvedic stones have Indian sub-continent origin.) If you want to know the modern birthstone for April, you might use the VLOOKUP() function, probably as follows:

  • =VLOOKUP(“April”,A1:E13,3,FALSE)

“April” for the month, A1:E13 as the array to search, 3 as the column from which to return, and FALSE because Months are not in an alphabetic sort and you want an exact match. If you do, you’ll get a #N/A error, indicating no match. That’s because of Wiki’s trailing space after April in A:A. Lets define a name: Birthstones =Sheet!$A$1:$E$13. Proper syntax could be:

  • =VLOOKUP(“April “,A1:E13,3,FALSE)
  • =VLOOKUP(“April “,Birthstones,3,FALSE)
  • =VLOOKUP(“April”&”*”,A1:E13,3,FALSE)
  • =VLOOKUP(“April”&”*”,Birthstones,3,FALSE)

All Return “Diamond “. Note the added wildcard asterisk in the last examples. You can use wildcards in FALSE VLOOKUPs, and you can do it front and back: =VLOOKUP(“*”&”April”&”*”,Birthstones,3,FALSE). But what if you want to know which month has the modern birthstone of Opal? You can’t use VLOOKUP() at all, because it only looks to the right, and the first column is the one searched. When I first encountered this limitation, my fix would be to add a column F:F to the right edge equal to A:A, and look right. It works, but there is a much better way. Lets define some more names:

  • Month =Sheet1!$A$1:$A$13
  • Traditional =Sheet1!$B$1:$B$13
  • Modern =Sheet1!$C$1:$C$13
  • Headings =Sheet1!$A$1:$E$1

The MATCH() function searches a 1*n or a n*1 array and returns the 1-based number where the match is found. It takes a third argument which gives the match type. When the match type argument is 0 (zero), MATCH() looks for exact matches, and can take wildcards, functioning akin to VLOOKUP() when set FALSE. Looking for Opal in the modern birthstones could be:

  • =MATCH(“*”&”Opal”&”*”,C1:C13,0)
  • =MATCH(“*”&”Opal”&”*”,Modern,0)

The INDEX() function specifies an array and specifies how far down to go within which column of the array, and doesn’t care left or right. Remembering that MATCH() returns a number, the syntax could be:

  • =INDEX(A1:E13,11,1) Opal being the 11th entry in C1:C13, and we thus want the 11th item from Column 1 (A1:A13)
  • =INDEX(A1:E13,MATCH(“*”&”Opal”&”*”,Modern,0),1)
  • =INDEX(Birthstones,MATCH(“*”&”Opal”&”*”,Modern,0),1)

All done knowing the Month is in Column 1. But you don’t really need to know even that:

  • =INDEX(Birthstones,MATCH(“*”&”Opal”&”*”,Modern,0),MATCH(“*”&Month&”*”,Headings,0))
  • =INDEX(Month,MATCH(“*”&”Opal”&”*”,Modern,0)) with no need to specify column.

Will return “October ” no matter where the Month column is. INDEX() and MATCH() work so powerfully together that I rarely use VLOOKUP() at all. Left or Right doesn’t matter.

…mrt

Building a self-sorting list (Part 2)

In Part 1 we ended up with Column D, a sorted list. One criticism was the many times we were counting the numbers in a column. We should improve it and only count once. Via Insert/Name/Define define Count_BB to =COUNT(Sheet1!$B:$B), and then select Columns C:D, and “Replace All” COUNT(B:B) with Count_BB. Column D should look something like this:

  D E
1 Aart Moonhammer =D1
2 Aart Moonhammer =IF(D2<>D1,D2,””)
3 Aart the Millwright
4 Aart Whiteson
5 Aberri of the White Heart
6 Aberri the Thieving Wizard
7 Acennan Badgerrunner
8 Acennan Kenricsson
9 Adei of the Red Ruins
10 Adolphus Blackbird
11 Adolphus Blackbird
12 Adolphus Stillearth
13 Adrik Yakovovich
14 Aethelred Awierganson
15 Aethelred Darkseed
16 Ageio the Fastidious Sorceror

 
In Cell E1 we simply move D1 over. In E2 we check to see if D2 is not equal to the cell above it, or in other words D2 starts a new run of names. If it is equal (FALSE condition) we put empty text. Otherwise, we put D2. Fill down from E2 as far down as columns B, C, and D are filled. Column E now looks like this. We have removed the duplicates.

  D E F
1 Aart Moonhammer Aart Moonhammer =IF(LEN(E1)>0,ROW(),””)
2 Aart Moonhammer  
3 Aart the Millwright Aart the Millwright
4 Aart Whiteson Aart Whiteson
5 Aberri of the White Heart Aberri of the White Heart
6 Aberri the Thieving Wizard Aberri the Thieving Wizard
7 Acennan Badgerrunner Acennan Badgerrunner
8 Acennan Kenricsson Acennan Kenricsson
9 Adei of the Red Ruins Adei of the Red Ruins
10 Adolphus Blackbird Adolphus Blackbird
11 Adolphus Blackbird  
12 Adolphus Stillearth Adolphus Stillearth
13 Adrik Yakovovich Adrik Yakovovich
14 Aethelred Awierganson Aethelred Awierganson
15 Aethelred Darkseed Aethelred Darkseed
16 Ageio the Fastidious Sorceror Ageio the Fastidious Sorceror

 
In F1 we check to see if there is anything of length in E1. If there is, put the row number, otherwise put empty text. As above, define Count_FF as =COUNT(Sheet1!$F$F). After filling down, Column F looks like this:

  D E F G
1 Aart Moonhammer Aart Moonhammer 1 =IF(ROW()<=Count_FF,SMALL(F:F,ROW()),””)
2 Aart Moonhammer  
3 Aart the Millwright Aart the Millwright 3
4 Aart Whiteson Aart Whiteson 4
5 Aberri of the White Heart Aberri of the White Heart 5
6 Aberri the Thieving Wizard Aberri the Thieving Wizard 6
7 Acennan Badgerrunner Acennan Badgerrunner 7
8 Acennan Kenricsson Acennan Kenricsson 8
9 Adei of the Red Ruins Adei of the Red Ruins 9
10 Adolphus Blackbird Adolphus Blackbird 10
11 Adolphus Blackbird  
12 Adolphus Stillearth Adolphus Stillearth 12
13 Adrik Yakovovich Adrik Yakovovich 13
14 Aethelred Awierganson Aethelred Awierganson 14
15 Aethelred Darkseed Aethelred Darkseed 15
16 Ageio the Fastidious Sorceror Ageio the Fastidious Sorceror 16

 
In G1 we again test the row number, but this time it’s against the count of numbers in F:F. If the ROW() is less than or equal to Count_FF, put the numbers from F:F there is ROW() order. Fill down as before. Column G looks like this:

  D E F G H
1 Aart Moonhammer Aart Moonhammer 1 1 =IF(ROW()<=Count_FF,INDEX(E:E,G1,1),””)
2 Aart Moonhammer   3
3 Aart the Millwright Aart the Millwright 3 4
4 Aart Whiteson Aart Whiteson 4 5
5 Aberri of the White Heart Aberri of the White Heart 5 6
6 Aberri the Thieving Wizard Aberri the Thieving Wizard 6 7
7 Acennan Badgerrunner Acennan Badgerrunner 7 8
8 Acennan Kenricsson Acennan Kenricsson 8 9
9 Adei of the Red Ruins Adei of the Red Ruins 9 10
10 Adolphus Blackbird Adolphus Blackbird 10 12
11 Adolphus Blackbird   13
12 Adolphus Stillearth Adolphus Stillearth 12 14
13 Adrik Yakovovich Adrik Yakovovich 13 15
14 Aethelred Awierganson Aethelred Awierganson 14 16
15 Aethelred Darkseed Aethelred Darkseed 15 18
16 Ageio the Fastidious Sorceror Ageio the Fastidious Sorceror 16 19

 
Last Step. In H1 we again compare the row number to the count of numbers in F:F. If ROW() is less than or equal to the Count_FF, then index E:E (could also be D:D) the number of rows shown in G:G. Fill down as before. Column H looks like this:

  D E F G H
1 Aart Moonhammer Aart Moonhammer 1 1 Aart Moonhammer
2 Aart Moonhammer   3 Aart the Millwright
3 Aart the Millwright Aart the Millwright 3 4 Aart Whiteson
4 Aart Whiteson Aart Whiteson 4 5 Aberri of the White Heart
5 Aberri of the White Heart Aberri of the White Heart 5 6 Aberri the Thieving Wizard
6 Aberri the Thieving Wizard Aberri the Thieving Wizard 6 7 Acennan Badgerrunner
7 Acennan Badgerrunner Acennan Badgerrunner 7 8 Acennan Kenricsson
8 Acennan Kenricsson Acennan Kenricsson 8 9 Adei of the Red Ruins
9 Adei of the Red Ruins Adei of the Red Ruins 9 10 Adolphus Blackbird
10 Adolphus Blackbird Adolphus Blackbird 10 12 Adolphus Stillearth
11 Adolphus Blackbird   13 Adrik Yakovovich
12 Adolphus Stillearth Adolphus Stillearth 12 14 Aethelred Awierganson
13 Adrik Yakovovich Adrik Yakovovich 13 15 Aethelred Darkseed
14 Aethelred Awierganson Aethelred Awierganson 14 16 Ageio the Fastidious Sorceror
15 Aethelred Darkseed Aethelred Darkseed 15 18 Agoztar of the Ghost Face
16 Ageio the Fastidious Sorceror Ageio the Fastidious Sorceror 16 19 Aide of the Dead Woods

 
The list that started in A:A is now sorted, de-duplicated, and collapsed to unique values, ready in H:H for whatever you might need as you write your fantasy novel. All you need to remember is to fill B2:Hn down well beyond any possible extent of A:A. This can be extended to handle several field records by indexing the appropriate columns at the two appropriate points.

…mrt

You can download self_sorting_names.zip

Building a self-sorting list

I haven’t posted in a good while. I’ve done over half the Euler problems, but haven’t had the time to research the high-numbered ones considering I might even understand them. Now that I’ve retired from my second career, and my third is only part-time, maybe I’ll get back to it. Today is something perhaps more useful…to have a list of names, specifications, part numbers, etc and have them self-sort, de-duplicate, and collapse without ever having to go to the Sort Menu. Add a member to the bottom, and it flows through. Replace or paste over the data, and Excel turns the crank. Unique values appear, and no VBA is involved. This was a monthly chore in Job #2 for sometimes as many as 8000 email addresses.

Assume in Column A are 1000 (fantasy) names, in random order. These names were generated by using this website four times. Rice University and Dr. Chris Brown have provided name generation tools for many spoken languages. Your Column A might look something like this:

  A B
1 Alfred Sundagger =IF(LEN(A1)>0,COUNTIF(A:A,”<“&A1)+COUNTIF($A$1:$A1,”=”&A1),””)
2 Aki Stonesaber =IF(LEN(A2)>0,COUNTIF(A:A,”<“&A2)+COUNTIF($A$1:$A2,”=”&A2),””)
3 Yrre the Carver
4 Alfred Birchleaf
5 Olaf the Dagger
6 Isen Grimboldson
7 Eagle Arianson
8 Besyrwan Odonson
9 Onund Boarherder
10 Berdoi of the Radiant Face
11 Azhar Fahim
12 Osric the Hostler
13 Yasha the Hare
14 Warian Pikethrower
15 Konrad Firelash
16 Faran Stillstoke

 

The formula in B1 tests if there in anything of length in A1. I’ve found that the LEN() function always returns a value that agrees with my ol’ Mark 1 Mod 0 eyeballs, and empty text doesn’t mislead me. If there is something of length in A1, then COUNTIF() everything in A:A that is less than A1. This uses Excel’s lexicographic algorithms, and that opinion may differ from what you want, but I’ve never had a problem with it. Add to that count using mixed references the COUNTIF() of everything between $A$1 and $A1 inclusive that equals $A1. When we fill down this will become COUNTIF($A$1:$A2,”=”&A2) in A2. Otherwise, if there is nothing of length, place empty text (“”-double double quotes). Fill down well below the extent of values in A:A.

Column B now looks something like this:

  A B C
1 Alfred Sundagger 43 =IF(ROW()<=COUNT(B:B),SMALL(B:B,ROW()),””)
2 Aki Stonesaber 25
3 Yrre the Carver 971
4 Alfred Birchleaf 39
5 Olaf the Dagger 660
6 Isen Grimboldson 470
7 Eagle Arianson 240
8 Besyrwan Odonson 144
9 Onund Boarherder 683
10 Berdoi of the Radiant Face 136
11 Azhar Fahim 110
12 Osric the Hostler 693
13 Yasha the Hare 960
14 Warian Pikethrower 944
15 Konrad Firelash 541
16 Faran Stillstoke 303

 

The formula in C1 tests if the row number is less than or equal to the count of the numbers in Column B. If it is, put the the smallest number there from Column B:B in ROW() order, otherwise put empty text. After filling down as far as in Column B, Column C is just 1 through the extent of your data in Column A. The use of SMALL() is what accommodates blank cells in A:A.

  A B C D
1 Alfred Sundagger 43 1 =IF(ROW()<=COUNT(B:B),INDEX(A:A,MATCH(C1,B:B,0),1),””)
2 Aki Stonesaber 25 2
3 Yrre the Carver 971 3
4 Alfred Birchleaf 39 4
5 Olaf the Dagger 660 5
6 Isen Grimboldson 470 6
7 Eagle Arianson 240 7
8 Besyrwan Odonson 144 8
9 Onund Boarherder 683 9
10 Berdoi of the Radiant Face 136 10
11 Azhar Fahim 110 11
12 Osric the Hostler 693 12
13 Yasha the Hare 960 13
14 Warian Pikethrower 944 14
15 Konrad Firelash 541 15
16 Faran Stillstoke 303 16

 

The formula in D1 tests the ROW() against the COUNT() again, and if ROW() is less than or equal to the COUNT(), specify zero as the third argument and find the exact MATCH() of the number in Column C:C within Column B:B, and then INDEX down Column A:A that far and return that result. Otherwise, put empty text. Fill down as far as in Columns B and C. After filling down, the list is sorted, and might look something like this:

  A B C D
1 Alfred Sundagger 43 1 Aart Moonhammer
2 Aki Stonesaber 25 2 Aart Moonhammer
3 Yrre the Carver 971 3 Aart the Millwright
4 Alfred Birchleaf 39 4 Aart Whiteson
5 Olaf the Dagger 660 5 Aberri of the White Heart
6 Isen Grimboldson 470 6 Aberri the Thieving Wizard
7 Eagle Arianson 240 7 Acennan Badgerrunner
8 Besyrwan Odonson 144 8 Acennan Kenricsson
9 Onund Boarherder 683 9 Adei of the Red Ruins
10 Berdoi of the Radiant Face 136 10 Adolphus Blackbird
11 Azhar Fahim 110 11 Adolphus Blackbird
12 Osric the Hostler 693 12 Adolphus Stillearth
13 Yasha the Hare 960 13 Adrik Yakovovich
14 Warian Pikethrower 944 14 Aethelred Awierganson
15 Konrad Firelash 541 15 Aethelred Darkseed
16 Faran Stillstoke 303 16 Ageio the Fastidious Sorceror

 

Next post we’ll de-dupe the list and then collapse it to unique entries.

…mrt

Euler Problem 188

Euler Problem 188 asks:

The hyperexponentiation or tetration of a number a by a positive integer b, denoted by a^^b or ba, is recursively defined by:

a^^1 = a,
a^^(k+1) = a(a^^k).

Thus we have e.g. 3^^2 = 33 = 27, hence 3^^3 = 327 = 7625597484987 and 3^^4 is roughly 103.6383346400240996*10^12.

Find the last 8 digits of 1777^^1855.

Euler uses double up-arrows for hyperexponentiation. I substituted double carets as a “reasonable facsimile.” Tetration is covered by this Wikipedia article. A key point is to note that tetration is not associative, and we must evaluate the expression from right to left (top to bottom).

This is the recursive version:

Function HyperExp(a As Double, k As Double) As Double
   If k = 0 Then
      HyperExp = 1
      Exit Function
   ElseIf k = 1 Then
      HyperExp = a
      Exit Function
   End If
 
   HyperExp = a ^ HyperExp(a, k – 1)
 
End Function

This works fine, but it won’t handle 1777^^1885. Python has a Pow(b,e,m) function that returns base b raised to exponent e modulo m.

This is what we want to duplicate, particularly since returning the last 8 digits in to the same as modulo 108. Here is the VBA translation of Pow(b,e,m):

Public Function Pow(b As Variant, e As Variant, m As Variant) As Long
‘pow(base,exponent,modulus): b^e mod m
‘That works as long as (m-1)^2 fits into your integer type.    
  Dim a As Variant, x As Variant
   If e = 0 Then
      Pow = 1
      Exit Function
   End If
   a = CDec(1)
   x = CDec(b – m * Int(b / m))   ‘b Mod m
  While (e GT 1)
      If e And 1 Then a = a * x – m * Int(a * x / m)  ‘If odd e then ax Mod m
     x = x * x – M * Int(x * x / M)   ‘x^2 Mod m
     e = BitShift(e, 1)
   Wend
   Pow = a * x – m * Int(a * x / m)   ‘ax Mod m
End Function

I used decimal variants, so this will work for m-1 up to the square root of ~7.92e29, or about ~8.9e14. Big enough. BitShift in this case is integer division by 32. Here are those functions:

Public Function BitShift(ByVal value As Long, ByVal shift As Integer) As Long
‘Right shift positive, left shift negative
  If shift GT 0 Then
      BitShift = shr(value, shift)
   Else
      BitShift = shl(value, -shift)
   End If
End Function
 
Public Function shr(ByVal value As Long, ByVal shift As Byte) As Long
‘http://www.excely.com/excel-vba/bit-shifting-function/
‘Right shifting is equal to dividing Value by 2^Shift.
  Dim i As Byte
   shr = value
   If shift GT 0 Then
      shr = Int(shr / (2 ^ shift))
   End If
End Function
 
Public Function shl(ByVal value As Long, ByVal shift As Byte) As Long
‘http://www.excely.com/excel-vba/bit-shifting-function/
‘Left shifting is equal to multiplying Value by 2^Shift. But to avoid an overflow error we use small trick:
  shl = value
   If shift GT 0 Then
      Dim i As Byte
      Dim M As Long
      For i = 1 To shift
         M = shl And &amp;H40000000   ‘ save 30th bit
        shl = (shl And &amp;H3FFFFFFF)   ‘ clear 30th and 31st bits
        shl = shl * 2   ‘ multiply by 2
        If M  0 Then
            shl = shl Or &amp;H80000000   ‘ set 31st bit
        End If
      Next i
   End If
End Function

The usual angle brackets substitutions are above. Altogether then, this is the code for Problem 188:

Sub Problem_188()
 
   Const a As Long = 1777
   Dim i As Long
   Dim Answer As Long, T As Single
 
   T = Timer
 
   Answer = 1
 
   For i = 1855 To 1 Step -1
      Answer = Pow(a, Answer, 10 ^ 8)
   Next i
 
   Debug.Print Answer; ”  Time:”; Timer – T
 
End Sub

Simple enough, but a lot of homework for this one. It put some more tools in the toolbox, and runs in less than 1/10 of a second.

..mrt

Summing the Digits of a Number

Summing the digits of a number is a chore I’ve been doing alot lately. Originally I’d parse the number out over the columns. And since SUM() ignores text, I’d turn the characters into digits by applying an arithmatic identity operation, like this:

  • =- -MID($A10,COLUMN(),1)

That’s double minus signs before the MID() function. The reasons for picking that identity operation are here at XLDYNAMIC’s website, about half-way down.

And filling right. But if the numbers were of uneven length, filling down would throw a #VALUE! error for all but the longest number. Contrary to what the Help advises, I find that SUM() does not ignore error values. So I was ending up with this formula so I could fill down:

  • =IF(ISERR(–MID($A10,COLUMN(),1)),0,–MID($A10,COLUMN(),1))

That’s double ugly, and a cell-eater to boot. I did a Google search and found Microsoft Knowledge Base article 214053 on this topic. Here’s what it says:

Formula 1: Sum the Digits of a Positive Number
To return the sum of the digits of a positive number contained in cell A10, follow these steps:

  1. Start Excel 2000.
  2. Type 123456 in cell A10.
  3. Type the following formula in cell B10:
    =SUM(VALUE(MID(A10,ROW(A1:OFFSET(A1,LEN(A10)-1,0)),1)))
  4. Press CTRL+SHIFT+ENTER to enter the formula as an array formula.
  5. The formula returns the value 21.

Ignoring Step 1, I looked at Step 4 and thought, from hanging around here, that we can do better. But to do better, let’s first look at the formula from the inside out. OFFSET() returns a reference one row less then A10 is long (more on OFFSET() later). ROW() then returns an array of row numbers starting from 1 (the row of A1–It’s the 1 that’s important, not the A) to the bottom of the offset. The array has as many elements as the length of the number in A10. MID() then creates an array of each digit as text. VALUE() turns the text into numbers, and then SUM(), array entered, sums the array of values.

While my formula was double-ugly, this one is just ugly. To impove it, from the outside in:

  1. Replace SUM() with SUMPRODUCT(). The formula no longer has to be array-entered, and it works just as well.
  2. Replace VALUE() with the double minus
  3. Instead of using LEN(A10)-1 as a row offset, use LEN(A10) as a height parameter.
  4. Make the reference to A1 absolute with respect to row, allowing fill-down.

The new formula is:

  • =SUMPRODUCT(- -MID(A10,ROW(OFFSET(A$1,,,LEN(A10))),1))

Much prettier, and even not counting curly-braces, two characters shorter. The Knowledge Base goes on to give this as the formula for summing the digits of a negative number:

  • =SUM(VALUE(MID(A11,ROW(A2:OFFSET(A2,LEN(A11)-2,0)),1))) also array-entered.

This is the better version, simply entered:

  • =SUMPRODUCT(–MID(A11,ROW(OFFSET(A$2,,,LEN(A11)-1)),1))

You have to start the array at 2 (via A$2) to skip the negative sign, and then also shorten the length by one for the same reason. This one is the same length as Microsoft’s. If you want one formula for all numbers, this one has no counterpart in the Knowledge base:

  • =SUMPRODUCT(- -MID(ABS(A11),ROW(OFFSET($A$1,,,LEN(ABS(A11)))),1))

It uses the absolute value ABS() function for the obvious reason. It only works for true numbers. It will not handle long text strings as numbers, such as you may have for credit cards or international phone numbers. For those, either use the earlier one, or use SUBSTITUTE(A11,”-“,””) in place of ABS(A11). Now we’re the ones getting getting ugly.

…mrt

Euler Problem 203

Euler Problem 203 asks:

The binomial coefficients nCk can be arranged in triangular form, Pascal’s triangle, like this:

It can be seen that the first eight rows of Pascal’s triangle contain twelve distinct numbers: 1, 2, 3, 4, 5, 6, 7, 10, 15, 20, 21 and 35.

A positive integer n is called squarefree if no square of a prime divides n. Of the twelve distinct numbers in the first eight rows of Pascal’s triangle, all except 4 and 20 are squarefree. The sum of the distinct squarefree numbers in the first eight rows is 105.

Find the sum of the distinct squarefree numbers in the first 51 rows of Pascal’s triangle.

Each number inside Pascal’s Triangle is the sum of the two numbers above it. Pascal’s Triangle, traditionally zero-based, can be portrayed in an array, like this:

nk 0 1 2 3 4 5 6 7
0 1 0 0 0 0 0 0 0
1 1 1 0 0 0 0 0 0
2 1 2 1 0 0 0 0 0
3 1 3 3 1 0 0 0 0
4 1 4 6 4 1 0 0 0
5 1 5 10 10 5 1 0 0
6 1 6 15 20 15 6 1 0
7 1 7 21 35 35 21 7 1

In this form, each number below row(0) and right of column(0) is the sum of the number diagonally left and above with the number directly above. There is another way to determine each number, which is what Euler’s nCk notification signifies. Where n is the row, and k is the column, each number is n!/((n-k)!*k!). In the problem statement, the largest n is 50. Therefore the largest prime-squared we have to deal with is 49, and the largest prime is 7. We have no need to investigate higher.

Once we build the 0×50, 0×50 Pascal array, a collection is the perfect tool to collect(duh!) unique entries because you can’t have duplicates; and because of symmetry, we only need look in the left half of the array. Finally, loop through the distinct numbers checking the remainders from the divisions by 22, 32, 52, and 72. If all of them are non-zero, the distinct number is square-free, and add it to the running total to form the answer. Equivalently, as implemented, if any of them are zero, then square-free is false, and don’t add the distinct number.

This is the code that does this. It runs in about 2/100ths of a second, and builds the “triangle” by addition rather than factorization.

Sub Problem_203()
   Dim Pascal(0 To 50, 0 To 50) As Double
   Dim R As Long, C As Long, N As Long, P As Long
   Dim Answer As Double, T As Single
   Dim DistinctNums As New Collection
   Dim Item As Double, Key As String
   Dim Prime(1 To 7) As Boolean, SquareFree As Boolean
   Dim Max As Double
 
   T = Timer
   Pascal(0, 0) = 1
 
   For R = 1 To 50 ‘Build the array
     Pascal(R, 0) = 1
      Pascal(R, R) = 1
      Pascal(R, 1) = R
      Pascal(R, R – 1) = R
      For C = 1 To R – 1
         Pascal(R, C) = Pascal(R – 1, C – 1) + Pascal(R – 1, C)
      Next C
   Next R
 
   For R = 0 To 50 ‘Collect distinct numbers
     For C = 0 To 25
         If Pascal(R, C)  0 Then
            Key = CStr(Pascal(R, C))
            If Not IsIn(DistinctNums, Key) Then
               Item = Pascal(R, C)
               DistinctNums.Add Item:=Item, Key:=Key
               If Item &gt; Max Then Max = Item
            End If
         End If
      Next C
   Next R
 
   Sift Sieve:=Prime
   Debug.Print Max
   
   For N = 1 To DistinctNums.Count
      SquareFree = True
      For P = 1 To UBound(Prime)
         If Prime(P) Then
            If DistinctNums(N) / (P * P) – Int(DistinctNums(N) / (P * P)) = 0 Then
               SquareFree = False
               Exit For
            End If
         End If
      Next P
      If SquareFree Then Answer = Answer + DistinctNums(N)
   Next N
 
   Debug.Print Answer; ”  Time:”; Timer – T
End Sub
 
Function IsIn(Col As Collection, Key As String) As Boolean
   Dim errNum As Long, TEMP As Variant
   errNum = 0
   Err.Clear
   On Error Resume Next
   TEMP = Col.Item(Key)
   errNum = CLng(Err.Number)
   On Error GoTo 0
   If errNum = 5 Then   ‘IsIn = False
     Exit Function
   End If
   IsIn = True   ‘errNums 0 , 438
End Function
 
Function Sift(ByRef Sieve() As Boolean) As Variant
‘Sets Sieve(n) TRUE if prime
  Dim Limit As Long, BreakPT As Long
   Dim N As Long, m As Long
 
   Limit = UBound(Sieve)
   BreakPT = Int(Sqr(Limit))
 
   Sieve(1) = False
   Sieve(2) = True
 
   For N = 3 To Limit
      Sieve(N) = True
      If N Mod 2 = 0 Then Sieve(N) = False
   Next N
 
   For N = 3 To BreakPT Step 2
      If Sieve(N) Then
         For m = N * N To Limit Step 2 * N
            Sieve(m) = False
         Next m
      End If
   Next N
 
End Function

It’s probably overkill to use a sieve for only 4 prime numbers, but it’s a tool the tool box now. I wanted to use “distinctnum(n) mod p*p” but that throws errors for large values of distinctnum(n), so I went with definition of mod instead.

…mrt