Lazy Worker of Lazy Boss

Sinatra - Sinatra is hard but nice, what can I do

When I was trying to learn Sinatra I found myself in an application. I want to tell about it step by step. First I made an application folder and create a file in it name is "susohe.rb".

1
2
3
4
5
6
require "rubygems"
require "sinatra"

get "/" do
haml :index
end


If you try to rub this file, first you need "sinatra" gem installed. Also it will not run properly because we want it to render a haml page named "index.haml". It will search that file in "views" subfolder of our application folder and throw an error because there is no file named "index.haml".

Let's create a subfolder and name it "views". Then create a file named "index.haml" in "views" subfolder. And put this lines in index.haml :

1
%h1 Hello from sinatra!

After write this line and save the file you can run susohe.rb file and open localhost:4567 address in your browser. If there is no missing point you will see a huge "Hello from Sinatra!" in your browser window. It means that we are started to crawl on all fours.


susohe_1.png
When I was a young man like you, I saw an awesome Sudoku Solving Helper application. We'll try to make something like it. First some visual editings. How many cells are there in a sudoku table? 81 cells. We'll put 81 cells on the screen. In index.haml clear all lines and write these:

1
2
3
4
- (1..81).each do |x|
%div.b
%h1 1


Because it is not the main file we can refresh our browser without restarting the server. "-" sign in the first line means that there is a Ruby command line after it and it wil not produce an output. With these lines we are looping from 1 to 81 and put huge "1" writings on the screen. Hmm! where is boxes? Let's continue coding, if you inspect the source code of the page in your browser, there are 81 div which has "class='b'".
We'll first get these divs in a border like boxes, and allocate them like sudoku table. Say How? Sure we'll use CSS! Put these lines at the end of index.haml file.

1
2
3
4
5
6
7
8
9
10
11
12


:css
div.b {
position: absolute;
border: 2px solid;
width: 51px;
height: 51px;
text-align: center;
}
h1 {
margin: 0;
}

From the first line, ":css" starts style definition lines. The indented lines after this line evaluated as css commands. First we style class="b" divs. "position. absolute;" because we want to allocate divs certain positions. We want solid, 2 pixel wide borders around our divs. Width and height are adjusted 51 pixel. And last, we want to center the writings in the div. These are enough but some browsers (like firefox) puts some margins default on <h1> tags. So we style the <h1> tags margin: 0; . Let's save the haml file and refresh our browser. Our 81 div has gone and there is only one boxed div on browser:

susohe_2.png

What happened? We styled divs as absolute position and didn't set their positions. So they are all at position 0:0 (top left korner in window). If you inspect the page source code, you'll see all our 81 divs are still in the page.

At this point we add some lines to our haml file and allocate 81 divs separate positions.

1
2
3
4

