« July 2006 | Main | January 2007 »

August 14, 2006

more silly sql

I was amused by Phil Factor's gems of twisted SQL, like his code for compressing ASCII teddy bears, or The Sudoku Puzzle Generator,

So here's my contribution, a function that makes 'banners' suitable for printing on a dot matrix printer. (all code in this post was prettified with Simple-Talk SQL Prettifier )

First, we need a table that contains some font data, e.g.

SELECT FROM font_data WHERE CHAR_number=ASCII('A'ORDER BY row_number 



char
_number,row_number,row_data
65
,0,   ###  
65,1,  ## ## 
65,2##   ##
65,3##   ##
65,4#######
65,5##   ##
65,6##   ##
65,7,        

(
8 row(saffected)  

One way to get this table is to dump the standard 8x25 text mode font from the BIOS ROM, which is stored at memory location FFA6:000E. We can call the MS-DOS debug.com command to get this as some hex digits, and then unpack them into 8 rows of 8 chars each, like this:

------------- SQL STARTS HERE ---------

SET nocount ON

CREATE TABLE 
#bits_for_hex_digit(hex_digit CHAR(1),bits CHAR(4))
INSERT INTO #bits_for_hex_digit(hex_digit,bitsVALUES ('0','    ')
INSERT INTO #bits_for_hex_digit(hex_digit,bitsVALUES ('1','   #')
INSERT INTO #bits_for_hex_digit(hex_digit,bitsVALUES ('2','  # ')
INSERT INTO #bits_for_hex_digit(hex_digit,bitsVALUES ('3','  ##')
INSERT INTO #bits_for_hex_digit(hex_digit,bitsVALUES ('4',' #  ')
INSERT INTO #bits_for_hex_digit(hex_digit,bitsVALUES ('5',' # #')
INSERT INTO #bits_for_hex_digit(hex_digit,bitsVALUES ('6',' ## ')
INSERT INTO #bits_for_hex_digit(hex_digit,bitsVALUES ('7',' ###')
INSERT INTO #bits_for_hex_digit(hex_digit,bitsVALUES ('8','#   ')
INSERT INTO #bits_for_hex_digit(hex_digit,bitsVALUES ('9','#  #')
INSERT INTO #bits_for_hex_digit(hex_digit,bitsVALUES ('A','# # ')
INSERT INTO #bits_for_hex_digit(hex_digit,bitsVALUES ('B','# ##')
INSERT INTO #bits_for_hex_digit(hex_digit,bitsVALUES ('C','##  ')
INSERT INTO #bits_for_hex_digit(hex_digit,bitsVALUES ('D','## #')
INSERT INTO #bits_for_hex_digit(hex_digit,bitsVALUES ('E','### ')
INSERT INTO #bits_for_hex_digit(hex_digit,bitsVALUES ('F','####')

CREATE TABLE #rom_font_dump(data VARCHAR(100))

CREATE TABLE font_data(CHAR_number INTrow_number INT,row_data CHAR(8))

DECLARE @temp_file VARCHAR(100)
DECLARE @cmd VARCHAR(100)
SET @temp_file='%TMP%\debug.txt'

SET @cmd='echo d FFA6:000E l 400 >'+@temp_file
EXEC xp_cmdshell @cmd no_output
SET @cmd='echo q >>'+@temp_file
EXEC xp_cmdshell @cmd no_output
SET @cmd='debug <'+@temp_file
INSERT INTO #rom_font_dump(dataEXEC xp_cmdshell @cmd 

DECLARE @byte_counter INT
SET 
@byte_counter=0

DECLARE row_cursor CURSOR fast_forward FOR
SELECT 
data FROM #rom_font_dump
       
WHERE data LIKE 'ffa6:%'
       
ORDER BY data

DECLARE @data VARCHAR(100)
OPEN row_cursor
FETCH next FROM row_cursor INTO @data
WHILE @@fetch_status >-BEGIN
       DECLARE 
@i INT
       SET 
@i=12
       
WHILE @i<63 BEGIN
               DECLARE 
@row CHAR(8)
               
SET @row=NULL
                
SELECT @row=d1.bits+d2.bits FROM #bits_for_hex_digit d1
                       
INNER JOIN #bits_for_hex_digit d2
                       
ON d1.hex_digit=SUBSTRING(@data,@i,1)
                       AND 
d2.hex_digit=SUBSTRING(@data,@i+1,1)
               
IF @row IS NOT NULL BEGIN            
                       INSERT INTO 
font_data(CHAR_numberrow_number,row_data)
                               
SELECT @byte_counter/8@byte_counter%8@row
                       
SET @byte_counter=@byte_counter+1
               
END
               SET 
@i=@i+3

       
END

       FETCH 
next FROM row_cursor INTO @data
END
CLOSE 
row_cursor
DEALLOCATE row_cursor

DROP TABLE #bits_for_hex_digit
DROP TABLE #rom_font_dump  

------------- SQL ENDS HERE ---------

This is the function that uses that table to turn a string into a banner:

------------- SQL STARTS HERE ---------

CREATE function ufn_Banner(@s VARCHAR(100)) 
returns VARCHAR(8000AS
BEGIN

       DECLARE 
@row_0 VARCHAR(1000)
       
DECLARE @row_1 VARCHAR(1000)
       
DECLARE @row_2 VARCHAR(1000)
       
DECLARE @row_3 VARCHAR(1000)
       
DECLARE @row_4 VARCHAR(1000)
       
DECLARE @row_5 VARCHAR(1000)
       
DECLARE @row_6 VARCHAR(1000)
       
DECLARE @row_7 VARCHAR(1000)

       
SET @row_0 =''
       
SET @row_1 =''
       
SET @row_2 =''
       
SET @row_3 =''
       
SET @row_4 =''
       
SET @row_5 =''
       
SET @row_6 =''
       
SET @row_7 =''

       
DECLARE @i INT
       SET 
@i=0
       
WHILE @i<=LEN(@s
       
BEGIN
               DECLARE 
@c CHAR(1)
               
SET @c=SUBSTRING(@s,@i,1)
                       
SELECT @row_0=@row_0+row_data FROM font_data WHERE CHAR_number=ASCII(@c) AND row_number=0
                       
SELECT @row_1=@row_1+row_data FROM font_data WHERE CHAR_number=ASCII(@c) AND row_number=1
                       
SELECT @row_2=@row_2+row_data FROM font_data WHERE CHAR_number=ASCII(@c) AND row_number=2
                       
SELECT @row_3=@row_3+row_data FROM font_data WHERE CHAR_number=ASCII(@c) AND row_number=3
                       
SELECT @row_4=@row_4+row_data FROM font_data WHERE CHAR_number=ASCII(@c) AND row_number=4
                       
SELECT @row_5=@row_5+row_data FROM font_data WHERE CHAR_number=ASCII(@c) AND row_number=5
                       
SELECT @row_6=@row_6+row_data FROM font_data WHERE CHAR_number=ASCII(@c) AND row_number=6
                       
SELECT @row_7=@row_7+row_data FROM font_data WHERE CHAR_number=ASCII(@c) AND row_number=7
               
SET @i=@i+1
               
       
END
       RETURN
               
@row_0+CHAR(10)
               +
@row_1+CHAR(10)
               +
@row_2+CHAR(10)
               +
@row_3+CHAR(10)
               +
@row_4+CHAR(10)
               +
@row_5+CHAR(10)
               +
@row_6+CHAR(10)
               +
@row_7+CHAR(10)
       
END  

------------- SQL ENDS HERE ---------

And finally, here's some example screenshots.

First, showing the output in SQL Server Management Studio

You could also pop up a banner in internet explorer, using  usp_Show_HTML, like this:

 

August 13, 2006

discovering phil factor

Nick came over on thursday so we could play around with gnokii, which turned into a small debacle (a debaclet?) when we turned out to be one cable short of a comms link, and then the oven died with a half roasted leg of lamb still in it.

On the upside, I did find out about Phil Factor, that plus the wine and the 2 six packs of peroni we got through meant the evening was net positive I think.

Here's some excerpts....

From Attack of the barber-surgeons of IT: A tale of cursed cursors & embedded SQL 

The development foot soldiers in the IT army are required to have two arrows in their quiver: fluency with the procedural language currently in fashion and a measure of skill in database programming.

Few people are proficient in both. It reminds me of the medieval barbers who cheerfully cut off limbs and performed other surgery as a sideline. If they could cut hair, they reasoned, why not cut the body as well?

From The Writing on the wall: lies, damned lies and documented working procedures

The management consultants accepted everything at face value. They did not seem to even consider that old Joe in dispatch, whose job was a sinecure that an energetic man could complete in half an hour, might have lied to them when he described the elaborate and skilled nature of his contribution to the company. It was all noted, analysed, diagrammed and documented. The final report was delivered in forty volumes and placed in a large filing cabinet, along with a final bill to the company of several million pounds.

Also check out The Index: An Elegy for an excerpt from Phil's upcoming book on SQL Server, rendered in Tennyson style epic verse.

August 06, 2006

making geo mashups with ruby and ms virtual earth

I've put together some helper classes for making geo mashups with ruby and MS Virtual Earth.

The first is called 'PlaceFinder', which takes a list of places (stored as a yaml file), then lets that list be searched by name.

In the examples below, I've used my list of NSW suburbs. Unfortunately, the co-ordinates in this file are a kilometer or so out of alignment with the geodetic system used by either the MS or Google map servers, which is noticible when you zoom in to street level, but for a high level 'city wide' view, it's close enough.

Here's an irb session showing how placefinder works:
irb(main):001:0>
irb(main):002:0* require 'placefinder'
=> true
irb(main):003:0> place_finder=PlaceFinder.new
=> #<PlaceFinder:0x2c5d128 @places={}>
irb(main):004:0> place_finder.load_places("nsw_places.yml")
=> true
irb(main):005:0> place_finder.find_by_name("Leura")
=> #<Place:0x310a5e0 @latitude=-33.7089, @longitude=150.335, @postcode=2780, @name="Leura">
irb(main):006:0>

The second module is 'MapMaker', which has a single function 'make_map' that takes a list of points and a zoom factor, and returns html that loads a map from the Microsoft Virtual Earth website, with 'pins' at the specified points

each pin has the following attributes

name
longitude
latitude
caption (which can contain any HTML tags, but should not have any line break characters)

There's some predefined Zoom Factors whose name indicates approximately how large an area will be visible on screen:

MapMaker::ZOOM_STATE=6
MapMaker::ZOOM_GREATER_CITY=9
MapMaker::ZOOM_CITY=10
MapMaker::ZOOM_SUBURB=11

So to put this to use, we need a list of places we want to make pins for. For no particular reason other than that it makes for a fairly simple example, I've decided to plot the home grounds of the 12 rugby clubs that make up the sydney 'grade' competition. (For ancient political reasons, rugby in Sydney is administered in 2 tiers - the top tier is called 'Club Rugby', and the other tier is 'Subbies').

So I went looking for a list of all the sydney club rugby teams which has enough structure to parse with regular expressions.

Screen scraping is a bit of a black art, I'm not going to explain in detail what I'm doing to pull the info out of the web page, but I'm taking advantage of that fact that the data for each team is enclosed in a <tr>..</tr> tag set, and within each  team, the data all has nice regexable prefixes like "Address: ", so a line like this
 
address=team_row.match(/Address: ([^<]*)/)[1]

means "extract the text that comes between the text 'Address: ' and the first '<' character."

The full example follows,  but first, have a look at the output, a map showing the home grounds of the sydney club rugby teams. And all the code needed to make this work (including a list of locations of NSW suburbs) is in the file MapMaker_Demo.zip

require 'map_maker'
require 'placefinder'

place_finder=PlaceFinder.new
place_finder.load_places("nsw_places.yml")

require 'open-uri'

html=open('http://www.tahkids.com.au/aboutthe_TNC.html').read

pin_points=[]

#strip off all the stuff up until the table we're interested in
html.gsub!(/.*CLICK&nbsp;HERE/m,"")

#find all the <tr> elements that contain the text 'Nickname:'
team_rows=html.scan(/<tr>.*?Nickname:.*?<\/tr>/m)

team_rows.each do |team_row|
 #the team name is the first text in bold
 team_name=team_row.match(/<b>(\w[^<]*)/)[1]

 home_ground=team_row.match(/Home Ground: ([^<]*)/)[1]

 address=team_row.match(/Address: ([^<]*)/)[1]

 suburb=address.sub(/.*,\s*/,"") #addresses are all of the form Street, Suburb so strip up to the comma
 
 #some entries have a dud href for the website field so correct them 
 website=team_row.sub(/href="www/,'href="
http://www').match(/http[^"]*/)[0]
 
 caption="<a href=#{website}>#{team_name}</a><br>Home Ground: #{home_ground}"

 #now we've extracted all the interesting data, make up a new pin point to stick on the map
 pin_point=place_finder.find_by_name(suburb)
 pin_point.name=team_name
 pin_point.caption=caption
 pin_points<<pin_point
end

#now make a map with the list of pin points
map_html=MapMaker.make_map(pin_points,MapMaker::ZOOM_CITY)

#save the html
File.open("sydney_club_rugby_map.html","w") {|f| f<<map_html}