]> git.cworth.org Git - sup/blob - lib/sup/util.rb
many many changes. this is what happens when i have 5 hours on an airplane
[sup] / lib / sup / util.rb
1 require 'lockfile'
2 require 'mime/types'
3 require 'pathname'
4
5 ## time for some monkeypatching!
6 class Lockfile
7   def gen_lock_id
8     Hash[
9          'host' => "#{ Socket.gethostname }",
10          'pid' => "#{ Process.pid }",
11          'ppid' => "#{ Process.ppid }",
12          'time' => timestamp,
13          'pname' => $0,
14          'user' => ENV["USER"]
15         ]
16   end
17
18   def dump_lock_id lock_id = @lock_id
19       "host: %s\npid: %s\nppid: %s\ntime: %s\nuser: %s\npname: %s\n" %
20         lock_id.values_at('host','pid','ppid','time','user', 'pname')
21     end
22
23   def lockinfo_on_disk
24     h = load_lock_id IO.read(path)
25     h['mtime'] = File.mtime path
26     h
27   end
28
29   def touch_yourself; touch path end
30 end
31
32 class Pathname
33   def human_size
34     s =
35       begin
36         size
37       rescue SystemCallError
38         return "?"
39       end
40
41     if s < 1024
42       s.to_s + "b"
43     elsif s < (1024 * 1024)
44       (s / 1024).to_s + "k"
45     elsif s < (1024 * 1024 * 1024)
46       (s / 1024 / 1024).to_s + "m"
47     else
48       (s / 1024 / 1024 / 1024).to_s + "g"
49     end
50   end
51
52   def human_time
53     begin
54       ctime.strftime("%Y-%m-%d %H:%M")
55     rescue SystemCallError
56       "?"
57     end
58   end
59 end
60
61 ## more monkeypatching!
62 module RMail
63   class EncodingUnsupportedError < StandardError; end
64
65   class Message
66     def add_file_attachment fn
67       bfn = File.basename fn
68       a = Message.new
69       t = MIME::Types.type_for(bfn).first || MIME::Types.type_for("exe").first
70
71       a.header.add "Content-Disposition", "attachment; filename=#{bfn.to_s.inspect}"
72       a.header.add "Content-Type", "#{t.content_type}; name=#{bfn.to_s.inspect}"
73       a.header.add "Content-Transfer-Encoding", t.encoding
74       a.body =
75         case t.encoding
76         when "base64"
77           [IO.read(fn)].pack "m"
78         when "quoted-printable"
79           [IO.read(fn)].pack "M"
80         when "7bit", "8bit"
81           IO.read(fn)
82         else
83           raise EncodingUnsupportedError, t.encoding
84         end
85
86       add_part a
87     end
88
89     def charset
90       if header.field?("content-type") && header.fetch("content-type") =~ /charset="?(.*?)"?(;|$)/
91         $1
92       end
93     end
94   end
95 end
96
97 class Range
98   ## only valid for integer ranges (unless I guess it's exclusive)
99   def size 
100     last - first + (exclude_end? ? 0 : 1)
101   end
102 end
103
104 class Module
105   def bool_reader *args
106     args.each { |sym| class_eval %{ def #{sym}?; @#{sym}; end } }
107   end
108   def bool_writer *args; attr_writer(*args); end
109   def bool_accessor *args
110     bool_reader(*args)
111     bool_writer(*args)
112   end
113
114   def defer_all_other_method_calls_to obj
115     class_eval %{
116       def method_missing meth, *a, &b; @#{obj}.send meth, *a, &b; end
117       def respond_to? meth; @#{obj}.respond_to?(meth); end
118     }
119   end
120 end
121
122 class Object
123   def ancestors
124     ret = []
125     klass = self.class
126
127     until klass == Object
128       ret << klass
129       klass = klass.superclass
130     end
131     ret
132   end
133
134   ## "k combinator"
135   def returning x; yield x; x; end
136
137   ## clone of java-style whole-method synchronization
138   ## assumes a @mutex variable
139   ## TODO: clean up, try harder to avoid namespace collisions
140   def synchronized *meth
141     meth.each do
142       class_eval <<-EOF
143         alias unsynchronized_#{meth} #{meth}
144         def #{meth}(*a, &b)
145           @mutex.synchronize { unsynchronized_#{meth}(*a, &b) }
146         end
147       EOF
148     end
149   end
150
151   def ignore_concurrent_calls *meth
152     meth.each do
153       mutex = "@__concurrent_protector_#{meth}"
154       flag = "@__concurrent_flag_#{meth}"
155       oldmeth = "__unprotected_#{meth}"
156       class_eval <<-EOF
157         alias #{oldmeth} #{meth}
158         def #{meth}(*a, &b)
159           #{mutex} = Mutex.new unless defined? #{mutex}
160           #{flag} = true unless defined? #{flag}
161           run = #{mutex}.synchronize do
162             if #{flag}
163               #{flag} = false
164               true
165             end
166           end
167           if run
168             ret = #{oldmeth}(*a, &b)
169             #{mutex}.synchronize { #{flag} = true }
170             ret
171           end
172         end
173       EOF
174     end
175   end
176 end
177
178 class String
179   def camel_to_hyphy
180     self.gsub(/([a-z])([A-Z0-9])/, '\1-\2').downcase
181   end
182
183   def find_all_positions x
184     ret = []
185     start = 0
186     while start < length
187       pos = index x, start
188       break if pos.nil?
189       ret << pos
190       start = pos + 1
191     end
192     ret
193   end
194
195   ## one of the few things i miss from perl
196   def ucfirst
197     self[0 .. 0].upcase + self[1 .. -1]
198   end
199
200   ## a very complicated regex found on teh internets to split on
201   ## commas, unless they occurr within double quotes.
202   def split_on_commas
203     split(/,\s*(?=(?:[^"]*"[^"]*")*(?![^"]*"))/)
204   end
205
206   ## ok, here we do it the hard way. got to have a remainder for purposes of
207   ## tab-completing full email addresses
208   def split_on_commas_with_remainder
209     ret = []
210     state = :outstring
211     pos = 0
212     region_start = 0
213     while pos <= length
214       newpos = case state
215         when :escaped_instring, :escaped_outstring: pos
216         else index(/[,"\\]/, pos)
217       end 
218       
219       if newpos
220         char = self[newpos]
221       else
222         char = nil
223         newpos = length
224       end
225
226       case char
227       when ?"
228         state = case state
229           when :outstring: :instring
230           when :instring: :outstring
231           when :escaped_instring: :instring
232           when :escaped_outstring: :outstring
233         end
234       when ?,, nil
235         state = case state
236           when :outstring, :escaped_outstring:
237             ret << self[region_start ... newpos].gsub(/^\s+|\s+$/, "")
238             region_start = newpos + 1
239             :outstring
240           when :instring: :instring
241           when :escaped_instring: :instring
242         end
243       when ?\\
244         state = case state
245           when :instring: :escaped_instring
246           when :outstring: :escaped_outstring
247           when :escaped_instring: :instring
248           when :escaped_outstring: :outstring
249         end
250       end
251       pos = newpos + 1
252     end
253
254     remainder = case state
255       when :instring
256         self[region_start .. -1].gsub(/^\s+/, "")
257       else
258         nil
259       end
260
261     [ret, remainder]
262   end
263
264   def wrap len
265     ret = []
266     s = self
267     while s.length > len
268       cut = s[0 ... len].rindex(/\s/)
269       if cut
270         ret << s[0 ... cut]
271         s = s[(cut + 1) .. -1]
272       else
273         ret << s[0 ... len]
274         s = s[len .. -1]
275       end
276     end
277     ret << s
278   end
279
280   def normalize_whitespace
281     gsub(/\t/, "    ").gsub(/\r/, "")
282   end
283 end
284
285 class Numeric
286   def clamp min, max
287     if self < min
288       min
289     elsif self > max
290       max
291     else
292       self
293     end
294   end
295
296   def in? range; range.member? self; end
297 end
298
299 class Fixnum
300   def num_digits base=10
301     return 1 if self == 0
302     1 + (Math.log(self) / Math.log(10)).floor
303   end
304   
305   def to_character
306     if self < 128 && self >= 0
307       chr
308     else
309       "<#{self}>"
310     end
311   end
312
313   ## hacking the english language
314   def pluralize s
315     to_s + " " +
316       if self == 1
317         s
318       else
319         if s =~ /(.*)y$/
320           $1 + "ies"
321         else
322           s + "s"
323         end
324       end
325   end
326 end
327
328 class Hash
329   def - o
330     Hash[*self.map { |k, v| [k, v] unless o.include? k }.compact.flatten_one_level]
331   end
332
333   def select_by_value v=true
334     select { |k, vv| vv == v }.map { |x| x.first }
335   end
336 end
337
338 module Enumerable
339   def map_with_index
340     ret = []
341     each_with_index { |x, i| ret << yield(x, i) }
342     ret
343   end
344
345   def sum; inject(0) { |x, y| x + y }; end
346   
347   def map_to_hash
348     ret = {}
349     each { |x| ret[x] = yield(x) }
350     ret
351   end
352
353   # like find, except returns the value of the block rather than the
354   # element itself.
355   def argfind
356     ret = nil
357     find { |e| ret ||= yield(e) }
358     ret || nil # force
359   end
360
361   def argmin
362     best, bestval = nil, nil
363     each do |e|
364       val = yield e
365       if bestval.nil? || val < bestval
366         best, bestval = e, val
367       end
368     end
369     best
370   end
371
372   ## returns the maximum shared prefix of an array of strings
373   ## optinally excluding a prefix
374   def shared_prefix caseless=false, exclude=""
375     return "" if empty?
376     prefix = ""
377     (0 ... first.length).each do |i|
378       c = (caseless ? first.downcase : first)[i]
379       break unless all? { |s| (caseless ? s.downcase : s)[i] == c }
380       next if exclude[i] == c
381       prefix += first[i].chr
382     end
383     prefix
384   end
385
386   def max_of
387     map { |e| yield e }.max
388   end
389 end
390
391 class Array
392   def flatten_one_level
393     inject([]) { |a, e| a + e }
394   end
395
396   def to_h; Hash[*flatten]; end
397   def rest; self[1..-1]; end
398
399   def to_boolean_h; Hash[*map { |x| [x, true] }.flatten]; end
400
401   def last= e; self[-1] = e end
402   def nonempty?; !empty? end
403 end
404
405 class Time
406   def to_indexable_s
407     sprintf "%012d", self
408   end
409
410   def nearest_hour
411     if min < 30
412       self
413     else
414       self + (60 - min) * 60
415     end
416   end
417
418   def midnight # within a second
419     self - (hour * 60 * 60) - (min * 60) - sec
420   end
421
422   def is_the_same_day? other
423     (midnight - other.midnight).abs < 1
424   end
425
426   def is_the_day_before? other
427     other.midnight - midnight <=  24 * 60 * 60 + 1
428   end
429
430   def to_nice_distance_s from=Time.now
431     later_than = (self < from)
432     diff = (self.to_i - from.to_i).abs.to_f
433     text = 
434       [ ["second", 60],
435         ["minute", 60],
436         ["hour", 24],
437         ["day", 7],
438         ["week", 4.345], # heh heh
439         ["month", 12],
440         ["year", nil],
441       ].argfind do |unit, size|
442         if diff.round <= 1
443           "one #{unit}"
444         elsif size.nil? || diff.round < size
445           "#{diff.round} #{unit}s"
446         else
447           diff /= size.to_f
448           false
449         end
450       end
451     if later_than
452       text + " ago"
453     else
454       "in " + text
455     end  
456   end
457
458   TO_NICE_S_MAX_LEN = 9 # e.g. "Yest.10am"
459   def to_nice_s from=Time.now
460     if year != from.year
461       strftime "%b %Y"
462     elsif month != from.month
463       strftime "%b %e"
464     else
465       if is_the_same_day? from
466         strftime("%l:%M%P")
467       elsif is_the_day_before? from
468         "Yest."  + nearest_hour.strftime("%l%P")
469       else
470         strftime "%b %e"
471       end
472     end
473   end
474 end
475
476 ## simple singleton module. far less complete and insane than the ruby
477 ## standard library one, but automatically forwards methods calls and
478 ## allows for constructors that take arguments.
479 ##
480 ## You must have #initialize call "self.class.i_am_the_instance self"
481 ## at some point or everything will fail horribly.
482 module Singleton
483   module ClassMethods
484     def instance; @instance; end
485     def instantiated?; defined?(@instance) && !@instance.nil?; end
486     def deinstantiate!; @instance = nil; end
487     def method_missing meth, *a, &b
488       raise "no instance defined!" unless defined? @instance
489
490       ## if we've been deinstantiated, just drop all calls. this is
491       ## useful because threads that might be active during the
492       ## cleanup process (e.g. polling) would otherwise have to
493       ## special-case every call to a Singleton object
494       return nil if @instance.nil?
495
496       @instance.send meth, *a, &b
497     end
498     def i_am_the_instance o
499       raise "there can be only one! (instance)" if defined? @instance
500       @instance = o
501     end
502   end
503
504   def self.included klass
505     klass.extend ClassMethods
506   end
507 end
508
509 ## wraps an object. if it throws an exception, keeps a copy.
510 class Recoverable
511   def initialize o
512     @o = o
513     @error = nil
514     @mutex = Mutex.new
515   end
516
517   attr_accessor :error
518
519   def clear_error!; @error = nil; end
520   def has_errors?; !@error.nil?; end
521
522   def method_missing m, *a, &b; __pass m, *a, &b end
523   
524   def id; __pass :id; end
525   def to_s; __pass :to_s; end
526   def to_yaml x; __pass :to_yaml, x; end
527   def is_a? c; @o.is_a? c; end
528
529   def respond_to? m; @o.respond_to? m end
530
531   def __pass m, *a, &b
532     begin
533       @o.send(m, *a, &b)
534     rescue Exception => e
535       @error ||= e
536       raise
537     end
538   end
539 end
540
541 ## acts like a hash with an initialization block, but saves any
542 ## newly-created value even upon lookup.
543 ##
544 ## for example:
545 ##
546 ## class C
547 ##   attr_accessor :val
548 ##   def initialize; @val = 0 end
549 ## end
550 ## 
551 ## h = Hash.new { C.new }
552 ## h[:a].val # => 0
553 ## h[:a].val = 1
554 ## h[:a].val # => 0
555 ##
556 ## h2 = SavingHash.new { C.new }
557 ## h2[:a].val # => 0
558 ## h2[:a].val = 1
559 ## h2[:a].val # => 1
560 ##
561 ## important note: you REALLY want to use #member? to test existence,
562 ## because just checking h[anything] will always evaluate to true
563 ## (except for degenerate constructor blocks that return nil or false)
564 class SavingHash
565   def initialize &b
566     @constructor = b
567     @hash = Hash.new
568   end
569
570   def [] k
571     @hash[k] ||= @constructor.call(k)
572   end
573
574   defer_all_other_method_calls_to :hash
575 end