Recently in Thoughts / Misc Category

Array#natural_sort

| | Comments (0)

Sitting in omnifocus for ~4 years...

require 'fileutils'

20.times do |n|
  FileUtils.touch "f#{n}.txt"
end

p Dir["f*.txt"].sort
# => ["f0.txt", "f1.txt", "f10.txt", "f11.txt", ..., "f2.txt", "f3.txt", ...]

class Array
  def human_sort
    sort_by { |item| item.to_s.split(/(\d+)/).map { |e| [e.to_i, e] } }
  end
end

p Dir["f*.txt"].human_sort
# => ["f0.txt", "f1.txt", "f2.txt", "f3.txt", "f4.txt", "f5.txt", "f6.txt", ...]

Enumerable#uniq_by

| | Comments (1)

Random code sitting around waiting to be blogged...

module Enumerable
  def uniq_by
    r, s = [], {}
    each do |e|
      v = yield(e)
      next if s[v]
      r << e
      s[v] = true
    end
    r
  end
end

Simulating .PHONY in Rake

| | Comments (0)

Makefiles have a construct called .PHONY. You declare your task a non-filesystem based task like so:

.PHONY: mytask

and then make knows that it shouldn't force your task to run just because "mytask" doesn't exist in the filesystem.

Rake doesn't bother with such nonsense. It doesn't assume that all tasks map to the filesystem. That's great. What's not great is that it does assume that if your file task has non-file-task dependencies, that they should probably rebuild even if those tasks have run. The problem code looks like:

def out_of_date?(stamp) # on FileTask
  @prerequisites.any? { |n| application[n, @scope].timestamp > stamp}
end

In my case, I was trying to intelligently hook up some generated files to isolate so they could access their compiler:

file "lib/ruby18_parser.rb" => :isolate
file "lib/ruby19_parser.rb" => :isolate

The problem is that :isolate is a regular task, and they calculate their dependencies like so:

def timestamp # on Task
  prerequisite_tasks.collect { |pre| pre.timestamp }.max || Time.now
end

:isolate doesn't have any prerequisites, so it falls back to Time.now. This is wrong in my case, as it forces my parser files to regenerate every time.

What I need is a prerequisite that fixes this with a timestamp earlier than my files:

def (task(:phony)).timestamp # omg I love/hate this grammar construct
  Time.at 0
end

task :isolate => :phony

and everything is happy.

I'm going to officially ask that :phony gets added to rake so others can be spared this pain.

Reverse Sort

| | Comments (0)

It's fairly common knowledge that you can sort and reverse a collection in one pass by using sort_by with a negated number:

tally = [["a", 2], ["b", 1], ["c", 2]]

p tally.sort_by { |(str, num)| [-num, str] }
# => [["a", 2], ["c", 2], ["b", 1]]

but what happens if you need to reverse something like strings? Strings don't respond to unary minus, so you can't do this:

tally.sort_by { |(str, num)| [-str, num] } rescue nil
# => undefined method `-@' for "a":String (NoMethodError)

What you want to do is make this behavior dynamic and available everywhere:

module Reversed
  def <=> target
    -super
  end
end

class Object
  def -@
    self.dup.extend Reversed
  end
end

p tally.sort_by { |(str, num)| [-str, num] }
# => [["c", 2], ["b", 1], ["a", 2]]

What would be really special is if Ruby allowed you to apply modules multiple times so that -(-a) == a.

Check out the time it takes just to start up some of these systems:

    # # of iterations = 1000
    #                                user     system      total        real
    # true                       0.030000   0.180000   1.780000 (  2.698916)
    # ruby -e 0                  0.040000   0.170000   4.770000 (  6.149741)
    # ruby -e 'eval "1 + 1"'     0.030000   0.180000   6.030000 (  6.811871)
    # csi -e 0                   0.050000   0.200000   5.720000 (  8.098138)
    # perl -e 0                  0.050000   0.200000   6.410000 (  8.193336)
    # csi -e eval 1+1 (scheme)   0.050000   0.230000   7.060000 (  8.663545)
    # perl -e 'eval "1 + 1"'     0.040000   0.200000   7.580000 (  8.552224)
    # ruby193 -e 0               0.050000   0.210000  14.700000 ( 15.981277)
    # ruby193 -e 'eval "1 + 1"'  0.060000   0.220000  14.760000 ( 16.010711)
    # python -c 0                0.060000   0.280000  46.230000 ( 49.630480)
    # python -c 'eval("1 + 1")'  0.060000   0.250000  47.080000 ( 49.251939)