- (1..81).each do |x|
%div.b{:id=> x.to_s,
:style=>"left: #{((x-1)%9)*52+50}px; top: #{((x-1)/9)*52+50}px;"
%h1= x




To accomplish this, we write different styles for every div that we put, and allocate them at certain positions. Let's make changes and see the result by refreshing our page on browser.


susohe_3.png


Sudoku table appears slowly. We are focusing to control codes from view. I want that all available values will be written in cells as litle numbers. If one of these litle numbers cliked, that value will be the only available value for the cell, and others will be deleted. Next, this only value will be presented in a <h1> tag (like last picture).
So first create an array (which has 81 elements) to save all available values for cells.  Stop the server and edit susohe.rb file like this :

1
2
3
4
5
6
7
8
9
10
11
require "rubygems"
require "sinatra"

mat = []
(1..81).each do
mat << "123456789"
end

get "/" do
haml :index, :locals=>{:mat=>mat}
end

At startup, all values from "1" to "9" are posible. To show these values in our cells, we have to send the array to index.haml file. So we transfer the array to haml file with same name in "locals" block. Lets edit our haml file :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
- mat[0]="1"
- mat[1]="28"

- (1..81).each do |x|
%div.b{:id=> x.to_s,
:style=>"left: #{((x-1)%9)*52+50}px; top: #{((x-1)/9)*52+50}px;"}
- if mat[x-1].length==1
%h1
= mat[x-1]
- else
- (1..9).each do |xx|
- if mat[x-1].include?(xx.to_s)
%div.l{:style=>"left: #{((xx-1)%3)*17+0}px; top: #{((xx-1)/3)*17+0}px; "}
=xx

:css
div.b {
position: absolute;
border: 2px solid;
width: 51px;
height: 51px;
text-align: center;
}
div.l {
position: absolute;
border: 1px solid;
width: 16px;
height: 16px;
text-align: center;
font-size: 10px;
z-index: 1;
}
h1 {
margin: 0;
}



We are assigning some test values to array elements on first two lines, so we can see the result. We'll remove them after. Lets test the view.
   

susohe_4.png


Our application takes its form. At this point something got my interest. I've tried to make same screen with static texts on wxRuby, but it was slower than this. I think Sinatra can be used for desktop application, as well as web application. And I decided to make a request from Sinatra developers - SinatraViewer.
Lets focus on the code we write, we have a new group of divs which have class="l". These are litle boxes which include available values in them. We added styles for it in css block. If a cell has only one available value we put this value in a <h1> tag. If there are more than one value available, we show all available values in litle boxes allocated in the cell. Backgroud logic like this:
We have a sudoku problem to solve, and there are given cell values. We'll select values by clicking corresponding litle number in cell, and cell takes this value. When a cell takes a value the same values on the row, column and 3x3 groups will be deleted. It must run opposite, if a single value in a cell clicked, cell gets all available values and turn back litle numbers. To do this we create two new method in susohe.rb file.

1
2
3
4
5
6
7
8
9
10
11
12
13


get "/set/:x/:xx" do
x = params[:x].to_i
xx = params[:xx]
mat[x-1]=xx
redirect("/")
end

get "/reset/:x" do
x = params[:x].to_i
mat[x-1]="123456789"
redirect "/"
end


The first method catches urls like "localhost:4567/set/3/5". 3 in example assigned to :x parameter and 5 assigned to :xx parameter. This method replaces the value of cell with clicked number, and redirect to index page. It means that cell (:x numbered cell) will set the number :xx. Second method is to reset a cell which is setled before. How will it do this? It puts all values from 1 to 9 in the cell.
Let's edit our view file to call these methods accordingly:

1
2
3
4
5
6
7
8
9
10
11
12
13
14



- (1..81).each do |x|
%div.b{:id=> x.to_s,
:style=>"left: #{((x-1)%9)*52+50}px; top: #{((x-1)/9)*52+50}px;"}
- if mat[x-1].length==1
%h1{:onclick=>"window.location = '/reset/#{x}'"}
= mat[x-1]
- else
- (1..9).each do |xx|
- if mat[x-1].include?(xx.to_s)
%div.l{:style=>"left: #{((xx-1)%3)*17+0}px; top: #{((xx-1)/3)*17+0}px; ",
:onclick=>"window.location = '/set/#{x}/#{xx}'"}
=xx





With help of a litle bit JavaScript addition to our divs, we call our methods when they 're clicked. Yep! Save it, start application, and click items to see the result. Is that nice? Now it works with a few lines of code (Like all other Ruby programs).
 
There is nothing to add our view file after now. We'll work in our array. First start with rows, same numbers isn't allowed on same row. So if a number is setled in a cell we have to delete this number from all available values on the same row cells. Create a method named "check" to manage our array's manipulations.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
require "rubygems"
require "sinatra"

mat = []
(1..81).each do
mat << "123456789"
end

def check mat
#search for single values
singles=[]
for row in 1..9
for col in 1..9
v = mat[(row-1)*9+(col-1)]
if v.length==1
singles << (row.to_s + col.to_s + v)
end
end
end
#remove singles from cell groups
for row in 1..9
for col in 1..9
if mat[(row-1)*9+(col-1)].length != 1 #if there are more than one value in a cell
"123456789".each_char do |x| # for all available values
# if there is same value at the row as single
if singles.detect {|v| v =~ (Regexp.new(row.to_s + "[1-9]" + x))}
mat[(row-1)*9+(col-1)].delete! x # delete this number
# else add that value to available values
else
mat[(row-1)*9+(col-1)] += x
end
end
end
end
end
end

get "/" do
haml :index, :locals=>{:mat => mat}
end

get "/set/:x/:xx" do
x = params[:x].to_i
xx = params[:xx]
mat[x-1]=xx
check mat
redirect("/")
end

get "/reset/:x" do
x = params[:x].to_i
mat[x-1]="123456789"
check mat
redirect "/"
end


First we produce a list of cells which include a single value. This list has string elements. This elements are 3 character long, first one is row number, second one is column number and last one is cell's value. For example if 5th row's 3th column has value of 4, the array element of this cell will be "534".

After create singles array, we check all cells to delete values if there is single value in same row. If tested value isn't in singles array at same row, we add this value to cell's values. With "singles.detect {|v| v =~ (Regexp.new(row.to_s + "[1-9]" + x))}"  syntax we hunt same values on the row in singles array.

 
susohe_5.png


With a same algorithm we can hunt same values on the column. Change the "if" condition :

1
2
3
4
 
if singles.detect {|v| v =~ (Regexp.new(row.to_s + "[1-9]" + x)) or
v =~ Regexp.new("[1-9]" + col.to_s + x)}



As we made this modification, columns also goes under control. There is only exception , 3x3 groups. To accomplish this, first we need to determine which cells group of 3x3 that our cell allocated.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19


#remove singles from cell groups
for row in 1..9
for col in 1..9
if mat[(row-1)*9+(col-1)].length != 1 #if there are more than one value in a cell
"123456789".each_char do |x| # for all available values
boxes = ["123","456","789"]
row_box = boxes.detect {|v| v.include?(row.to_s)}
col_box = boxes.detect {|v| v.include?(col.to_s)}
# if there is same value at the row or column as single
if singles.detect {|v| v =~ (Regexp.new(row.to_s + "[1-9]" + x)) or
v =~ Regexp.new("[1-9]" + col.to_s + x) or
v =~ (Regexp.new("[" + row_box + "]" + "[" + col_box + "]" + x))}
mat[(row-1)*9+(col-1)].delete! x # delete this number
# else add that value to available values
else
mat[(row-1)*9+(col-1)] += x
end



Then we add a new line to if condition for 3x3 groups. By the way we used "detect" method too many.

That's it. After now you have a helper application when you solve sudoku, and this application created by you. Enjoy it , develop it , and send me your feedback.

susohe_6.png

Tuxweet - Twitter - Rstat.us - Github - Identi.ca