Saturday, July 24, 2010

Python Library for Reading Excel Files

Came across a pretty sweet python library for reading data from Excel files: http://pypi.python.org/pypi/xlrd. Xlrd is super easy to install and use. Here's a snippet of code:
1:  import xlrd  
2:
3: mybook = xlrd.open_workbook("test.xls")
4: sheets = mybook.sheets()
5: for sheet in sheets:
6: for i in range(sheet.nrows):
7: for j in range(sheet.ncols):
8: cell = sheet.cell(i, j)
9: if cell.ctype != xlrd.XL_CELL_EMPTY:
10: print cell.value
11:

There are many other cool things you can do with xlrd. I encourage you to try it out!

Sunday, July 4, 2010

SEP a success!

For the past few weeks, I had been intensely preparing for a weeklong program for high school girls interested in computer science. I mentioned the program a couple times in past posts. I wanted to follow up now with a summary of week, including highlights and lowlights and lessons learned.

The program ran from Jun 28th to Jul 2nd. It started at 10am and went to 3pm every day, with an hour lunch break at noon. There were about 20 students in total, although some students had previous commitments and didn't make it every day.

Over the course of the week, we taught the students a variety of computer related topics. First, we started them off with a group exercise where they came up with an idea for a mobile app (exercise based on the Technovation Challenge). This exercise was designed to teach the students that applications don't start with coding. You first need an idea before you can start creating the app. The students worked for an hour on this task, then had the opportunity to present their idea to the rest of the group. The students came up with some really great products:

1. An app for learning more about colleges and signing up for newsletters, etc
2. An app for identifying what an animals body language might be telling you
3. [I can't remember this one, and it's the one that won! I'll figure it out soon and fill in this blank]
4. An app for allowing users to quickly jot down reminders and add the reminders to a calendar

The students then voted for the best app, and the group with the most votes got a prize (#3).

Next, we taught the students about computer architecture. We had about 6 computers set up that the students could take apart. We had worksheets with labeled pictures. I was surprised to find out that several of the students had already taken apart computers, and one had even built a computer from scratch! Students typically enjoy this exercise, since it's very hands on.

After computer architecture, we jumped into some python programming. We showed them some of the basics, including the infamous print "hello world" statement, data types, variables, and data structures. The students picked up on the concepts very quickly. They worked through exercises and seemed to understand the concepts really well.

After the intro to programming, we moved on to Computer Networking. This is usually one of the most popular lessons! As part of the lesson, we teach the students how to send email directly from the command line. This allows them to specify any sender email address, they'd like. :) This is fun for them, since they can send emails to friends from obama@whitehouse.gov or some other similar email address. They also learned about GET and POST, using GET directly from the command line.

Next, we taught them about databases and persistent data. They created tables in a database, added data to the table, and retrieved the data from the table, all using SQL! They even learned how to do joins, and were very curious, asking how to delete data and remove tables from the database.

With networking, databases, and programming under their belt, we introduced them to Google App Engine. Google App Engine allows users to create web applications very easily. We had developed a basic web app for them to build upon. The final product can be seen here: http://testingforsep.appspot.com. We choose the Outfit Builder app because it seemed like a product they could relate to. I know when I was a teen, I loved shopping and buying new clothes. We were assuming that teens haven't changed much over the years. :) As part of the intro to App Engine, I showed them how to create a basic application from scratch. They then practiced deploying an application to the Google servers.

Now that they understood how App Engine worked, we needed to teach them a couple more programming concepts before they could actually start filling in the missing pieces of the app. First, we taught them if statements. Once they understood if statements, they could build out the change background color portion of the site. Next, we taught them for loops. They could then display the bottoms on the page, and also work on the filtering form. We gave them a few other exercises to work on, including updating the database when a form was submitted on the Edit page and creating the random outfit. Overall, the students performed very well on all these exercises and finished them very quickly! In the future, I would prepare more advanced exercises for the students who were more advanced.

On Thursday, we gave the students a break and took them to the Intel museum. This turned out to be a lot of fun. Our tour guide was very knowledgeable and good with the students. We had a tour and also an activity called "Puzzle Ball" where the students had to write instructions for for to put together a puzzle. The students were allowed to keep the puzzles afterwards, which everyone was happy about. I would highly recommend the Intel museum tour to any school or summer program. It offers a great opportunity for students to learn more about computer processing.

Finally, we taught them about Android phone development and gave them access to App Inventor to create the basic HelloPurr application. App Inventor provides developers with a GUI interface that allows developers to drag and drop UI elements onto the phone screen, edit the properties of each element, then open another GUI application to add the functionality to each element. This portion, the Code Blocks editor, acts like a jigsaw puzzle. The students choose which element they want to add functionality to, then can select from different methods and outcomes, each fitting together like puzzle pieces. I was very impressed at how quickly the students picked it up!! The entire exercise was supposed to take about 1:30 in total. They finished in about 20 minutes!! Amazing. In fact, many of them even figured out how to add different things to the app, including changing the background color of the screen or changing the .wav file associated with the program. I was really impressed and am excited to see what Google does with App Inventor. I think it's a great tool for students to learn how to program the Android phone. In fact, I think it could be useful for everyone, kids and adults alike. I might even use it if I needed to create a basic Android app! Good work Google, keep innovating. :)

Overall, it was a great week. When the students were leaving, they were all smiles, which is always a good sign. A few things I learned from the program:

1. Prepare extra difficult material for those students who might outperform the rest
2. Have more lessons for App Inventor.
3. Go to the Intel museum again!
4. Give the students more opportunity to customize the App Engine application
5. Have backup material prepared in case something doesn't work correctly.

Thanks! If you're interested in learning more about the program, please feel free to contact me (kat.brisbin at gmail). You can also download most of the material at this website: http://cs.usfca.edu/~sep/calendar.html

Sunday, June 6, 2010

Python for Girls!

Hi all! I'm working on a new python tutorial for beginners, specifically aimed at girls. There aren't many of those out there yet, so I thought it would be a great opportunity to help start building the library! Here's a link to my documentation so far: http://docs.google.com/View?docID=0AQGjCIWYc3RlZGdoYjk0bnRfMTQwMmNtcTVrNmhm&revision=_latest&hgd=1. It's definitely a work in progress, but feel free to peruse the tutorial. If you notice any errors, feel free to comment!

What I think sets this tutorial apart from the rest is the writing style and approach to teaching. I'm writing the tutorial in a very comfortable and relaxed voice. I write what I would say if I was explaining python to someone in person, interjections and all. Hopefully this will make the tutorial more interesting and easier to read.

My approach to teaching is also different in that I provide concrete examples before explaining the programming concept. From what I've read (sorry, can't find the post anymore..), girls learn programming better when they first understand the problem at hand, then are taught how to solve the problem. I use this format frequently in my tutorial. For example, I first ask how lists are used in real life and provide some examples before even explaining what a python list is. This will hopefully help the girls realize that programming is about solving real life problems, such as creating the list of TV shows to display in the TiVo menu or creating an application to track your grocery list, and then teach them how to do so.

I'm hoping this approach will work well! I get a chance to test my material at the Summer Enrichment Program! If I have time, I will try to document my experience (what worked, what didn't) so that future tutorials can continue to improve!