What's up with python? Still using getc to load all your core libraries? dtruss doesn't seem to think so but obviously something is going on.

(I think something is wrong with my build of ruby 1.9.3, I'll look into it)

From:

    require 'benchmark'

    MAX = (ARGV.shift || 1_000).to_i

    puts "# of iterations = #{MAX}"
    Benchmark::bm(25) do |x|
      def x.go cmd, name=cmd
        report name do
          for i in 0..MAX do
            system cmd
          end
        end
      end

      ruby18 = "~/.multiruby/install/1.8.7-p330/bin/ruby"
      ruby18 = "ruby"
      ruby19 = "~/.multiruby/install/1.9.3-p0/bin/ruby --disable-gems"
      csi_code = "csi -e '(eval (with-input-from-string \"(+ 1 1)\" read))'"

      x.go "true"
      x.go "#{ruby18} -e 0",                "ruby187 -e 0"
      x.go "#{ruby18} -e 'eval \"1 + 1\"'", "ruby187 -e 'eval \"1 + 1\"'"
      x.go "csi -e 0"
      x.go "perl -e 0"
      x.go csi_code,                        "csi -e eval 1+1 (scheme)"
      x.go "perl -e 'eval \"1 + 1\"'"
      x.go "#{ruby19} -e 0",                "ruby193 -e 0"
      x.go "#{ruby19} -e 'eval \"1 + 1\"'", "ruby193 -e 'eval \"1 + 1\"'"
      x.go "python -c 0"
      x.go "python -c 'eval(\"1 + 1\")'"
    end

45° Perspective

| | Comments (1)

I was working on Project Euler problem #15 a few weeks back. It's based on traveling routes from one corner of a grid to the opposite corner. The fact that it is on a fixed grid means that it is related to the Manhattan distance. Originally, I kept thinking about this problem the way it was stated, going from point A to B and was terribly stuck. There are ways to brute force it, but they're not practical at all. Eventually, I decided to stop thinking about it in terms of Manhattan streets. I flipped the diagram 45° and started thinking about it from the bottom/destination up and how many paths there were up to the source:

maffs

Almost immediately the problem solved itself with a constant-time solution.

Sometimes it is simply a matter of changing your perspective on a problem to make the solution obvious.

Ruby Classes are Open, Damnit

| | Comments (0)

Ruby Classes are Open. Use it to your advantage. Ruby's Date.parse changed between 1.8 and 1.9 so that European date formats are preferred over US date formats. I've seen a lot of American rubyists whine about this. It took about 5 minutes looking at Date code to figure out that the solution is trivial:

#!/usr/bin/ruby -w

require 'date'

class Date
  class << self
    alias :_temp_us  :_parse_us
    alias :_parse_us :_parse_eu
    alias :_parse_eu :_temp_us
  end
end if RUBY_VERSION =~ /1\.9/

puts Date.parse("10/4/2010") # => 2010-10-04

Recently, Jeremy Evans' released ruby-american_date to address this issue. You should probably use his solution over mine. Mine is just here as an illustration that it really doesn't take much time or effort to address your whining.

I've been refining my omnifocus strategy a lot of late. One thing I've done is set all of my review intervals to be a prime number of weeks and then evenly staggered them based on their priority. I've got 5 different priority levels on my projects and they're currently spread out like so:

          date\ tot 1w 2w 3w 5w 7w 
2011-12-16 Fri:  23  7  6  3  6  1 
2011-12-23 Fri:  17  0  6  3  7  1 
2011-12-30 Fri:  11  0  0  3  7  1 
2012-01-06 Fri:   8  0  0  0  7  1 
2012-01-13 Fri:   8  0  0  0  7  1 
2012-01-20 Fri:   1  0  0  0  0  1 
2012-01-27 Fri:   1  0  0  0  0  1 
         total:  69  7 12  9 34  7

This way all of my priority 1 projects are looked at every week. Half of my priority 2 are seen every week. Etc. This normalizes the amount of projects I need to review on any given week. It has helped a lot because they used to clump up a fair amount. If you only have 10-20 projects, this probably isn't as big of a deal to you, but when you get to 40+, it starts to matter.

One thing I had problems with was getting the values set easily. I finally figured out the applescript mumbo-jumbo. Setting the review interval was tough, as structs are treated differently in applescript for some reason. The trick is to set it to a local variable, modify it, and then set it back:

tell application "OmniFocus"
  tell default document
    repeat with proj in flattened projects
      set ri to review interval of proj
      if (steps of ri) > 3 then
        set steps of ri to 7
        set review interval of proj to ri
      end if
    end repeat
  end tell
end tell

This sets any project that has a review interval of 4+ weeks to be 7 weeks. Tweak the numbers however you see fit and run it once to normalize everything.

Top Left Corner Analysis

| | Comments (0)

I screen-grabbed the original from some obscure presentation I read and can't find/recall anymore (*). Since I can't properly attribute I'm not going to include the original and instead have recreated the core concept in a single image.

To decide what to work on at any given time, plot tasks by difficulty and importance:

#alttext#

Start with the easier but important stuff, on good days work on harder stuff. on bad days work on less important stuff, and on terrible days just stay home.

*) I vaguely remember it was from a scheme developer on Mac OS? but I really don't remember where I found this.