Sunday, May 23, 2010

Android Unboxing

Having fun with Android today:

Wednesday, May 12, 2010

Creating an Android 'Library'

In some instances, you might want Android classes available to multiple Android applications. To simply add an Android project to the another Android project's build path will not work (a good explanation is here: http://groups.google.com/group/android-developers/browse_thread/thread/5537ae10e4143240). A workaround is summarized below:

1. Create a new Android project.
2. Add any necessary classes and interfaces (including AIDLs)
3. Allow the AIDLs to compile and generate Java code.
4. Update the .project file. This file is located in the root of the Android project. It contains the following contents:

1:  <?xml version="1.0" encoding="UTF-8"?> 
2: <projectDescription>
3: <name>[project name]</name>
4: <comment></comment>
5: <projects>
6: </projects>
7: <buildSpec>
8: <buildCommand>
9: <name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>
10: <arguments>
11: </arguments>
12: </buildCommand>
13: <buildCommand>
14: <name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>
15: <arguments>
16: </arguments>
17: </buildCommand>
18: <buildCommand>
19: <name>org.eclipse.jdt.core.javabuilder</name>
20: <arguments>
21: </arguments>
22: </buildCommand>
23: <buildCommand>
24: <name>com.android.ide.eclipse.adt.ApkBuilder</name>
25: <arguments>
26: </arguments>
27: </buildCommand>
28: </buildSpec>
29: <natures>
30: <nature>com.android.ide.eclipse.adt.AndroidNature</nature>
31: <nature>org.eclipse.jdt.core.javanature</nature>
32: </natures>
33: </projectDescription>
Remove the following elements from the buildSpec:
1:   <buildCommand>  
2: <name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>
3: <arguments>
4: </arguments>
5: </buildCommand>
6: <buildCommand>
7: <name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>
8: <arguments>
9: </arguments>
10: </buildCommand>
11: <buildCommand>
12: <name>com.android.ide.eclipse.adt.ApkBuilder</name>
13: <arguments>
14: </arguments>
15: </buildCommand>

Remove the following element from the natures:

1:   <nature>com.android.ide.eclipse.adt.AndroidNature</nature>  

5. Save the updated .project file.
6. Refresh the Android project in Eclipse.
7. Clean the project (select the project folder, select Project > Clean... from the main menu)
8. Remove the R.java file from the generated files.

Your project should now be ready to be added to the build path of other Android projects.

Sunday, May 9, 2010

Updating sqlite database from the command line - django

I'm working with django today and realized halfway through that my database was incorrect. I needed to drop columns from a table and create a new table. The syncdb command, from what I've read, does create new tables, but it will not drop columns. So, I had to directly access the sqlite database from the command line. Luckily this is pretty easy. Just navigate in the Terminal window to the directory where the database file is saved. Then run the following command:

sqlite3 [database filename]

This will start sql in the Terminal window. From here, you can run the typical sql commands to select rows, update rows, etc. In my case, I simply dropped all tables so I could start from scratch:

DROP TABLE search_searchresult;

This could be a very useful for other problems, such as uploading bulk data.

Wednesday, April 28, 2010

Google App Engine - printing debug messages

The logging.debug statements are not printed by default in the local development server of Google App Engine projects. In order to display these messages, you need to add the "--debug flag" to the launch settings. Here are the steps to do so:

1. Open GoogleAppEngineLauncher
2. Right-click on your app engine project
3. Select Info...
4. In the Launch Settings section, enter "--debug" in the Extra Flags box.
5. Click Update.

Restart the server. The Logs should now display DEBUG statements in addition to warnings and info.

Thursday, April 8, 2010

Unzipping jar files

Wow, opening jar files is much easier than I realized! I had been expanding them from the command line for a couple years now. Today, I just found this page: http://ostermiller.org/opening_jar_files.html. All I have to do on my Mac is change the extension to .zip and double click. Wow.

Sunday, April 4, 2010

Installing rdflib - Mac OS X 10.5.8

Installing rdflib seems easy enough, but I ran into a couple of weird issues that were not documented anywhere online. I wanted to share my experience in case anyone else ran into these same problems!

First, I tried the easy_install command recommended by the site. This was the output I received:

Searching for rdflib==2.4.2
Reading http://pypi.python.org/simple/rdflib/
Reading http://rdflib.net/
Best match: rdflib 2.4.2
Downloading http://rdflib.net/rdflib-2.4.2.tar.gz
Processing rdflib-2.4.2.tar.gz
Running rdflib-2.4.2/setup.py -q bdist_egg --dist-dir /var/folders/Lr/Lrriod8lEEm63-sPBZf6XE+++TI/-Tmp-/easy_install-xa0EVw/rdflib-2.4.2/egg-dist-tmp-qvivjK
warning: no files found matching 'example.py'
zip_safe flag not set; analyzing archive contents...
No eggs found in /var/folders/Lr/Lrriod8lEEm63-sPBZf6XE+++TI/-Tmp-/easy_install-xa0EVw/rdflib-2.4.2/egg-dist-tmp-qvivjK (setup script problem?)

Uh, no egg found? I suppose that's somewhat appropriate, it is Easter today (darn Easter bunny!). I didn't think I could easily solve this problem, since there might be a problem with the setup script.

So, I tried downloading the tar file. Upon unpacking the file, there was a setup.py file. Great! I just need to run python setup.py install, right? Wrong.. here's the output:

Traceback (most recent call last):
File "setup.py", line 1, in
from setuptools import setup, find_packages
ImportError: No module named setuptools

OK, I checked the directory again, and noticed that there was an ez_setup.py file. Maybe this would do the trick? I ran python ez_setup.py, and there were no errors! Here was the output:

Downloading http://pypi.python.org/packages/2.5/s/setuptools/setuptools-0.6c9-py2.5.egg
Processing setuptools-0.6c9-py2.5.egg
Copying setuptools-0.6c9-py2.5.egg to /Library/Frameworks/Python.framework/Versions/2.5/lib/python2.5/site-packages
Adding setuptools 0.6c9 to easy-install.pth file
Installing easy_install script to /Library/Frameworks/Python.framework/Versions/2.5/bin
Installing easy_install-2.5 script to /Library/Frameworks/Python.framework/Versions/2.5/bin

Installed /Library/Frameworks/Python.framework/Versions/2.5/lib/python2.5/site-packages/setuptools-0.6c9-py2.5.egg
Processing dependencies for setuptools==0.6c9
Finished processing dependencies for setuptools==0.6c9

Looks like this solves the earlier error I received when running python setup.py install: the setuptools package was created! Great! I ran python setup.py install again, and rdflib was successfully installed...

with one caveat: the build files are located in build > lib.macosx-10.3-fat-2.5 (or some similar directory) > rdflib. Make sure to put this path in your PYTHONPATH. So far, I found one file in here that is necessary for running SPARQL queries, SPARQLParserc.so. It's not in the rdflib directory at the root of the rdflib-2.x.x directory!

Good luck!

Saturday, April 3, 2010

Bulk Upload Data to App Engine Dev DB

NOTE: these instructions are for the previous version of App Engine!


How to upload bulk data to App Engine Dev:

1. Create a loader file:
import datetime
import main
from google.appengine.ext import db
from google.appengine.tools import bulkloader

class ClothesLoader(bulkloader.Loader):
def __init__(self):
bulkloader.Loader.__init__(self, 'Clothes',
[('title', lambda x: x.decode('utf-8')),
('image', lambda x: x.decode('utf-8')),
('type', lambda x: x.decode('utf-8')),
('tag', lambda x: x.decode('utf-8'))
])

loaders = [ClothesLoader]

2. Create a CSV file with your data. Example:
 Jeans, http://localhost:8080/images/jeans1.jpg, bottom, spring

3. In your app.yaml file, MAKE SURE you have the following lines:
 handlers:
- url: /remote_api
script: $PYTHON_LIB/google/appengine/ext/remote_api/handler.py

4. Also make sure that your PYTHONPATH env var has the directory in which you saved your models. On a Mac:
 export PYTHONPATH=$PYTHONPATH:<directory>

5. Finally, run the following command:
 appcfg.py upload_data --config_file=<loader python file> --filename=<csv data file> --kind=<model name> --url=http://localhost:8083/remote_api <app directory>

Tuesday, March 30, 2010

Stack Implemented as a LinkedList in Python

In preparation for interviews, I am attempting to write a Stack as a LinkedList in python. This is what I came up with:
 class Stack:  
class Element:
def __init__(self, data=None, next=None):
self.next = next
self.data = data

def __repr__(self):
return str(self.data)

def __init__(self):
self.head = self.Element()

def push(self, data):
newHead = self.Element(data=data, next=self.head)
self.head = newHead

def pop(self):
head = self.head
self.head = head.next
return head

def delete(self):
self.head = None

If anyone sees anything wrong, please let me know!

Friday, March 26, 2010

Latex - compile from PS -> PDF

I'm a total n00b (sp?) when it comes to Latex. Today, I had to compile a latex file with .eps files in it. When hitting the Typeset button in Emacs, there was an error: "Latex Error: Unknown graphics extension: .eps." What? Google searches were vague, difficult to interpret because I'm a n00b. I finally found some documentation that mentioned converting directly from Latex => PDF won't work when using EPS images. Instead, you need to convert from Latex => PS => PDF. OK, how do I do this??

This website offered the solution I was looking for: http://www.maths.ox.ac.uk/help/faqs/latex/conversions. Basically, from the command line, you can use the following commands when in the directory that your .tex file is in:

latex [texfile].tex
dvips -o file.ps [texfile][.dvi]
ps2pdf file.ps file.pdf
open file.pdf

Substitute the correct file names in the commands above with your own! The last line will open your new latex pdf file.

Happy latexing!

Matplotib - adding patterns to bars

Matplotlib is an excellent library for building graphs in python. I've used it before to create graphs for school and my post on jacks or better. Today, I needed to create a bar chart for black and white printing, so the standard color scheme wouldn't work! I tried to find documentation online for how to do this, and found nothing. Finally, I figured it out. You can use the hatch property in the optional kwargs arguments to create the patterns. For example, the code looks something like this
1:  kwargs = {'hatch':'.'}
2: rects = ax.bar(left, height, width, color='w', **kwargs)
This creates a bar with dots in the center. Here's an example:


Here's all of the code:
1:  import numpy as np  
2: import matplotlib.pyplot as plt
3:
4: N = 2
5: ind = np.arange(N) # the x locations for the groups
6: offset = 0.05
7: width = 0.24 # the width of the bars
8:
9: fig = plt.figure()
10: ax = fig.add_subplot(111)
11:
12: baseline = [7.64, 3.89]
13: baselineStd = [0.59, 0.06]
14: kwargs = {"hatch":'x'}
15: rects1 = ax.bar(offset+ind, baseline, width, color='w', ecolor='k', yerr=baselineStd, **kwargs)
16:
17: leaf = [7.06, 3.69]
18: leafStd = [0.67, 0.12]
19: kwargs = {"hatch":'.'}
20: rects2 = ax.bar(offset+ind+width, leaf, width, color='w', ecolor='k', yerr=leafStd, **kwargs)
21:
22: ultrapeer = [5.76, 3.25]
23: ultrapeerStd = [0.32, 0.19]
24: kwargs = {"hatch":'/'}
25: rects3 = ax.bar(offset+ind+width+width, ultrapeer, width, color='w', ecolor='k', yerr=ultrapeerStd, **kwargs)
26:
27: # add labels
28: ax.set_ylabel('Estimated Lifetime (hours)')
29: ax.set_xticks(offset+ind+width+width/2)
30: ax.set_xticklabels( ('Inactive Mac2', 'Active Mac2') )
31:
32: ax.legend( (rects1[0], rects2[0], rects3[0]), ('Baseline', 'Leaf', 'Ultrapeer') )
33:
34: plt.show()

Thursday, March 18, 2010

Lazy Load - a jQuery plugin

While visiting Mashable today, I noticed that the images on the page were being loaded as I scrolled down. Very cool! Allows for a very quick page load time. I did some research and found that this can be accomplished using Lazy Load, a plugin for jQuery: http://www.appelsiini.net/projects/lazyload. I haven't tried it myself, so I can't really attest to it's functionality, but I highly recommend this plugin for anyone creating a web page with heavy image content.

Saturday, March 6, 2010

Photo-bot



My latest project for school involved creating a photo editing and sharing website using Google App Engine. I was excited about the opportunity because (a) I love creating web apps and (b) I was interested in learning app engine. I was pretty happy with the results of my assignment, so I thought I'd share my site with everyone: http://photo-bot.appspot.com/.

There was one pretty tricky use case I had to handle for the assignment. App Engine limits the size of Blobs saved in their datastore to 1MB. However, I had to allow users to upload photos greater than 1MB. Before saving to the datastore, the images needed to be resized. Simply keeping the image data in memory and calling images.resize() is not good enough, since the call to images.resize() actually saves the image data in the datastore, which throws an exception when the image is too large.

To solve the problem, I used blobstore. Blobstore allows files up to 10MB in size. The images could then retrieved from the blobstore, resized and saved to the datastore. The blobstore entry would then be deleted, since saving to the blobstore costs $$. :)

Of course, this makes the code more complex. And, of course, documentation on the App Engine google page is quite limited! Luckily, I found some excellent code posted by Benjamin Pearson. Thanks to Benjamin for posting this code, it helped immensely in trying to solve the problem! I thought I'd post my code, too, in order to get more information out there on the topic. Here's what my basic class looked like:
1:  class AddPhotos(blobstore_handlers.BlobstoreUploadHandler, GenericHandler):
2: def get(self):
3: uploadUrl = blobstore.create_upload_url('/addPhotos')
4: self.template_values['uploadUrl']=uploadUrl
5: self.setTemplate('templates/addPhotos.html')
6:
7: def post(self):
8: upload_files = self.get_uploads()
9: blob_info = upload_files[0]
10: blob_key = blob_info.key()
11: photo = main.Photo(image=db.Blob(self.getImage(maxImageDimension, maxImageDimension, blob_key)),
12: dateAdded=datetime.today(),
13: dateModified=datetime.today(),
14: thumb=db.Blob(self.getImage(maxThumbWidthDimension, maxThumbHeightDimension, blob_key))
15: )
16: photo.put()
17: blobstore.get(blob_key).delete() #delete image after use
18: self.redirect('/')
19:
20: def getImage(self, maxWidthDimension, maxHeightDimension, blob_key):
21: img = images.Image(blob_key=str(blob_key))
22: img.resize(width=maxWidthDimension, height=maxHeightDimension)
23: image = img.execute_transforms(output_encoding=images.PNG)
24: return image

First, a disclaimer about the code: this is the most basic form of my code. I didn't include error checking, among other things. I only wanted to cover the basics, since even the basics can be tricky.

That being said, I want to point out the important pieces of the code (in bold). At line 1, I made my class a subclass of BlobstoreUploadHandler, and not the typical request handler. This is required when uploading to the blobstore.

Next, I created an upload URL, passing it a path (line 3). App engine redirects to a post call on the path that you pass. This path should be in your call to the WSGIApplication constructor:
1:  application = webapp.WSGIApplication([
2: #Main Page
3: ('/', handlers.MainHandler),
4:
5: #Add photos
6: ('/addPhotos', handlers.AddPhotos),
7: ],
8: debug=True)
9: util.run_wsgi_app(application)
In my case, app engine redirects a post call on /addPhotos, which is handled by the AddPhotos class.

Now we have to handle the post and retrieve the blobstore upload. Behind the scenes, Google App engine uploads the file to the blobstore, and allows us to get access to the blob via the key. This is done in lines 8-10. First, the uploaded files are retrieved by calling self.get_uploads(). Next the blob_info is obtained for the first file uploaded. Finally, the key is retrieved. (Note: this could probably be done in one line!)

The key gives access to the image data. In line 21, you can see that a new Image object is created by passing the blob key to the constructor. The image object can be used to resize or transform the uploaded photo using any of the Image functions. After performing the transformations, make sure to execute the transformations by calling execute_transformations() on the Image.

Once the image has been altered, you can save the image to the database as a blob (line 11). Pass the result of the execute_transformations() call to the db.Blob constructor and, as long as your image is now less than 1MB in size, you can save this blob to the datastore!

Finally, delete the blobstore object, since there is no need for it anymore (line 17).

One thing I should mention before I end: you need a billing account with Google before you can use the Blobstore in production (locally it will work fine). However, as long as your blobstore data usage is small, you won't get charged.

Good luck!

Saturday, February 13, 2010

Jacks or Better

I recently went to Las Vegas and became intrigued with the game Jacks or Better (JoB). This was partly due to the fact that my fiance and I were fairly successful playing the game: we won $13 overall! I was also interested in the game because there is the illusion that you have excellent odds of winning. A hand of Jacks or Better seems very easy to get.

For those who don't know JoB, the rules are pretty simple: any hand with a pair of Jacks or better is a winner. The game can cost any amount to play, but here I focus on the $1 game. Winning hands with a $1 bet depend on the type of Jacks or Better, but they typically follow this pattern:

Jacks or better: $1
2 pair: $2
3 of a kind: $3
Straight: $4
Flush: $6
Full house: $9
Four of a kind: $25
Straight flush: $50
Royal flush: $250