ETA: I found it. Remembering scheme + mac os somehow triggered the word "pixie" and google came up with:

http://web.mac.com/Jay_Reynolds_Freeman/My/Software_files/WraithScheme.2.pdf

This is a really good talk. You should read it.

My favorite café decided to "fix the internet" by filtering everything outgoing over port 1024. Gtalk, IRC, perforce, and many other things for me broke. Gtalk and some other stuff often have an option to run encrypted over port 443 or something similar, but I was stuck for IRC and perforce.

I decided to set up a permanent ssh tunnel on my laptop. On OSX this is actually really easy and there are a lot of benefits from it. First, OSX supports ssh-agent with keychain integration, so once you give ssh-agent access to the keychain, all ssh interactions are cleaner and easier. Second, by using launchd, this is automatic upon login and then gets out of your way from then on (even after network changes!).

Below is my plist which I created using an old version of Lingon, and then hand-modified as I added more parameters. I wouldn't recommend newer versions of Lingon, honestly... It has been neutered into uselessness in my opinion.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>com.zenspider.ssh-tunnel</string>
  <key>OnDemand</key>
  <false/>
  <key>ProgramArguments</key>
  <array>
    <string>/usr/bin/ssh</string>
    <string>-C</string>
    <string>-N</string>
    <string>-c</string>
    <string>blowfish</string>
    <string>-o</string>
    <string>ServerAliveInterval=3</string>
    <string>-L</string>
    <string>1666:localhost:1666</string>
    <string>-L</string>
    <string>16667:localhost:16667</string>
    <string>-D</string>
    <string>8081</string>
    <string>e</string>
  </array>
</dict>
</plist>

This is really quite simple. My job is called 'com.zenspider.ssh-tunnel' and is stored in ~/Library/LaunchAgents/com.zenspider.ssh-tunnel.plist. It is an "OnDemand" job, which in this case means it fires up when you login and stays up at all times. The args being run are:

  • /usr/bin/ssh = duh
  • -C = compress the connection
  • -N = No remote command to execute... ie, just set up a tunnel.
  • -c blowfish = Use the fastest cipher available.
  • -o ServerAliveInterval=3 = ping the server every 3 seconds
  • -L 1666:localhost:1666 = Forward localhost:1666 to remote 1666
  • -L 16667:localhost:16667 = Forward localhost:16667 to remote 16667
  • -D 8081 = Set up a SOCKS proxy, in case I want to tunnel HTTP
  • e = the name of my server, as defined in ~/.ssh/config

Next, start up the service using:

% launchctl load ~/Library/LaunchAgents/com.zenspider.ssh-tunnel.plist

Finally, I change my perforce and IRC configurations to use localhost instead of my server. Now, everything is tunnelled over port 22 via ssh. With blowfish and compression, I'm not noticing any delays at all (esp given the café's shared internet speeds).

About this Archive

This page is a archive of recent entries in the Thoughts / Misc category.

Seattle.rb is the previous category.

Toys is the next category.

Find recent content on the main index or look in the archives to find all content.

Pages

Powered by Movable Type 4.32-en