There are many strategies already well documented online. I found the strategy below (I lost the link to the site with this strategy, but here's a link to a similar one). Basically, you want to hold cards in the order of rules listed.

Hold any winning hand of four cards or better
Hold any 4 cards to a royal flush (10, J, Q, K, A)
Hold any other winning hand
Hold any 4 cards to Straight Flush
Hold any 3 cards to a royal flush
Hold any 4 cards to a flush
Hold 2 of a kind
Hold any cards to an open straight
Hold any 2 high cards of the same suit
Hold any 3 cards to a straight flush
Hold a J, Q, and K of different suits
Hold any two high cards of different suits
Hold J, Q or K with a Ten of the same suit
Hold any single high card

With this strategy in mind, I was curious to see what the winnings would be over time. I wrote a program in python to simulate a person playing the game of JoB using the strategy listed above. I started the agent with an initial amount of $50. The agent played until he was below $50 or 50 times (which amounts to 50 plays). Over the course of play, I tracked how much the agent won or loss, and repeated this process 10,000 times. After 10,000 rounds, I plotted the results using matplotlib. The results are below:



As you can see, most of the runs fall between a gain of $50 and a loss of $50. There are much steeper gains than losses. Here are some statistics I gathered during the run:

Total Winnings: -$1.46
Number of Runs Zero or Above: 3734
Highest amount earned during a run: $258
Number of Runs Below Zero: 5973
Highest amount lost during a run: $32

What's so interesting about this is that the total winnings is practically zero! After 10,000 times of playing, the agent loses less than $2. I was curious to see if this small loss would hold over time, so, to the dismay of my computer, I ran the code a couple more times. The next two times, the agent lost $1.85 and $1.36. With such small gains for the casino, I'm wondering how much a casino makes off this game. I suppose that a good number of players do not know the best strategy, so this might tip there earnings even higher.

Also interesting about these results is the 5:3 ratio of losing to not losing. This would suggest that the agent should lose more. However, the agent can win a lot more than it loses. Each play only costs a dollar, but a win could be up to $250. Even with these large earnings, it's not enough to overcome the losing trend. It might seem like you have a good opportunity to win with Jacks or Better. However, in the long run, you will still lose like any other game in Las Vegas. :)

I've posted my code here for anyone to review. Also, please feel free to offer any suggestions for enhancements to the strategy or the code!

Tuesday, February 9, 2010

Summer Enrichment Program Website Now Live!

I'm pleased to announce the launch of the Summer Enrichment Program (SEP) Website! As I mentioned in my previous post, the SEP is a program designed to get young women excited about computer science. It's being held this summer at the University of San Francisco, most likely during the week of June 28th - July 2nd.



Thanks to Anna Hurley for designing the logo! It's perfect!

Saturday, February 6, 2010

Minimizing the Gender Gap in Computer Science

I'm female. And I like computers and technology. I'm a minority. This has led me to try to understand the gender gap in technology. In this post, I take a look at some recent studies in the field and look at my own experience to develop some ideas that might help minimize this gap.

First, I should ask the question: why do we even need women in technology? The field seems to be progressing fine without them, right? A good reason to have women more involved in technology is because groups with both women and men tend to work better together. According to this article in Forbes, papers that have both men and women authors have 42% more citations than papers with a single gender. It seems that women have a very positive effect on any collaborative work!

Next comes the question: why are girls avoiding the tech industry? One study suggests that the environment itself prevents them from taking an interest. A group of scientists recently performed a study on students' interest in computer science. Students were asked to sit in 2 very different rooms and asked to fill out a survey about their interest in computer science. Given a room decorated with very geeky paraphernalia, the girls were much less likely to be interested in computers than if they were in a room with gender-neutral decor.

Also, as a child, I remember my own experiences. I liked playing computer games as a kid, and I liked programming my TI-85 as a high school student. But I never took a computer class in high school. In fact, I never even considered computers as a potential career path. How could this happen? One obvious reason was that computer classes were never required in elementary school, middle school, or high school. Another major factor was peer pressure. I'm afraid to say this, but only the geekiest, most unpopular kids were taking the computer courses at my high school. Taking such a course would be akin to "social suicide".

So, what can be done to get more girls in the industry? First, we should address the fact that the environment might have an affect on students' perception of computer science. We should make an effort to minimize any male-oriented objects in the environment and make the classrooms as gender neutral as possible. One study suggests that we should eliminate all boys from the room, since single-sex classrooms increase the likelihood that girls will take interest in computer science.

Another suggestion I have is based on my own experience. I think we should require students to learn how to use computers starting from elementary school all the way through high school. By exposing all students to computers, these classes would not be reserved for the geekiest, most unpopular students. Plus, it would allow all students to explore computers as a potential career. We already impose requirements for biology, math, and English, why not computers too? Computer engineering is a highly viable career option (maybe more than English). And, even if computer science isn't a student's main interest, knowing how to use computers and technology is integral to almost every professional career and would be useful for everyone.

Finally, I think we need to reach out to girls on more areas than just schools. There are many other media that can help. I'm so excited to see that Barbie is reaching out to young girls! Let's not stop here, though! TV programs for young children can do the same by including more girls using computers and studying computer science. Teen magazines can include articles about programming and majoring in computer engineering. Movies can include more female computer scientists.

With all these ideas in mind, we can start making a difference! I'm going to start this semester. I'm helping plan the Summer Enrichment Program for high school girls interested in computer science. The program is held by my grad school, USF. It runs for a week in the summer and exposes girls to a wide range of topics in computer science. Last year, we taught them about computer architecture, python programming, and java programming. The program is such a great event. Last year, the girls showed marked increase in their interest in computer science, approximately 1.5 points on a scale of 5. I look forward to this year's program and hope to inspire the girls just as much!

If you are interested in minimizing the gender gap too, I urge you to get involved!

Tuesday, February 2, 2010

Forms with File Fields - Java

Background

The standard HttpServletRequest class is not capable of handling file input fields in an HTML form. If you use this class, you will only receive the file name as the parameter value and not the entire file itself. In this tutorial, I will show you how to create the HTML form and process the form data using the com.oreilly.servlet package.

Step 1: Creating the Form

To create a form with a file input field, you need two add 2 additional features to any form: an enctype attribute to the form tag and a file input field. Set the enctype as "multipart/form-data". Here is what the code would look like:

 <form action="upload" id="upload" name="upload" enctype="multipart/form-data" method="post">
Name: <input type="text" name="name" id="name" />
Attachment: <input type="file" name="attachment" id="attachment" />
<input type="submit" value="submit" />
</form>
Step 2: Processing the form data

Like I mentioned earlier, the standard HttpServletRequest class is not capable of handling file input fields. Luckily, there are several easy-to-use libraries already created for capturing file data from multipart forms. I recommend using the com.oreilly.servlet package. The methods in this library use the same names as the HttpServletRequest methods, making is very easy to learn and use.

Download the jar file from the servlets.com website. Install the jar file to use with your project. (If you are using Eclipse, you can find instructions on how to install .jar files in your project here.)

Import the package at the top of your file.

 import com.oreilly.servlet.*;  

Create a new MultipartRequest object. The constructor can accept several parameters (see the API). I like to use the constructor accepting the following parameters: the HttpServletRequest from the doPost method, the path to the directory where file is to be saved, and the size limit of the files. If you do not specify a size limit of the files, a default of 1MB will be set.

 MultipartRequest mpRequest = new MultipartRequest(request, "/Users/bob/project/attachments", 3000000);  

Once the MultipartRequest object has been initialized, you can retrieve form data. Use the getParameter method to retrieve form input field data for all fields other than the file field. Use this method with the input field name as a parameter to retrieve the value of the field.

 String name = mpRequest.getParameter("name");  

For the file field, use the getFile method to retrieve the file uploaded in the file input field. Use the name of the file input field as a parameter. This method returns a file object, which is the file that was uploaded in the form. The file is saved in the directory specified when creating the MultipartRequest object.

 File file = mpRequest.getFile("attachment");  

That's about it! Simple, right? There are several other methods that you can use in the com.oreilly.servlet package, which are discussed in the API.

Saturday, January 23, 2010

HTML emails

HTML emails can be hard to create since most of the tricks used to develop web pages are completely unusable when creating HTML emails. CSS is not well supported, images can be a disaster, and layout is best done using tables (gasp). www.MailChimp.com describes the situation quite nicely: "Think back to the old days of the web browser wars. When Netscape and Microsoft were duking it out. There was no Flash plugin. It was only shockwave. Put away your DIVs and DHTML. Dust off those tables, shim.gifs, and font tags, because you’re gonna need 'em."

Webmail services

Webmail services strip the <html> <head> and <body> tags from an HTML email. Keep this in mind when coding emails.

Email width

Since most people view emails in a reading pane rather than full screen, set the email width at:

500-600 pixels

Images

Images are not displayed immediately in all email clients (to name a few: Outlook 2003, Outlook Express, AOL, Gmail, Thunderbird) . The user has to allow the email client to display images before they are shown. Since this is the case, do the following when using images in emails:

  • Enter a height and a width in the image tag to act as a placeholder
  • Use alternative text (alt="")
  • Don't use images for important content
  • Don't use background images. They are not displayed by all email clients.
Javascript, ActiveX, Flash, and embedded movie files

Don't even think about it.

Tables

Don't use div tags to position elements since some email clients strip div tags. Using tables for layout is probably the best way to create a well-formed email. www.MailChimp.com suggests using separate tables for the header, body, and footer of emails since some email clients do not handle colspan (Lotus Notes).

As noted above, Webmail services strip the <body> tag from an email. If you have a background color applied to the body tag, this will get stripped as well. Use a 100% wide table to hold your email and apply the background color to the table.

CSS

There is no standard support for CSS in today's email clients. No email clients allow for external style sheets, and, since Webmail services strip the portion of the email, it is best to place any style within the body of the email. In addition, CSS positioning is not supported by email clients.

To keep email style consistent throughout all email clients, review the following table :

Email ClientSupports…
Yahoo! MailCSS
HotmailCSS
Outlook and Outlook ExpressCSS
GmailInline CSS
Lotus NotesInline CSS
AOL 9CSS
ThunderbirdCSS
Mac MailCSS
EntourageCSS
Eudora for the MacNo CSS


Testing

Email clients to test:
  • Web: Hotmail, Yahoo! Mail, Yahoo! Mail Beta, Gmail
  • PC – Outlook, Outlook Express, AOL 9, Thunderbird, Lotus Notes
  • Mac – Mac Mail, Entourage, Eudora
Summary:
  • Inline CSS is most widely supported
  • Don't use CSS to position elements
  • Use tables for layout (maybe even use separate tables for header, body, footer)
  • If applying a background color, wrap the whole email in a 100% width table
  • Test in all email clients before sending your email!
Where I Got My Info

Monday, January 11, 2010

Source Code Formatter

Just wanted to send out a quick thanks to the developers of the source code formatter tool I used to format the code in my previous post! It's very helpful; I'm sure I'll be using it many times in the future! Check it out here: http://codeformatter.blogspot.com/2009/06/about-code-formatter.html.

Sunday, January 10, 2010

How to Create a Bookmarklet Like FriendFeed's

Background

I've been working on a gift web site called Gifty with my fiance, James. The site allows users to create groups, such as a family group. The user can then add their gift wishlist to the group. Other users in the group can see the person's wishlist and select which gifts to get that person.

I thought a great addition for the site would be a bookmarklet similar to FriendFeed's. The user would be able to add a gift from any site they're on. For example, if they're on the Amazon Kindle page, when they clicked on the bookmarklet, they would be able to add the Kindle to their wishlist.

Before I began, I had to figure out how FriendFeed did it. I encountered a couple surprises along the way, and thought I'd share with everyone the details of the code.

The Concept

The concept is pretty simple:
  1. Create a link that is purely a JavaScript method call, which acts as the bookmark
  2. Have this JavaScript method add a div element to any page using JavaScript
  3. Display a form in this div element
  4. Have the form submit a post to a URL on Gifty
  5. Remove the div element from the page.
Seems easy, right? Well, yes, the concept is easy, but there are a few tricky aspects that I didn't foresee. I'll go through each step, describing how I (and FriendFeed) accomplished the goal.

1. Create a link that is purely a JavaScript method call, which acts as the bookmark

This step was very simple. Drag the following link to your bookmark bar and test it out:

Bookmark

You should see a JavaScript alert pop up, while you remain on the same page. (If nothing happens, you might have JavaScript disabled.) What is the code behind this link? It's simply

 javascript:alert('hi')


Now, I wanted to do something more complex than a simple alert, of course, so I reviewed FriendFeed's bookmark JavaScript, which is as follows:

javascript:void((function(){
var%20e=document.createElement('script');
e.setAttribute('type','text/javascript');
e.setAttribute('src','http://friendfeed.com/share/bookmarklet/javascript');
document.body.appendChild(e)})())


As you can see, their function creates a new script element with their JavaScript file as the source and appends this to the page. How clever! All updates to the code can be changed in their file. As long as the link to the JavaScript file remains the same, new versions will be pushed automatically to all users.

I replaced FriendFeed's src file with my own, bookmarklet.js, and was on my way to the next step.

2. Have this javascript method add a div element to any page using javascript

Adding the div tag to the page is pretty simple, too. I applied a similar approach to FriendFeed, and used the following code:

 var container = document.createElement("div");
container.style.padding = "0";
container.style.margin = "0";
container.style.border = "1px solid #000000";
container.id = "giftybox";
container.style.position = "absolute";
container.style.top = "0";
container.style.right = "0";
container.style.zIndex = 100000;
container.style.width = "350px";
container.style.height = "210px";
container.style.backgroundColor = "white";
document.body.appendChild(container);


A new document element is created and all the style is applied to the element. Important style attributes to note are the absolute positioning and the z-index. The absolute positioning allows the element to be placed exactly where we ask it to be placed on the page, in spite of any other elements that might be in the way. The z-index means that this element should be placed at a position of 100,000 above any other elements. This takes an assumption that all other HTML elements on the page have a z-index smaller than this. If there's an element with a z-index of 100,000+, then that element will display in front of my div. However, I'm willing to take the bet that most HTML elements will not have such a large z-index, and have settled with 100K.

This JavaScript is placed in the JavaScript file mentioned above. To make sure my JavaScript didn't interfere with any other JavaScript on the page, I used namespaces. I created a new variable called giftyFunctions and set this equal to an anonymous function with the return value being a map of the function name to another anonymous function. Confused? Here's what it looks like in the code:

 var giftyFunctions = function() {
return {
addDiv : function() {
//code from above
}
}
}


Now, at the bottom of the file, I can call the function to add the div element as follows:

 giftyFunctions().addDiv();


This should, hopefully, eliminate the possibility that another function on the page would have the same name and would interfere with my function.

3. Display a form in the div element

The form was a bit trickier. The HTML had to be hosted on our own servers for two reasons:
  1. We needed to make sure the user is logged in before showing the form
  2. We needed to add user-specific data to the page (e.g., what groups they belong to)
To get around this, I looked to FriendFeed again. Again, they provided a very clever solution: they added an iframe element to the div element added earlier. The source was set to an HTML page hosted on their own servers. I applied a similar approach, creating the following HTML page:

 <body onload="addText();">
<p style="float:right;"><a href="javascript:closeBox('close');">Close</a></p>
<p><strong>Gifty</strong></p>
<form id="frameform">
<p><label for="giftDescription">Gift Name:</label> <input type="text" name="_giftDescription" id="_giftDescription" /></p>
<p><label for="giftLink">Link:</label> <input type="text" name="_giftLink" id="_giftLink" /></p>
<p><label for="groupId">Group:</label> <select name="_groupId" id ="_groupId" >
{% for group in userGroups %}
<option value={{ group.key.id }}>{{ group.groupName }}</option>
{% endfor %}
</select></p>
<input type="button" value="Submit" onclick="submitForm();" />
</form>
</body>


Note that this form does not contain an action, and the submit button is just a button with a JavaScript onclick event. This differs from FriendFeed's form, in which they use an actual submit input. I'm using a button since I want to control the submission of the form via the JavaScript function, submitForm. More about this later.

The iframe source is set to the above HTML by adding a couple lines of JavaScript to the JavaScript file:

 container.innerHTML = '<iframe style="width:100%;height:100%;border:0px;" id="giftyframe"></iframe>';
document.body.appendChild(container);
giftySetIframe();


The setIFrameLocation code is as follows:

 function setIFrameLocation() {
var iframe;
if (navigator.userAgent.indexOf("Safari") != -1) {
iframe = frames["giftyframe"];
} else {
iframe = document.getElementById("giftyframe").contentWindow;
}
if (!iframe) return;
var url = 'http://www.kathrynbrisbin.com/development/practice/frame.html'
url += '#gifty?giftname=' + document.title;
url += '&giftylocation=' + window.location.href;
try {
iframe.location.replace(url);
} catch (e) {
iframe.location = url; // safari
}
}


Note, there are two parameters added to the end of the iframe URL: giftname and giftylocation. These two parameters pass important information to the iframe so that the two input fields in the form, Gift Name and Link, can be given initial values. These are added via the addText function called on page load.

 function addText() { 
var windowLocation = window.location.href;
var params = windowLocation.split('#gifty?')[1];
var subparams = params.split('&gifty');
var title = unescape(subparams[0].split('=',2)[1]);
parentLocation = subparams[1].substring(subparams[1].indexOf('=')+1,subparams[1].length);
document.getElementById('_giftDescription').value = title;
document.getElementById('_giftLink').value = parentLocation;
}


Now that the form is added to the div, it's time to move on to the form submission.

4.
Have the form submit a post to a URL on Gifty

As I mentioned earlier, the form submission is performed via the submitForm method. This method uses a jQuery AJAX method to submit the form data via a post. The code is extremely simple, thanks to jQuery's help:

 function submitForm() {
$.post("/bookmarklet", $("#frameform").serialize());
...
}


The $.post function, a jQuery function, takes a URL and data as parameters. It makes an asynchronous call to the URL and posts the data to the URL. Since the call is asynchronous, it allows the user to remain on the current page. More information can be found on the jQuery website.

5. Remove the div element from the page.

Once the form is submitted, the div element needs to be removed from the page. To do so, control needs to return to the parent window, since the iframe itself can not remove the div element from the parent page. Unfortunately, JavaScript does easily allow for control to return to the parent. If the source of an iframe has a different domain from the parent, the parent property can not be used. So, what to do??

Again, I looked to FriendFeed to see their approach. Their solution was unique. In the initial JavaScript file, they set up an interval for a function that checks the page URL every 50 milliseconds. The function checks whether a message has been added to the end of the URL. The message begins with # + a FriendFeed specific string + =. Depending on the message, the function performs different actions, including removal of the div element.

I mimicked this design, and added the interval to my bookmarklet.js file:

 var interval = window.setInterval(function(){
giftyFrameMessage();
}, 50);


The giftyFrameMessage function finds the message at the end of the URL and sends this to another function to handle messages:

 function giftyFrameMessage(){
var gCurScroll = giftyScrollPos();
var hash = location.href.split('#');
if (hash.length > 1 && hash[hash.length - 1].match('gifty') != null) {
location.replace(hash[0] + "#");
giftySetScroll(gCurScroll);
giftyHandleMessage(hash[hash.length - 1]);
}
}


Finally, the giftyHandleMessage simply closes the div element:

 function giftyHandleMessage(msg){
giftyClose();
}
//close the box
function giftyClose(){
var giftybox = document.getElementById('giftybox');
giftybox.parentNode.removeChild(giftybox);
window.onscroll = null;
}


This function can be more complex and perform different actions based on the message. However, my code required nothing more complex at this point.

Now that I had the interval set up, I had to update the URL from the iframe. The iframe doesn't have access to the parent, but it does have access to "top". After submitting the form, I called the following method:

 function closeBox(message) {
var url = parentLocation + "#gifty=" + message;
try {
top.location.replace(url);
} catch (e) {
top.location = url;
}
}


This changes the URL, adding the message to the end. Once the giftyFrameMessage function is called, it finds the #gifty string in it, and then closes the div.

And that's the majority of the code for the FriendFeed bookmarklet! Of course there's much more to it than this, but this will help get you started!

Saturday, January 9, 2010

Genetic Algorithms - Roulette Elitism and Sorting

I wrote this paper on Genetic Algorithms for my AI class. I tested the effects of sorting on Roulette Elitism. Based on my studies, I believe that sorting actually has a positive effect on Roulette Elitism and should be included in the algorithm. Please read my paper for more details!

My First Android Application - Device photos

This tutorial shows you the code to get metadata about the images on your Android phone and also how to set up your development environment to test the code locally using the Emulator. This tutorial assumes that you have properly installed version 1.5 of the Android SDK, are using Eclipse to develop Android applications, and have installed the ADT for Eclipse. Also, these instructions were written for Mac users. I will link to instructions for Linux users whenever I can. PC users - you're on your own.

Creating an SD Card

First and foremost: you should know that the photos you take with your Android phone are stored on the SD Card. The images are stored in the directory: /sdcard/dcim/Camera. We're going to mimic the SD Card locally. The local SD Card is actually a disk image. No SD Card disk image comes with Android SDK, so we need to create one.

To create the SD Card image, navigate to your /tools directory using the Terminal. Depending on your PATH variable, you might have to add the tools directory to your PATH. This will allow you run the files in the tools directory. In my case, I ran the command:

export PATH=$PATH:/Users/kat/JavaLibraries/android1.5/tools

Next, we need to actually create a disk image of the SD Card. Run the command:

mksdcard 1024M .img

In my case, I ran the command:

mksdcard 1024M myimage.img

If you look in the tools directory, you should see a new file in there called 'myimage.img' (or whatever you decided to call it). Please make sure you use the .img extension, since this will help you later when mounting the image. Which brings me to my next topic: mounting the image. :)

Adding Folders/Images to the SD Card Image

In order to create folders and upload files to our new image, I found it easiest to use the Disk Utility on my mac to mount the 'myimage.img' file. If you're using Linux, try the directions here.

Start the Disk Utility application (it's in the System Utilities folder in the Applications directory). Once started, Select File > Open Disk Image... Navigate to your newly created disk image in the tools directory. Select OK. You should see the disk image in the list. If it's not already opened, right click on the image and select "Open". The SDCARD should appear under it. Right-click on the SDCARD and select "Reveal in Finder". Double-click on the "sdcard" folder in the Finder window. Once in this folder, you can create new folders and copy and paste images within the folders. In our case, we want to create the following directory structure under sdcard:

dcim/Camera

Within the Camera folder, place some images. JPGs should work fine. Not too sure about any other formats, I haven't tried them yet.

Finally, we need to unmount the disk image. Right-click on the SDCARD in the Disk Utility window. Select 'unmount sdcard'. Then eject the disk image by right-clicking on it and select "eject".

Perfect, we're almost done! Not too difficult, right?

Setting up Eclipse to Recognize the New SD Card Image

Next step is to set up Eclipse to run our new sdcard image with the Emulator instead of the non-existent one it uses by default. There are 2 possible places to make this change in Eclipse. I found one doesn't work at all, the other does. I'm going to include both places, though, just in case.

The one that didn't seem to work - In Eclipse, click on the Eclipse Menu > Preference. Expand the Android menu, select Launch. In the Default emulator options, enter

-sdcard /[path]/[imagename].img

In my case, I entered:

-sdcard /Users/kat/JavaLibraries/android1.5/tools/myimage.img

The one that did work - In Eclipse, create an Android project titled 'PhotoFun' (or whatever). Right click on the Project, select Run As > Open Run Dialog... In the Run Dialog Box, under the "Target" tab, enter the same text as above (-sdcard /[path]/[imagename].img) into the Additional Emulator Command Line Options box.

THE CODE

If you didn't create the Android Project as I mentioned in Step 2 directly above, please do so now. Call the Project 'PhotoFun' or whatever you want.

First, open the res > layout > main.xml file. Replace the TextView element with the following:

<TextView
android:id="@+id/imageData"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text=""
/>

Next, open the main class (src > PhotoFun.java). Add the following lines to the onCreate method after setContentView(R.layout.main);

TextView imageData = (TextView) findViewById(R.id.imageData);

String[] s = {MediaStore.Images.Media.TITLE,
MediaStore.Images.Media.DATE_MODIFIED, MediaStore.Images.Media.SIZE};
Cursor cursor = managedQuery(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, s, null,null,null);

StringBuilder string = new StringBuilder();
while(cursor.moveToNext()) {
String theFile =
"Title: " + cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.TITLE))
+ "\n" +
"Date Mod: " +
cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATE_MODIFIED))
+ "\n" +
"Size: " +
cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.SIZE));

string.append(theFile + "\n\n");
}

imageData.setText(string);

Now run your application as a Android app. Once the Emulator is loaded you should see a list of all the photos in the /sdcard/dcim/Camera directory! Awesome!

Hello, World

OK, yea, the title to this post is lame and much overused, but I'm using it! This is my first post in, hopefully, a series of weekly (biweekly? monthly?) posts about programming. I'm a second year masters student at the University of San Francisco, studying web sciences. I've learned a lot during my studies and would like to share some of this knowledge with the rest of the world!

Thanks for stopping by!