Removed acts_as_taggable because it is deprecated and replaced with has_many_polymorphs:

<http://blog.evanweaver.com/articles/2006/06/02/has_many_polymorphs>

Also removed chronic because it is not currently used.

  * Tags are entered separated by commas, so tags with spaces are allowed
  * When you edit an action, whatever is submitted in the tags text field replaces existing tags: if you submit an empty field, tags are removed from the action
  * Clicking on a tag shows a page listing all the actions with that tag (/todo/tag/tag+name)

Todo:
  
  * Tests
  * RESTful routes for Tags (if it makes sense for tags - I haven't decided)
  * If you remove tags for an action, it removes the entries from the Taggings table, but it can leave an orphan Tag if there are no more Taggings for that Tag. One problem is that another user might have an identically-named Tag, so we don't want to remove their Tag, just because we have finished with it. I'm not sure how to arrange this yet.
  
Don't forget to rake db:migrate. There is also a change in config/environment.rb.tmpl, so remember to copy the changes to your copy.
 


git-svn-id: http://www.rousette.org.uk/svn/tracks-repos/trunk@425 a4c988fc-2ded-0310-b66e-134b36920a42
This commit is contained in:
bsag 2007-02-04 15:33:24 +00:00
parent 6fce959bf8
commit d0666038f2
89 changed files with 1612 additions and 3317 deletions

View file

@ -7,6 +7,7 @@ require "redcloth"
require 'date'
require 'time'
Tag # We need this in development mode, or you get 'method missing' errors
class ApplicationController < ActionController::Base

View file

@ -33,7 +33,6 @@ class TodosController < ApplicationController
def create
@item = @user.todos.build
p = params['request'] || params
# @item.tag_with(params[:tag_list])
@item.attributes = p['todo']
if p['todo']['project_id'].blank? && !p['project_name'].blank? && p['project_name'] != 'None'
@ -70,9 +69,11 @@ class TodosController < ApplicationController
@item.show_from = parse_date_per_user_prefs(p['todo']['show_from'])
end
@item.tag_with(params[:tag_list], @user)
@item.save
@item.tag_with(params[:tag_list],@user)
@saved = @item.save
respond_to do |wants|
wants.html { redirect_to :action => "index" }
wants.js do
@ -126,7 +127,7 @@ class TodosController < ApplicationController
def update
@item = check_user_return_item
@item.tag_with(params[:tag_list], @user)
@item.tag_with(params[:tag_list],@user)
@original_item_context_id = @item.context_id
@original_item_project_id = @item.project_id
@original_item_was_deferred = @item.deferred?
@ -245,10 +246,11 @@ class TodosController < ApplicationController
#
def tag
@tag = tag_name = params[:name]
if Tag.find_by_name(tag_name)
@todos = Todo.find_tagged_with(tag_name, @user)
else
tag_collection = Tag.find_by_name(tag_name).todos
if tag_collection.empty?
@todos = []
else
@todos = tag_collection.find(:all, :conditions => ['taggings.user_id = ?', @user.id])
end
@count = @todos.size unless @todos.empty?

11
tracks/app/models/tag.rb Normal file
View file

@ -0,0 +1,11 @@
class Tag < ActiveRecord::Base
has_many_polymorphs :taggables,
:from => [:todos],
:through => :taggings,
:dependent => :destroy
def on(taggable, user)
tagging = taggings.create :taggable => taggable, :user => user
end
end

View file

@ -0,0 +1,11 @@
class Tagging < ActiveRecord::Base
belongs_to :tag
belongs_to :taggable, :polymorphic => true
belongs_to :user
# def before_destroy
# # disallow orphaned tags
# # TODO: this doesn't seem to be working
# tag.destroy if tag.taggings.count < 2
# end
end

View file

@ -1,11 +1,10 @@
class Todo < ActiveRecord::Base
require 'validations'
require 'validations'
belongs_to :context, :order => 'name'
belongs_to :project
belongs_to :user
acts_as_taggable
acts_as_state_machine :initial => :active, :column => 'state'
state :active, :enter => Proc.new { |t| t[:show_from] = nil }
@ -85,5 +84,5 @@ class Todo < ActiveRecord::Base
original_set_initial_state
end
end
end

View file

@ -43,8 +43,8 @@ Event.observe($('todo_project_name'), "focus", projectAutoCompleter.activate.bin
Event.observe($('todo_project_name'), "click", projectAutoCompleter.activate.bind(projectAutoCompleter));
</script>
<label for="tag_list">Tags</label>
<%= text_field_tag "tag_list", nil, :size => 40, :tabindex => 5 %>
<label for="tag_list">Tags (separate with commas)</label>
<%= text_field_tag "tag_list", nil, :size => 40, :tabindex => 5 %>
<label for="todo_due">Due</label>
<%= text_field("todo", "due", "size" => 10, "class" => "Date", "onfocus" => "Calendar.setup", "tabindex" => 6, "autocomplete" => "off") %>

View file

@ -35,8 +35,8 @@
</script>
</tr>
<tr>
<td class="label"><label for="<%= dom_id(@item, 'tag_list') %>">Tags</label></td>
<td><%= text_field_tag "tag_list", @item.tags.collect{|t| t.name}.join(" "), :size => 40 %></td>
<td class="label"><label for="<%= dom_id(@item, 'tag_list') %>">Tags (separate with commas)</label></td>
<td><%= text_field_tag "tag_list", @item.tags.collect{|t| t.name}.join(", "), :size => 40 %></td>
</tr>
<tr>
<td class="label"><label for="<%= dom_id(@item, 'due') %>">Due</td>

View file

@ -1,3 +1,4 @@
<% Tag %>
<div id="<%= dom_id(item) %>" class="item-container">
<div id="<%= dom_id(item, 'line') %>">
<%= link_to_remote_todo item %>
@ -19,7 +20,7 @@
<%= sanitize(item.description) %>
<%= if item.tags.blank?
<%= if item.tag_list.blank?
""
else
tag_string = ""

View file

@ -66,6 +66,7 @@ require 'name_part_finder'
require 'todo_list'
require 'url_friendly_name'
require 'config'
require 'activerecord_base_tag_extensions' # Needed for tagging-specific extensions
if (AUTHENTICATION_SCHEMES.include? 'ldap')
require 'net/ldap' #requires ruby-net-ldap gem be installed

View file

@ -5,15 +5,14 @@
ActiveRecord::Schema.define(:version => 27) do
create_table "contexts", :force => true do |t|
t.column "name", :string, :default => "", :null => false
t.column "hide", :integer, :limit => 4, :default => 0, :null => false
t.column "position", :integer, :default => 0, :null => false
t.column "user_id", :integer, :default => 0, :null => false
t.column "name", :string, :default => "", :null => false
t.column "position", :integer, :default => 0, :null => false
t.column "hide", :boolean, :default => false
t.column "user_id", :integer, :default => 1
t.column "created_at", :datetime
t.column "updated_at", :datetime
end
add_index "contexts", ["user_id"], :name => "index_contexts_on_user_id"
add_index "contexts", ["user_id", "name"], :name => "index_contexts_on_user_id_and_name"
create_table "notes", :force => true do |t|
@ -64,14 +63,13 @@ ActiveRecord::Schema.define(:version => 27) do
create_table "projects", :force => true do |t|
t.column "name", :string, :default => "", :null => false
t.column "position", :integer, :default => 0, :null => false
t.column "user_id", :integer, :default => 0, :null => false
t.column "user_id", :integer, :default => 1
t.column "description", :text
t.column "state", :string, :limit => 20, :default => "active", :null => false
t.column "created_at", :datetime
t.column "updated_at", :datetime
end
add_index "projects", ["user_id"], :name => "index_projects_on_user_id"
add_index "projects", ["user_id", "name"], :name => "index_projects_on_user_id_and_name"
create_table "sessions", :force => true do |t|
@ -100,16 +98,16 @@ ActiveRecord::Schema.define(:version => 27) do
add_index "tags", ["name"], :name => "index_tags_on_name"
create_table "todos", :force => true do |t|
t.column "context_id", :integer, :default => 0, :null => false
t.column "description", :string, :limit => 100, :default => "", :null => false
t.column "context_id", :integer, :default => 0, :null => false
t.column "project_id", :integer
t.column "description", :string, :default => "", :null => false
t.column "notes", :text
t.column "created_at", :datetime
t.column "due", :date
t.column "completed_at", :datetime
t.column "project_id", :integer
t.column "user_id", :integer, :default => 0, :null => false
t.column "user_id", :integer, :default => 1
t.column "show_from", :date
t.column "state", :string, :limit => 20, :default => "immediate", :null => false
t.column "state", :string, :limit => 20, :default => "immediate", :null => false
end
add_index "todos", ["user_id", "state"], :name => "index_todos_on_user_id_and_state"
@ -119,10 +117,10 @@ ActiveRecord::Schema.define(:version => 27) do
add_index "todos", ["user_id", "context_id"], :name => "index_todos_on_user_id_and_context_id"
create_table "users", :force => true do |t|
t.column "login", :string, :limit => 80
t.column "password", :string, :limit => 40
t.column "login", :string, :limit => 80, :default => "", :null => false
t.column "password", :string, :limit => 40, :default => "", :null => false
t.column "word", :string
t.column "is_admin", :integer, :limit => 4, :default => 0, :null => false
t.column "is_admin", :boolean, :default => false, :null => false
t.column "first_name", :string
t.column "last_name", :string
t.column "auth_type", :string, :default => "database", :null => false

View file

@ -0,0 +1,26 @@
class ActiveRecord::Base
# These methods will work for any model instances
# Tag with deletes all the current tags before adding the new ones
# This makes the edit form more intiuitive:
# Whatever is in the tags text field is what gets set as the tags for that action
# If you submit an empty tags text field, all the tags are removed.
def tag_with(tags, user)
Tag.transaction do
Tagging.delete_all(" taggable_id = #{self.id} and taggable_type = '#{self.class}' and user_id = #{user.id}")
tags.downcase.split(", ").each do |tag|
Tag.find_or_create_by_name(tag).on(self, user)
end
end
end
def tag_list
tags.map(&:name).join(', ')
end
def delete_tags tag_string
split = tag_string.downcase.split(" ")
tags.delete tags.select{|t| split.include? t.name}
end
end

View file

@ -0,0 +1,10 @@
require File.dirname(__FILE__) + '/../test_helper'
class TagTest < Test::Unit::TestCase
fixtures :tags
# Replace this with your real tests.
def test_truth
assert true
end
end

View file

@ -0,0 +1,10 @@
require File.dirname(__FILE__) + '/../test_helper'
class TaggingTest < Test::Unit::TestCase
fixtures :taggings
# Replace this with your real tests.
def test_truth
assert true
end
end

View file

@ -1,5 +0,0 @@
require 'acts_as_taggable'
ActiveRecord::Base.send(:include, ActiveRecord::Acts::Taggable)
require File.dirname(__FILE__) + '/lib/tagging'
require File.dirname(__FILE__) + '/lib/tag'

View file

@ -1,4 +0,0 @@
Acts As Taggable
=================
Allows for tags to be added to multiple classes.

View file

@ -1,59 +0,0 @@
module ActiveRecord
module Acts #:nodoc:
module Taggable #:nodoc:
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def acts_as_taggable(options = {})
write_inheritable_attribute(:acts_as_taggable_options, {
:taggable_type => ActiveRecord::Base.send(:class_name_of_active_record_descendant, self).to_s,
:from => options[:from]
})
class_inheritable_reader :acts_as_taggable_options
has_many :taggings, :as => :taggable, :dependent => true
has_many :tags, :through => :taggings
include ActiveRecord::Acts::Taggable::InstanceMethods
extend ActiveRecord::Acts::Taggable::SingletonMethods
end
end
module SingletonMethods
def find_tagged_with(list, user)
find_by_sql([
"SELECT #{table_name}.* FROM #{table_name}, tags, taggings " +
"WHERE #{table_name}.#{primary_key} = taggings.taggable_id " +
"AND taggings.user_id = ? " +
"AND taggings.taggable_type = ? " +
"AND taggings.tag_id = tags.id AND tags.name IN (?)",
user.id, acts_as_taggable_options[:taggable_type], list
])
end
end
module InstanceMethods
def tag_with(list, user)
Tag.transaction do
taggings.destroy_all
Tag.parse(list).each do |name|
if acts_as_taggable_options[:from]
send(acts_as_taggable_options[:from]).tags.find_or_create_by_name(name).on(self, user)
else
Tag.find_or_create_by_name(name).on(self, user)
end
end
end
end
def tag_list
tags.collect { |tag| tag.name.include?(" ") ? '"' + tag.name + '"' : tag.name }.join(" ")
end
end
end
end
end

View file

@ -1,40 +0,0 @@
class Tag < ActiveRecord::Base
has_many :taggings
def self.parse(list)
tag_names = []
# first, pull out the quoted tags
list.gsub!(/\"(.*?)\"\s*/ ) { tag_names << $1; "" }
# then, replace all commas with a space
list.gsub!(/,/, " ")
# then, get whatever's left
tag_names.concat list.split(/\s/)
# strip whitespace from the names
tag_names = tag_names.map { |t| t.strip }
# delete any blank tag names
tag_names = tag_names.delete_if { |t| t.empty? }
return tag_names
end
def tagged
@tagged ||= taggings.collect { |tagging| tagging.taggable }
end
def on(taggable, user)
tagging = taggings.create :taggable => taggable, :user => user
end
def ==(comparison_object)
super || name == comparison_object.to_s
end
def to_s
name
end
end

View file

@ -1,13 +0,0 @@
class Tagging < ActiveRecord::Base
belongs_to :tag
belongs_to :taggable, :polymorphic => true
belongs_to :user
def self.tagged_class(taggable)
ActiveRecord::Base.send(:class_name_of_active_record_descendant, taggable.class).to_s
end
def self.find_taggable(tagged_class, tagged_id)
tagged_class.constantize.find(tagged_id)
end
end

View file

@ -1 +0,0 @@
# Testing goes here

View file

@ -1,119 +0,0 @@
=Chronic
Chronic is a natural language date/time parser written in pure Ruby. See below for the wide variety of formats Chronic will parse.
==Installation
Chronic can be installed via RubyGems:
$ sudo gem install chronic
==Usage
You can parse strings containing a natural language date using the Chronic.parse method.
require 'chronic'
Time.now #=> Sun Aug 27 23:18:25 PDT 2006
#---
Chronic.parse('tomorrow')
#=> Mon Aug 28 12:00:00 PDT 2006
Chronic.parse('monday', :context => :past)
#=> Mon Aug 21 12:00:00 PDT 2006
Chronic.parse('this tuesday 5:00')
#=> Tue Aug 29 17:00:00 PDT 2006
Chronic.parse('this tuesday 5:00', :ambiguous_time_range => :none)
#=> Tue Aug 29 05:00:00 PDT 2006
Chronic.parse('may 27th', :now => Time.local(2000, 1, 1))
#=> Sat May 27 12:00:00 PDT 2000
Chronic.parse('may 27th', :guess => false)
#=> Sun May 27 00:00:00 PDT 2007..Mon May 28 00:00:00 PDT 2007
See Chronic.parse for detailed usage instructions.
==Examples
Chronic can parse a huge variety of date and time formats. Following is a small sample of strings that will be properly parsed. Parsing is case insensitive and will handle common abbreviations and misspellings.
Simple
thursday
november
summer
friday 13:00
mon 2:35
4pm
6 in the morning
friday 1pm
sat 7 in the evening
yesterday
today
tomorrow
this tuesday
next month
last winter
this morning
last night
this second
yesterday at 4:00
last friday at 20:00
last week tuesday
tomorrow at 6:45pm
afternoon yesterday
thursday last week
Complex
3 years ago
5 months before now
7 hours ago
7 days from now
1 week hence
in 3 hours
1 year ago tomorrow
3 months ago saturday at 5:00 pm
7 hours before tomorrow at noon
3rd wednesday in november
3rd month next year
3rd thursday this september
4th day last week
Specific Dates
January 5
dec 25
may 27th
October 2006
oct 06
jan 3 2010
february 14, 2004
3 jan 2000
17 april 85
5/27/1979
27/5/1979
05/06
1979-05-27
Friday
5
4:00
17:00
0800
Specific Times (many of the above with an added time)
January 5 at 7pm
1979-05-27 05:00
etc
==Limitations
Chronic uses Ruby's built in Time class for all time storage and computation. Because of this, only times that the Time class can handle will be properly parsed. Parsing for times outside of this range will simply return nil. Support for a wider range of times is planned for a future release.
Time zones other than the local one are not currently supported. Support for other time zones is planned for a future release.

View file

@ -1,5 +0,0 @@
require 'chronic'
ActiveRecord::Base.class_eval do
include Chronic
end

View file

@ -1,41 +0,0 @@
#=============================================================================
#
# Name: Chronic
# Author: Tom Preston-Werner
# Purpose: Parse natural language dates and times into Time or
# Chronic::Span objects
#
#=============================================================================
require 'chronic/chronic'
require 'chronic/handlers'
require 'chronic/repeater'
require 'chronic/repeaters/repeater_year'
require 'chronic/repeaters/repeater_season'
require 'chronic/repeaters/repeater_season_name'
require 'chronic/repeaters/repeater_month'
require 'chronic/repeaters/repeater_month_name'
require 'chronic/repeaters/repeater_fortnight'
require 'chronic/repeaters/repeater_week'
require 'chronic/repeaters/repeater_weekend'
require 'chronic/repeaters/repeater_day'
require 'chronic/repeaters/repeater_day_name'
require 'chronic/repeaters/repeater_day_portion'
require 'chronic/repeaters/repeater_hour'
require 'chronic/repeaters/repeater_minute'
require 'chronic/repeaters/repeater_second'
require 'chronic/repeaters/repeater_time'
require 'chronic/grabber'
require 'chronic/pointer'
require 'chronic/scalar'
require 'chronic/ordinal'
require 'chronic/separator'
module Chronic
def self.debug=(val); @debug = val; end
end
Chronic.debug = false

View file

@ -1,242 +0,0 @@
module Chronic
class << self
# Parses a string containing a natural language date or time. If the parser
# can find a date or time, either a Time or Chronic::Span will be returned
# (depending on the value of <tt>:guess</tt>). If no date or time can be found,
# +nil+ will be returned.
#
# Options are:
#
# [<tt>:context</tt>]
# <tt>:past</tt> or <tt>:future</tt> (defaults to <tt>:future</tt>)
#
# If your string represents a birthday, you can set <tt>:context</tt> to <tt>:past</tt>
# and if an ambiguous string is given, it will assume it is in the
# past. Specify <tt>:future</tt> or omit to set a future context.
#
# [<tt>:now</tt>]
# Time (defaults to Time.now)
#
# By setting <tt>:now</tt> to a Time, all computations will be based off
# of that time instead of Time.now
#
# [<tt>:guess</tt>]
# +true+ or +false+ (defaults to +true+)
#
# By default, the parser will guess a single point in time for the
# given date or time. If you'd rather have the entire time span returned,
# set <tt>:guess</tt> to +false+ and a Chronic::Span will be returned.
#
# [<tt>:ambiguous_time_range</tt>]
# Integer or <tt>:none</tt> (defaults to <tt>6</tt> (6am-6pm))
#
# If an Integer is given, ambiguous times (like 5:00) will be
# assumed to be within the range of that time in the AM to that time
# in the PM. For example, if you set it to <tt>7</tt>, then the parser will
# look for the time between 7am and 7pm. In the case of 5:00, it would
# assume that means 5:00pm. If <tt>:none</tt> is given, no assumption
# will be made, and the first matching instance of that time will
# be used.
def parse(text, specified_options = {})
# get options and set defaults if necessary
default_options = {:context => :future,
:now => Time.now,
:guess => true,
:ambiguous_time_range => 6}
options = default_options.merge specified_options
# ensure the specified options are valid
specified_options.keys.each do |key|
default_options.keys.include?(key) || raise(InvalidArgumentException, "#{key} is not a valid option key.")
end
[:past, :future].include?(options[:context]) || raise(InvalidArgumentException, "Invalid value '#{options[:context]}' for :context specified. Valid values are :past and :future.")
# store now for later =)
@now = options[:now]
# put the text into a normal format to ease scanning
text = self.pre_normalize(text)
# get base tokens for each word
@tokens = self.base_tokenize(text)
# scan the tokens with each token scanner
[Repeater].each do |tokenizer|
@tokens = tokenizer.scan(@tokens, options)
end
[Grabber, Pointer, Scalar, Ordinal, Separator].each do |tokenizer|
@tokens = tokenizer.scan(@tokens)
end
# strip any non-tagged tokens
@tokens = @tokens.select { |token| token.tagged? }
if @debug
puts "+---------------------------------------------------"
puts "| " + @tokens.to_s
puts "+---------------------------------------------------"
end
# do the heavy lifting
begin
span = self.tokens_to_span(@tokens, options)
rescue
raise
return nil
end
# guess a time within a span if required
if options[:guess]
return self.guess(span)
else
return span
end
end
# Clean up the specified input text by stripping unwanted characters,
# converting idioms to their canonical form, converting number words
# to numbers (three => 3), and converting ordinal words to numeric
# ordinals (third => 3rd)
def pre_normalize(text) #:nodoc:
normalized_text = text.to_s.downcase
normalized_text.gsub!(/['"\.]/, '')
normalized_text.gsub!(/([\/\-\,\@])/) { ' ' + $1 + ' ' }
normalized_text.gsub!(/\btoday\b/, 'this day')
normalized_text.gsub!(/\btomm?orr?ow\b/, 'next day')
normalized_text.gsub!(/\byesterday\b/, 'last day')
normalized_text.gsub!(/\bnoon\b/, '12:00')
normalized_text.gsub!(/\bmidnight\b/, '24:00')
normalized_text.gsub!(/\bfrom now\b/, 'future')
normalized_text.gsub!(/\bbefore now\b/, 'past')
normalized_text.gsub!(/\bnow\b/, 'this second')
normalized_text.gsub!(/\b(ago|before)\b/, 'past')
normalized_text.gsub!(/\bthis past\b/, 'last')
normalized_text.gsub!(/\bthis last\b/, 'last')
normalized_text.gsub!(/\b(?:in|during) the (morning)\b/, '\1')
normalized_text.gsub!(/\b(?:in the|during the|at) (afternoon|evening|night)\b/, '\1')
normalized_text.gsub!(/\btonight\b/, 'this night')
normalized_text.gsub!(/(?=\w)([ap]m|oclock)\b/, ' \1')
normalized_text.gsub!(/\b(hence|after|from)\b/, 'future')
normalized_text.gsub!(/\ba\b/, '1')
normalized_text.gsub!(/\s+/, ' ')
normalized_text = numericize_numbers(normalized_text)
normalized_text = numericize_ordinals(normalized_text)
end
# Convert number words to numbers (three => 3)
def numericize_numbers(text) #:nodoc:
text
end
# Convert ordinal words to numeric ordinals (third => 3rd)
def numericize_ordinals(text) #:nodoc:
text
end
# Split the text on spaces and convert each word into
# a Token
def base_tokenize(text) #:nodoc:
text.split(' ').map { |word| Token.new(word) }
end
# Guess a specific time within the given span
def guess(span) #:nodoc:
return nil if span.nil?
if span.width > 1
span.begin + (span.width / 2)
else
span.begin
end
end
end
class Token #:nodoc:
attr_accessor :word, :tags
def initialize(word)
@word = word
@tags = []
end
# Tag this token with the specified tag
def tag(new_tag)
@tags << new_tag
end
# Remove all tags of the given class
def untag(tag_class)
@tags = @tags.select { |m| !m.kind_of? tag_class }
end
# Return true if this token has any tags
def tagged?
@tags.size > 0
end
# Return the Tag that matches the given class
def get_tag(tag_class)
matches = @tags.select { |m| m.kind_of? tag_class }
#matches.size < 2 || raise("Multiple identical tags found")
return matches.first
end
# Print this Token in a pretty way
def to_s
@word << '(' << @tags.join(', ') << ') '
end
end
# A Span represents a range of time. Since this class extends
# Range, you can use #begin and #end to get the beginning and
# ending times of the span (they will be of class Time)
class Span < Range
# Returns the width of this span in seconds
def width
(self.end - self.begin).to_i
end
# Add a number of seconds to this span, returning the
# resulting Span
def +(seconds)
Span.new(self.begin + seconds, self.end + seconds)
end
# Subtract a number of seconds to this span, returning the
# resulting Span
def -(seconds)
self + -seconds
end
# Prints this span in a nice fashion
def to_s
'(' << self.begin.to_s << '..' << self.end.to_s << ')'
end
end
# Tokens are tagged with subclassed instances of this class when
# they match specific criteria
class Tag #:nodoc:
attr_accessor :type
def initialize(type)
@type = type
end
def start=(s)
@now = s
end
end
# Internal exception
class ChronicPain < Exception #:nodoc:
end
# This exception is raised if an invalid argument is provided to
# any of Chronic's methods
class InvalidArgumentException < Exception
end
end

View file

@ -1,26 +0,0 @@
#module Chronic
class Chronic::Grabber < Chronic::Tag #:nodoc:
def self.scan(tokens)
tokens.each_index do |i|
if t = self.scan_for_all(tokens[i]) then tokens[i].tag(t); next end
end
tokens
end
def self.scan_for_all(token)
scanner = {/last/ => :last,
/this/ => :this,
/next/ => :next}
scanner.keys.each do |scanner_item|
return self.new(scanner[scanner_item]) if scanner_item =~ token.word
end
return nil
end
def to_s
'grabber-' << @type.to_s
end
end
#end

View file

@ -1,403 +0,0 @@
module Chronic
class << self
def definitions #:nodoc:
@definitions ||=
{:time => [Handler.new([:repeater_time, :repeater_day_portion?], nil)],
:date => [Handler.new([:repeater_month_name, :scalar_day, :scalar_year], :handle_rmn_sd_sy),
Handler.new([:repeater_month_name, :scalar_day, :scalar_year, :separator_at?, 'time?'], :handle_rmn_sd_sy),
Handler.new([:repeater_month_name, :scalar_day, :separator_at?, 'time?'], :handle_rmn_sd),
Handler.new([:repeater_month_name, :ordinal_day, :separator_at?, 'time?'], :handle_rmn_od),
Handler.new([:repeater_month_name, :scalar_year], :handle_rmn_sy),
Handler.new([:scalar_day, :repeater_month_name, :scalar_year, :separator_at?, 'time?'], :handle_sd_rmn_sy),
Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_day, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sm_sd_sy),
Handler.new([:scalar_day, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sd_sm_sy),
Handler.new([:scalar_year, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_day, :separator_at?, 'time?'], :handle_sy_sm_sd),
Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_year], :handle_sm_sy)],
:anchor => [Handler.new([:grabber?, :repeater, :separator_at?, :repeater?, :repeater?], :handle_r),
Handler.new([:repeater, :grabber, :repeater], :handle_r_g_r)],
:arrow => [Handler.new([:scalar, :repeater, :pointer], :handle_s_r_p),
Handler.new([:pointer, :scalar, :repeater], :handle_p_s_r),
Handler.new([:scalar, :repeater, :pointer, 'anchor'], :handle_s_r_p_a)],
:narrow => [Handler.new([:ordinal, :repeater, :separator_in, :repeater], :handle_o_r_s_r),
Handler.new([:ordinal, :repeater, :grabber, :repeater], :handle_o_r_g_r)]
}
end
def tokens_to_span(tokens, options) #:nodoc:
# maybe it's a specific date
self.definitions[:date].each do |handler|
if handler.match(tokens, self.definitions)
good_tokens = tokens.select { |o| !o.get_tag Separator }
return self.send(handler.handler_method, good_tokens, options)
end
end
# I guess it's not a specific date, maybe it's just an anchor
self.definitions[:anchor].each do |handler|
if handler.match(tokens, self.definitions)
good_tokens = tokens.select { |o| !o.get_tag Separator }
return self.send(handler.handler_method, good_tokens, options)
end
end
# not an anchor, perhaps it's an arrow
self.definitions[:arrow].each do |handler|
if handler.match(tokens, self.definitions)
good_tokens = tokens.reject { |o| o.get_tag(SeparatorAt) || o.get_tag(SeparatorSlashOrDash) || o.get_tag(SeparatorComma) }
return self.send(handler.handler_method, good_tokens, options)
end
end
# not an arrow, let's hope it's an narrow
self.definitions[:narrow].each do |handler|
if handler.match(tokens, self.definitions)
#good_tokens = tokens.select { |o| !o.get_tag Separator }
return self.send(handler.handler_method, tokens, options)
end
end
# I guess you're out of luck!
return nil
end
#--------------
def day_or_time(day_start, time_tokens, options)
outer_span = Span.new(day_start, day_start + (24 * 60 * 60))
if !time_tokens.empty?
@now = outer_span.begin
time = get_anchor(dealias_and_disambiguate_times(time_tokens, options), options)
return time
else
return outer_span
end
end
#--------------
def handle_m_d(month, day, time_tokens, options) #:nodoc:
month.start = @now
span = month.next(options[:context])
day_start = Time.local(span.begin.year, span.begin.month, day)
day_or_time(day_start, time_tokens, options)
end
def handle_rmn_sd(tokens, options) #:nodoc:
handle_m_d(tokens[0].get_tag(RepeaterMonthName), tokens[1].get_tag(ScalarDay).type, tokens[2..tokens.size], options)
end
def handle_rmn_od(tokens, options) #:nodoc:
handle_m_d(tokens[0].get_tag(RepeaterMonthName), tokens[1].get_tag(OrdinalDay).type, tokens[2..tokens.size], options)
end
def handle_rmn_sy(tokens, options) #:nodoc:
month = tokens[0].get_tag(RepeaterMonthName).index
year = tokens[1].get_tag(ScalarYear).type
if month == 12
next_month_year = year + 1
next_month_month = 1
else
next_month_year = year
next_month_month = month + 1
end
begin
Span.new(Time.local(year, month), Time.local(next_month_year, next_month_month))
rescue ArgumentError
nil
end
end
def handle_rmn_sd_sy(tokens, options) #:nodoc:
month = tokens[0].get_tag(RepeaterMonthName).index
day = tokens[1].get_tag(ScalarDay).type
year = tokens[2].get_tag(ScalarYear).type
time_tokens = tokens.last(tokens.size - 3)
begin
day_start = Time.local(year, month, day)
day_or_time(day_start, time_tokens, options)
rescue ArgumentError
nil
end
end
def handle_sd_rmn_sy(tokens, options) #:nodoc:
new_tokens = [tokens[1], tokens[0], tokens[2]]
time_tokens = tokens.last(tokens.size - 3)
self.handle_rmn_sd_sy(new_tokens + time_tokens, options)
end
def handle_sm_sd_sy(tokens, options) #:nodoc:
month = tokens[0].get_tag(ScalarMonth).type
day = tokens[1].get_tag(ScalarDay).type
year = tokens[2].get_tag(ScalarYear).type
time_tokens = tokens.last(tokens.size - 3)
begin
day_start = Time.local(year, month, day) #:nodoc:
day_or_time(day_start, time_tokens, options)
rescue ArgumentError
nil
end
end
def handle_sd_sm_sy(tokens, options) #:nodoc:
new_tokens = [tokens[1], tokens[0], tokens[2]]
time_tokens = tokens.last(tokens.size - 3)
self.handle_sm_sd_sy(new_tokens + time_tokens, options)
end
def handle_sy_sm_sd(tokens, options) #:nodoc:
new_tokens = [tokens[1], tokens[2], tokens[0]]
time_tokens = tokens.last(tokens.size - 3)
self.handle_sm_sd_sy(new_tokens + time_tokens, options)
end
def handle_sm_sy(tokens, options) #:nodoc:
month = tokens[0].get_tag(ScalarMonth).type
year = tokens[1].get_tag(ScalarYear).type
if month == 12
next_month_year = year + 1
next_month_month = 1
else
next_month_year = year
next_month_month = month + 1
end
begin
Span.new(Time.local(year, month), Time.local(next_month_year, next_month_month))
rescue ArgumentError
nil
end
end
# anchors
def handle_r(tokens, options) #:nodoc:
dd_tokens = dealias_and_disambiguate_times(tokens, options)
self.get_anchor(dd_tokens, options)
end
def handle_r_g_r(tokens, options) #:nodoc:
new_tokens = [tokens[1], tokens[0], tokens[2]]
self.handle_r(new_tokens, options)
end
# arrows
def handle_srp(tokens, span, options) #:nodoc:
distance = tokens[0].get_tag(Scalar).type
repeater = tokens[1].get_tag(Repeater)
pointer = tokens[2].get_tag(Pointer).type
repeater.offset(span, distance, pointer)
end
def handle_s_r_p(tokens, options) #:nodoc:
repeater = tokens[1].get_tag(Repeater)
span =
case true
when [RepeaterYear, RepeaterSeason, RepeaterSeasonName, RepeaterMonth, RepeaterMonthName, RepeaterFortnight, RepeaterWeek].include?(repeater.class)
self.parse("this hour", :guess => false, :now => @now)
when [RepeaterWeekend, RepeaterDay, RepeaterDayName, RepeaterDayPortion, RepeaterHour].include?(repeater.class)
self.parse("this minute", :guess => false, :now => @now)
when [RepeaterMinute, RepeaterSecond].include?(repeater.class)
self.parse("this second", :guess => false, :now => @now)
else
raise(ChronicPain, "Invalid repeater: #{repeater.class}")
end
self.handle_srp(tokens, span, options)
end
def handle_p_s_r(tokens, options) #:nodoc:
new_tokens = [tokens[1], tokens[2], tokens[0]]
self.handle_s_r_p(new_tokens, options)
end
def handle_s_r_p_a(tokens, options) #:nodoc:
anchor_span = get_anchor(tokens[3..tokens.size - 1], options)
self.handle_srp(tokens, anchor_span, options)
end
# narrows
def handle_orr(tokens, outer_span, options) #:nodoc:
repeater = tokens[1].get_tag(Repeater)
repeater.start = outer_span.begin - 1
ordinal = tokens[0].get_tag(Ordinal).type
span = nil
ordinal.times do
span = repeater.next(:future)
if span.begin > outer_span.end
span = nil
break
end
end
span
end
def handle_o_r_s_r(tokens, options) #:nodoc:
outer_span = get_anchor([tokens[3]], options)
handle_orr(tokens[0..1], outer_span, options)
end
def handle_o_r_g_r(tokens, options) #:nodoc:
outer_span = get_anchor(tokens[2..3], options)
handle_orr(tokens[0..1], outer_span, options)
end
# support methods
def get_anchor(tokens, options) #:nodoc:
grabber = Grabber.new(:this)
pointer = :future
repeaters = self.get_repeaters(tokens)
repeaters.size.times { tokens.pop }
if tokens.first && tokens.first.get_tag(Grabber)
grabber = tokens.first.get_tag(Grabber)
tokens.pop
end
head = repeaters.shift
head.start = @now
case grabber.type
when :last: outer_span = head.next(:past)
when :this: outer_span = head.this(options[:context])
when :next: outer_span = head.next(:future)
else raise(ChronicPain, "Invalid grabber")
end
anchor = find_within(repeaters, outer_span, pointer)
end
def get_repeaters(tokens) #:nodoc:
repeaters = []
tokens.each do |token|
if t = token.get_tag(Repeater)
repeaters << t
end
end
repeaters.sort.reverse
end
# Recursively finds repeaters within other repeaters.
# Returns a Span representing the innermost time span
# or nil if no repeater union could be found
def find_within(tags, span, pointer) #:nodoc:
return span if tags.empty?
head, *rest = tags
head.start = pointer == :future ? span.begin : span.end
h = head.this(pointer)
if span.include?(h.begin) || span.include?(h.end)
return find_within(rest, h, pointer)
else
return nil
end
end
def dealias_and_disambiguate_times(tokens, options) #:nodoc:
# handle aliases of am/pm
# 5:00 in the morning => 5:00 am
# 7:00 in the evening => 7:00 pm
#ttokens = []
tokens.each_with_index do |t0, i|
t1 = tokens[i + 1]
if t1 && (t1tag = t1.get_tag(RepeaterDayPortion)) && t0.get_tag(RepeaterTime)
if [:morning].include?(t1tag.type)
t1.untag(RepeaterDayPortion)
t1.tag(RepeaterDayPortion.new(:am))
elsif [:afternoon, :evening, :night].include?(t1tag.type)
t1.untag(RepeaterDayPortion)
t1.tag(RepeaterDayPortion.new(:pm))
end
end
end
#tokens = ttokens
# handle ambiguous times if :ambiguous_time_range is specified
if options[:ambiguous_time_range] != :none
ttokens = []
tokens.each_with_index do |t0, i|
ttokens << t0
t1 = tokens[i + 1]
if t0.get_tag(RepeaterTime) && t0.get_tag(RepeaterTime).type.ambiguous? && (!t1 || !t1.get_tag(RepeaterDayPortion))
distoken = Token.new('disambiguator')
distoken.tag(RepeaterDayPortion.new(options[:ambiguous_time_range]))
ttokens << distoken
end
end
tokens = ttokens
end
tokens
end
end
class Handler #:nodoc:
attr_accessor :pattern, :handler_method
def initialize(pattern, handler_method)
@pattern = pattern
@handler_method = handler_method
end
def constantize(name)
camel = name.to_s.gsub(/(^|_)(.)/) { $2.upcase }
::Chronic.module_eval(camel, __FILE__, __LINE__)
end
def match(tokens, definitions)
token_index = 0
@pattern.each do |element|
name = element.to_s
optional = name.reverse[0..0] == '?'
name = name.chop if optional
if element.instance_of? Symbol
klass = constantize(name)
match = tokens[token_index] && !tokens[token_index].tags.select { |o| o.kind_of?(klass) }.empty?
return false if !match && !optional
(token_index += 1; next) if match
next if !match && optional
elsif element.instance_of? String
return true if optional && token_index == tokens.size
sub_handlers = definitions[name.intern] || raise(ChronicPain, "Invalid subset #{name} specified")
sub_handlers.each do |sub_handler|
return true if sub_handler.match(tokens[token_index..tokens.size], definitions)
end
return false
else
raise(ChronicPain, "Invalid match type: #{element.class}")
end
end
return false if token_index != tokens.size
return true
end
end
end

View file

@ -1,40 +0,0 @@
module Chronic
class Ordinal < Tag #:nodoc:
def self.scan(tokens)
# for each token
tokens.each_index do |i|
if t = self.scan_for_ordinals(tokens[i]) then tokens[i].tag(t) end
if t = self.scan_for_days(tokens[i]) then tokens[i].tag(t) end
end
tokens
end
def self.scan_for_ordinals(token)
if token.word =~ /^(\d*)(st|nd|rd|th)$/
return Ordinal.new($1.to_i)
end
return nil
end
def self.scan_for_days(token)
if token.word =~ /^(\d*)(st|nd|rd|th)$/
unless $1.to_i > 31
return OrdinalDay.new(token.word.to_i)
end
end
return nil
end
def to_s
'ordinal'
end
end
class OrdinalDay < Ordinal #:nodoc:
def to_s
super << '-day-' << @type.to_s
end
end
end

View file

@ -1,27 +0,0 @@
module Chronic
class Pointer < Tag #:nodoc:
def self.scan(tokens)
# for each token
tokens.each_index do |i|
if t = self.scan_for_all(tokens[i]) then tokens[i].tag(t) end
end
tokens
end
def self.scan_for_all(token)
scanner = {/past/ => :past,
/future/ => :future,
/in/ => :future}
scanner.keys.each do |scanner_item|
return self.new(scanner[scanner_item]) if scanner_item =~ token.word
end
return nil
end
def to_s
'pointer-' << @type.to_s
end
end
end

View file

@ -1,114 +0,0 @@
class Chronic::Repeater < Chronic::Tag #:nodoc:
def self.scan(tokens, options)
# for each token
tokens.each_index do |i|
if t = self.scan_for_month_names(tokens[i]) then tokens[i].tag(t); next end
if t = self.scan_for_day_names(tokens[i]) then tokens[i].tag(t); next end
if t = self.scan_for_day_portions(tokens[i]) then tokens[i].tag(t); next end
if t = self.scan_for_times(tokens[i], options) then tokens[i].tag(t); next end
if t = self.scan_for_units(tokens[i]) then tokens[i].tag(t); next end
end
tokens
end
def self.scan_for_month_names(token)
scanner = {/^jan\.?(uary)?$/ => :january,
/^feb\.?(ruary)?$/ => :february,
/^mar\.?(ch)?$/ => :march,
/^apr\.?(il)?$/ => :april,
/^may$/ => :may,
/^jun\.?e?$/ => :june,
/^jul\.?y?$/ => :july,
/^aug\.?(ust)?$/ => :august,
/^sep\.?(tember)?$/ => :september,
/^oct\.?(ober)?$/ => :october,
/^nov\.?(ember)?$/ => :november,
/^dec\.?(ember)?$/ => :december}
scanner.keys.each do |scanner_item|
return Chronic::RepeaterMonthName.new(scanner[scanner_item]) if scanner_item =~ token.word
end
return nil
end
def self.scan_for_day_names(token)
scanner = {/^m[ou]n(day)?$/ => :monday,
/^t(ue|eu|oo|u|)s(day)?$/ => :tuesday,
/^we(dnes|nds|nns)day$/ => :wednesday,
/^wed$/ => :wednesday,
/^th(urs|ers)day$/ => :thursday,
/^thu$/ => :thursday,
/^fr[iy](day)?$/ => :friday,
/^sat(t?[ue]rday)?$/ => :saturday,
/^su[nm](day)?$/ => :sunday}
scanner.keys.each do |scanner_item|
return Chronic::RepeaterDayName.new(scanner[scanner_item]) if scanner_item =~ token.word
end
return nil
end
def self.scan_for_day_portions(token)
scanner = {/^ams?$/ => :am,
/^pms?$/ => :pm,
/^mornings?$/ => :morning,
/^afternoons?$/ => :afternoon,
/^evenings?$/ => :evening,
/^nights?$/ => :night}
scanner.keys.each do |scanner_item|
return Chronic::RepeaterDayPortion.new(scanner[scanner_item]) if scanner_item =~ token.word
end
return nil
end
def self.scan_for_times(token, options)
if token.word =~ /^\d{1,2}(:?\d{2})?$/
return Chronic::RepeaterTime.new(token.word, options)
end
return nil
end
def self.scan_for_units(token)
scanner = {/^years?$/ => :year,
/^seasons?$/ => :season,
/^months?$/ => :month,
/^fortnights?$/ => :fortnight,
/^weeks?$/ => :week,
/^weekends?$/ => :weekends,
/^days?$/ => :day,
/^hours?$/ => :hour,
/^minutes?$/ => :minute,
/^seconds?$/ => :second}
scanner.keys.each do |scanner_item|
if scanner_item =~ token.word
klass_name = 'Chronic::Repeater' + scanner[scanner_item].to_s.capitalize
klass = eval(klass_name)
return klass.new(scanner[scanner_item])
end
end
return nil
end
def <=>(other)
width <=> other.width
end
# returns the width (in seconds or months) of this repeatable.
def width
raise("Repeatable#width must be overridden in subclasses")
end
# returns the next occurance of this repeatable.
def next(pointer)
!@now.nil? || raise("Start point must be set before calling #next")
[:future, :past].include?(pointer) || raise("First argument 'pointer' must be one of :past or :future")
#raise("Repeatable#next must be overridden in subclasses")
end
def this(pointer)
!@now.nil? || raise("Start point must be set before calling #next")
[:future, :past].include?(pointer) || raise("First argument 'pointer' must be one of :past or :future")
end
def to_s
'repeater'
end
end

View file

@ -1,44 +0,0 @@
class Chronic::RepeaterDay < Chronic::Repeater #:nodoc:
DAY_SECONDS = 86_400 # (24 * 60 * 60)
def next(pointer)
super
if !@current_day_start
@current_day_start = Time.local(@now.year, @now.month, @now.day)
end
direction = pointer == :future ? 1 : -1
@current_day_start += direction * DAY_SECONDS
Chronic::Span.new(@current_day_start, @current_day_start + DAY_SECONDS)
end
def this(pointer = :future)
super
case pointer
when :future
day_begin = Time.local(@now.year, @now.month, @now.day, @now.hour + 1)
day_end = Time.local(@now.year, @now.month, @now.day) + DAY_SECONDS
when :past
day_begin = Time.local(@now.year, @now.month, @now.day)
day_end = Time.local(@now.year, @now.month, @now.day, @now.hour)
end
Chronic::Span.new(day_begin, day_end)
end
def offset(span, amount, pointer)
direction = pointer == :future ? 1 : -1
span + direction * amount * DAY_SECONDS
end
def width
DAY_SECONDS
end
def to_s
super << '-day'
end
end

View file

@ -1,45 +0,0 @@
class Chronic::RepeaterDayName < Chronic::Repeater #:nodoc:
DAY_SECONDS = 86400 # (24 * 60 * 60)
def next(pointer)
super
direction = pointer == :future ? 1 : -1
if !@current_day_start
@current_day_start = Time.local(@now.year, @now.month, @now.day)
@current_day_start += direction * DAY_SECONDS
day_num = symbol_to_number(@type)
while @current_day_start.wday != day_num
@current_day_start += direction * DAY_SECONDS
end
else
@current_day_start += direction * 7 * DAY_SECONDS
end
Chronic::Span.new(@current_day_start, @current_day_start + DAY_SECONDS)
end
def this(pointer = :future)
super
self.next(pointer)
end
def width
DAY_SECONDS
end
def to_s
super << '-dayofweek-' << @type.to_s
end
private
def symbol_to_number(sym)
lookup = {:sunday => 0, :monday => 1, :tuesday => 2, :wednesday => 3, :thursday => 4, :friday => 5, :saturday => 6}
lookup[sym] || raise("Invalid symbol specified")
end
end

View file

@ -1,93 +0,0 @@
class Chronic::RepeaterDayPortion < Chronic::Repeater #:nodoc:
@@morning = (6 * 60 * 60)..(12 * 60 * 60) # 6am-12am
@@afternoon = (13 * 60 * 60)..(17 * 60 * 60) # 1pm-5pm
@@evening = (17 * 60 * 60)..(20 * 60 * 60) # 5pm-8pm
@@night = (20 * 60 * 60)..(24 * 60 * 60) # 8pm-12pm
def initialize(type)
super
if type.kind_of? Integer
@range = (@type * 60 * 60)..((@type + 12) * 60 * 60)
else
lookup = {:am => 1..(12 * 60 * 60),
:pm => (12 * 60 * 60)..(24 * 60 * 60),
:morning => @@morning,
:afternoon => @@afternoon,
:evening => @@evening,
:night => @@night}
@range = lookup[type]
lookup[type] || raise("Invalid type '#{type}' for RepeaterDayPortion")
end
@range || raise("Range should have been set by now")
end
def next(pointer)
super
full_day = 60 * 60 * 24
if !@current_span
now_seconds = @now - Time.local(@now.year, @now.month, @now.day)
if now_seconds < @range.begin
case pointer
when :future
range_start = Time.local(@now.year, @now.month, @now.day) + @range.begin
when :past
range_start = Time.local(@now.year, @now.month, @now.day) - full_day + @range.begin
end
elsif now_seconds > @range.end
case pointer
when :future
range_start = Time.local(@now.year, @now.month, @now.day) + full_day + @range.begin
when :past
range_start = Time.local(@now.year, @now.month, @now.day) + @range.begin
end
else
case pointer
when :future
range_start = Time.local(@now.year, @now.month, @now.day) + full_day + @range.begin
when :past
range_start = Time.local(@now.year, @now.month, @now.day) - full_day + @range.begin
end
end
@current_span = Chronic::Span.new(range_start, range_start + (@range.end - @range.begin))
else
case pointer
when :future
@current_span += full_day
when :past
@current_span -= full_day
end
end
end
def this(context = :future)
super
range_start = Time.local(@now.year, @now.month, @now.day) + @range.begin
@current_span = Chronic::Span.new(range_start, range_start + (@range.end - @range.begin))
end
def offset(span, amount, pointer)
@now = span.begin
portion_span = self.next(pointer)
direction = pointer == :future ? 1 : -1
portion_span + (direction * (amount - 1) * Chronic::RepeaterDay::DAY_SECONDS)
end
def width
@range || raise("Range has not been set")
return @current_span.width if @current_span
if @type.kind_of? Integer
return (12 * 60 * 60)
else
@range.end - @range.begin
end
end
def to_s
super << '-dayportion-' << @type.to_s
end
end

View file

@ -1,64 +0,0 @@
class Chronic::RepeaterFortnight < Chronic::Repeater #:nodoc:
FORTNIGHT_SECONDS = 1_209_600 # (14 * 24 * 60 * 60)
def next(pointer)
super
if !@current_fortnight_start
case pointer
when :future
sunday_repeater = Chronic::RepeaterDayName.new(:sunday)
sunday_repeater.start = @now
next_sunday_span = sunday_repeater.next(:future)
@current_fortnight_start = next_sunday_span.begin
when :past
sunday_repeater = Chronic::RepeaterDayName.new(:sunday)
sunday_repeater.start = (@now + Chronic::RepeaterDay::DAY_SECONDS)
2.times { sunday_repeater.next(:past) }
last_sunday_span = sunday_repeater.next(:past)
@current_fortnight_start = last_sunday_span.begin
end
else
direction = pointer == :future ? 1 : -1
@current_fortnight_start += direction * FORTNIGHT_SECONDS
end
Chronic::Span.new(@current_fortnight_start, @current_fortnight_start + FORTNIGHT_SECONDS)
end
def this(pointer = :future)
super
case pointer
when :future
this_fortnight_start = Time.local(@now.year, @now.month, @now.day, @now.hour) + Chronic::RepeaterHour::HOUR_SECONDS
sunday_repeater = Chronic::RepeaterDayName.new(:sunday)
sunday_repeater.start = @now
sunday_repeater.this(:future)
this_sunday_span = sunday_repeater.this(:future)
this_fortnight_end = this_sunday_span.begin
Chronic::Span.new(this_fortnight_start, this_fortnight_end)
when :past
this_fortnight_end = Time.local(@now.year, @now.month, @now.day, @now.hour)
sunday_repeater = Chronic::RepeaterDayName.new(:sunday)
sunday_repeater.start = @now
#sunday_repeater.next(:past)
last_sunday_span = sunday_repeater.next(:past)
this_fortnight_start = last_sunday_span.begin
Chronic::Span.new(this_fortnight_start, this_fortnight_end)
end
end
def offset(span, amount, pointer)
direction = pointer == :future ? 1 : -1
span + direction * amount * FORTNIGHT_SECONDS
end
def width
FORTNIGHT_SECONDS
end
def to_s
super << '-fortnight'
end
end

View file

@ -1,52 +0,0 @@
class Chronic::RepeaterHour < Chronic::Repeater #:nodoc:
HOUR_SECONDS = 3600 # 60 * 60
def next(pointer)
super
if !@current_hour_start
case pointer
when :future
@current_hour_start = Time.local(@now.year, @now.month, @now.day, @now.hour + 1)
when :past
@current_hour_start = Time.local(@now.year, @now.month, @now.day, @now.hour - 1)
end
else
direction = pointer == :future ? 1 : -1
@current_hour_start += direction * HOUR_SECONDS
end
Chronic::Span.new(@current_hour_start, @current_hour_start + HOUR_SECONDS)
end
def this(pointer = :future)
super
case pointer
when :future
hour_start = Time.local(@now.year, @now.month, @now.day, @now.hour, @now.min + 1)
hour_end = Time.local(@now.year, @now.month, @now.day, @now.hour + 1)
when :past
hour_start = Time.local(@now.year, @now.month, @now.day, @now.hour)
hour_end = Time.local(@now.year, @now.month, @now.day, @now.hour, @now.min)
when :none
hour_start = Time.local(@now.year, @now.month, @now.day, @now.hour)
hour_end = hour_begin + HOUR_SECONDS
end
Chronic::Span.new(hour_start, hour_end)
end
def offset(span, amount, pointer)
direction = pointer == :future ? 1 : -1
span + direction * amount * HOUR_SECONDS
end
def width
HOUR_SECONDS
end
def to_s
super << '-hour'
end
end

View file

@ -1,21 +0,0 @@
class Chronic::RepeaterMinute < Chronic::Repeater #:nodoc:
MINUTE_SECONDS = 60
def this(pointer = :future)
minute_begin = Time.local(@now.year, @now.month, @now.day, @now.hour, @now.min)
Chronic::Span.new(minute_begin, minute_begin + MINUTE_SECONDS)
end
def offset(span, amount, pointer)
direction = pointer == :future ? 1 : -1
span + direction * amount * MINUTE_SECONDS
end
def width
MINUTE_SECONDS
end
def to_s
super << '-minute'
end
end

View file

@ -1,54 +0,0 @@
class Chronic::RepeaterMonth < Chronic::Repeater #:nodoc:
MONTH_SECONDS = 2_592_000 # 30 * 24 * 60 * 60
YEAR_MONTHS = 12
def next(pointer)
super
if !@current_month_start
@current_month_start = offset_by(Time.local(@now.year, @now.month), 1, pointer)
else
@current_month_start = offset_by(Time.local(@current_month_start.year, @current_month_start.month), 1, pointer)
end
Chronic::Span.new(@current_month_start, Time.local(@current_month_start.year, @current_month_start.month + 1))
end
def this(pointer = :future)
super
case pointer
when :future
month_start = Time.local(@now.year, @now.month, @now.day + 1)
month_end = self.offset_by(Time.local(@now.year, @now.month), 1, :future)
when :past
month_start = Time.local(@now.year, @now.month)
month_end = Time.local(@now.year, @now.month, @now.day)
end
Chronic::Span.new(month_start, month_end)
end
def offset(span, amount, pointer)
Chronic::Span.new(offset_by(span.begin, amount, pointer), offset_by(span.end, amount, pointer))
end
def offset_by(time, amount, pointer)
direction = pointer == :future ? 1 : -1
amount_years = direction * amount / YEAR_MONTHS
amount_months = direction * amount % YEAR_MONTHS
new_year = time.year + amount_years
new_month = time.month + amount_months
if new_month > YEAR_MONTHS
new_year += 1
new_month -= YEAR_MONTHS
end
Time.local(new_year, new_month, time.day, time.hour, time.min, time.sec)
end
def width
MONTH_SECONDS
end
end

View file

@ -1,82 +0,0 @@
class Chronic::RepeaterMonthName < Chronic::Repeater #:nodoc:
MONTH_SECONDS = 2_592_000 # 30 * 24 * 60 * 60
def next(pointer)
super
if !@current_month_begin
target_month = symbol_to_number(@type)
case pointer
when :future
if @now.month < target_month
@current_month_begin = Time.local(@now.year, target_month)
else @now.month > target_month
@current_month_begin = Time.local(@now.year + 1, target_month)
end
when :past
if @now.month > target_month
@current_month_begin = Time.local(@now.year, target_month)
else @now.month < target_month
@current_month_begin = Time.local(@now.year - 1, target_month)
end
end
@current_month_begin || raise("Current month should be set by now")
else
case pointer
when :future
@current_month_begin = Time.local(@current_month_begin.year + 1, @current_month_begin.month)
when :past
@current_month_begin = Time.local(@current_month_begin.year - 1, @current_month_begin.month)
end
end
cur_month_year = @current_month_begin.year
cur_month_month = @current_month_begin.month
if cur_month_month == 12
next_month_year = cur_month_year + 1
next_month_month = 1
else
next_month_year = cur_month_year
next_month_month = cur_month_month + 1
end
Chronic::Span.new(@current_month_begin, Time.local(next_month_year, next_month_month))
end
def this(pointer = :future)
super
self.next(pointer)
end
def width
MONTH_SECONDS
end
def index
symbol_to_number(@type)
end
def to_s
super << '-month-' << @type.to_s
end
private
def symbol_to_number(sym)
lookup = {:january => 1,
:february => 2,
:march => 3,
:april => 4,
:may => 5,
:june => 6,
:july => 7,
:august => 8,
:september => 9,
:october => 10,
:november => 11,
:december => 12}
lookup[sym] || raise("Invalid symbol specified")
end
end

View file

@ -1,23 +0,0 @@
class Chronic::RepeaterSeason < Chronic::Repeater #:nodoc:
SEASON_SECONDS = 7_862_400 # 91 * 24 * 60 * 60
def next(pointer)
super
raise 'Not implemented'
end
def this(pointer = :future)
super
raise 'Not implemented'
end
def width
SEASON_SECONDS
end
def to_s
super << '-season'
end
end

View file

@ -1,24 +0,0 @@
class Chronic::RepeaterSeasonName < Chronic::RepeaterSeason #:nodoc:
@summer = ['jul 21', 'sep 22']
@autumn = ['sep 23', 'dec 21']
@winter = ['dec 22', 'mar 19']
@spring = ['mar 20', 'jul 20']
def next(pointer)
super
raise 'Not implemented'
end
def this(pointer = :future)
super
raise 'Not implemented'
end
def width
(91 * 24 * 60 * 60)
end
def to_s
super << '-season-' << @type.to_s
end
end

View file

@ -1,34 +0,0 @@
class Chronic::RepeaterSecond < Chronic::Repeater #:nodoc:
SECOND_SECONDS = 1 # haha, awesome
def next(pointer = :future)
super
direction = pointer == :future ? 1 : -1
if !@second_start
@second_start = @now + (direction * SECOND_SECONDS)
else
@second_start += SECOND_SECONDS * direction
end
Chronic::Span.new(@second_start, @second_start + SECOND_SECONDS)
end
def this(pointer = :future)
Chronic::Span.new(@now, @now + 1)
end
def offset(span, amount, pointer)
direction = pointer == :future ? 1 : -1
span + direction * amount * SECOND_SECONDS
end
def width
SECOND_SECONDS
end
def to_s
super << '-second'
end
end

View file

@ -1,106 +0,0 @@
class Chronic::RepeaterTime < Chronic::Repeater #:nodoc:
class Tick #:nodoc:
attr_accessor :time
def initialize(time, ambiguous = false)
@time = time
@ambiguous = ambiguous
end
def ambiguous?
@ambiguous
end
def *(other)
Tick.new(@time * other, @ambiguous)
end
def to_f
@time.to_f
end
def to_s
@time.to_s + (@ambiguous ? '?' : '')
end
end
def initialize(time, options = {})
t = time.sub(/\:/, '')
@type =
if (1..2) === t.size
Tick.new(t.to_i * 60 * 60, true)
elsif t.size == 3
Tick.new((t[0..0].to_i * 60 * 60) + (t[1..2].to_i * 60), true)
elsif t.size == 4
ambiguous = time =~ /:/ && t[0..0].to_i != 0 && t[0..1].to_i <= 12
Tick.new(t[0..1].to_i * 60 * 60 + t[2..3].to_i * 60, ambiguous)
else
raise("Time cannot exceed four digits")
end
end
# Return the next past or future Span for the time that this Repeater represents
# pointer - Symbol representing which temporal direction to fetch the next day
# must be either :past or :future
def next(pointer)
super
half_day = 60 * 60 * 12
full_day = 60 * 60 * 24
first = false
unless @current_time
first = true
midnight = Time.local(@now.year, @now.month, @now.day)
yesterday_midnight = midnight - full_day
tomorrow_midnight = midnight + full_day
catch :done do
if pointer == :future
if @type.ambiguous?
[midnight + @type, midnight + half_day + @type, tomorrow_midnight + @type].each do |t|
(@current_time = t; throw :done) if t > @now
end
else
[midnight + @type, tomorrow_midnight + @type].each do |t|
(@current_time = t; throw :done) if t > @now
end
end
else # pointer == :past
if @type.ambiguous?
[midnight + half_day + @type, midnight + @type, yesterday_midnight + @type * 2].each do |t|
(@current_time = t; throw :done) if t < @now
end
else
[midnight + @type, yesterday_midnight + @type].each do |t|
(@current_time = t; throw :done) if t < @now
end
end
end
end
@current_time || raise("Current time cannot be nil at this point")
end
unless first
increment = @type.ambiguous? ? half_day : full_day
@current_time += pointer == :future ? increment : -increment
end
Chronic::Span.new(@current_time, @current_time + width)
end
def this(context = :future)
[:future, :past].include?(context) || raise("First argument 'context' must be one of :past or :future")
self.next(context)
end
def width
1
end
def to_s
super << '-time-' << @type.to_s
end
end

View file

@ -1,62 +0,0 @@
class Chronic::RepeaterWeek < Chronic::Repeater #:nodoc:
WEEK_SECONDS = 604800 # (7 * 24 * 60 * 60)
def next(pointer)
super
if !@current_week_start
case pointer
when :future
sunday_repeater = Chronic::RepeaterDayName.new(:sunday)
sunday_repeater.start = @now
next_sunday_span = sunday_repeater.next(:future)
@current_week_start = next_sunday_span.begin
when :past
sunday_repeater = Chronic::RepeaterDayName.new(:sunday)
sunday_repeater.start = (@now + Chronic::RepeaterDay::DAY_SECONDS)
sunday_repeater.next(:past)
last_sunday_span = sunday_repeater.next(:past)
@current_week_start = last_sunday_span.begin
end
else
direction = pointer == :future ? 1 : -1
@current_week_start += direction * WEEK_SECONDS
end
Chronic::Span.new(@current_week_start, @current_week_start + WEEK_SECONDS)
end
def this(pointer = :future)
super
case pointer
when :future
this_week_start = Time.local(@now.year, @now.month, @now.day, @now.hour) + Chronic::RepeaterHour::HOUR_SECONDS
sunday_repeater = Chronic::RepeaterDayName.new(:sunday)
sunday_repeater.start = @now
this_sunday_span = sunday_repeater.this(:future)
this_week_end = this_sunday_span.begin
Chronic::Span.new(this_week_start, this_week_end)
when :past
this_week_end = Time.local(@now.year, @now.month, @now.day, @now.hour)
sunday_repeater = Chronic::RepeaterDayName.new(:sunday)
sunday_repeater.start = @now
last_sunday_span = sunday_repeater.next(:past)
this_week_start = last_sunday_span.begin
Chronic::Span.new(this_week_start, this_week_end)
end
end
def offset(span, amount, pointer)
direction = pointer == :future ? 1 : -1
span + direction * amount * WEEK_SECONDS
end
def width
WEEK_SECONDS
end
def to_s
super << '-week'
end
end

View file

@ -1,11 +0,0 @@
class Chronic::RepeaterWeekend < Chronic::Repeater #:nodoc:
WEEKEND_SECONDS = 172_800 # (2 * 24 * 60 * 60)
def width
WEEKEND_SECONDS
end
def to_s
super << '-weekend'
end
end

View file

@ -1,55 +0,0 @@
class Chronic::RepeaterYear < Chronic::Repeater #:nodoc:
def next(pointer)
super
if !@current_year_start
case pointer
when :future
@current_year_start = Time.local(@now.year + 1)
when :past
@current_year_start = Time.local(@now.year - 1)
end
else
diff = pointer == :future ? 1 : -1
@current_year_start = Time.local(@current_year_start.year + diff)
end
Chronic::Span.new(@current_year_start, Time.local(@current_year_start.year + 1))
end
def this(pointer = :future)
super
case pointer
when :future
this_year_start = Time.local(@now.year, @now.month, @now.day) + Chronic::RepeaterDay::DAY_SECONDS
this_year_end = Time.local(@now.year + 1, 1, 1)
when :past
this_year_start = Time.local(@now.year, 1, 1)
this_year_end = Time.local(@now.year, @now.month, @now.day)
end
Chronic::Span.new(this_year_start, this_year_end)
end
def offset(span, amount, pointer)
direction = pointer == :future ? 1 : -1
sb = span.begin
new_begin = Time.local(sb.year + (amount * direction), sb.month, sb.day, sb.hour, sb.min, sb.sec)
se = span.end
new_end = Time.local(se.year + (amount * direction), se.month, se.day, se.hour, se.min, se.sec)
Chronic::Span.new(new_begin, new_end)
end
def width
(365 * 24 * 60 * 60)
end
def to_s
super << '-year'
end
end

View file

@ -1,74 +0,0 @@
module Chronic
class Scalar < Tag #:nodoc:
def self.scan(tokens)
# for each token
tokens.each_index do |i|
if t = self.scan_for_scalars(tokens[i], tokens[i + 1]) then tokens[i].tag(t) end
if t = self.scan_for_days(tokens[i], tokens[i + 1]) then tokens[i].tag(t) end
if t = self.scan_for_months(tokens[i], tokens[i + 1]) then tokens[i].tag(t) end
if t = self.scan_for_years(tokens[i], tokens[i + 1]) then tokens[i].tag(t) end
end
tokens
end
def self.scan_for_scalars(token, post_token)
if token.word =~ /^\d*$/
unless post_token && %w{am pm morning afternoon evening night}.include?(post_token)
return Scalar.new(token.word.to_i)
end
end
return nil
end
def self.scan_for_days(token, post_token)
if token.word =~ /^\d\d?$/
unless token.word.to_i > 31 || (post_token && %w{am pm morning afternoon evening night}.include?(post_token))
return ScalarDay.new(token.word.to_i)
end
end
return nil
end
def self.scan_for_months(token, post_token)
if token.word =~ /^\d\d?$/
unless token.word.to_i > 12 || (post_token && %w{am pm morning afternoon evening night}.include?(post_token))
return ScalarMonth.new(token.word.to_i)
end
end
return nil
end
def self.scan_for_years(token, post_token)
if token.word =~ /^\d\d(\d\d)?$/
unless post_token && %w{am pm morning afternoon evening night}.include?(post_token)
return ScalarYear.new(token.word.to_i)
end
end
return nil
end
def to_s
'scalar'
end
end
class ScalarDay < Scalar #:nodoc:
def to_s
super << '-day-' << @type.to_s
end
end
class ScalarMonth < Scalar #:nodoc:
def to_s
super << '-month-' << @type.to_s
end
end
class ScalarYear < Scalar #:nodoc:
def to_s
super << '-year-' << @type.to_s
end
end
end

View file

@ -1,76 +0,0 @@
module Chronic
class Separator < Tag #:nodoc:
def self.scan(tokens)
tokens.each_index do |i|
if t = self.scan_for_commas(tokens[i]) then tokens[i].tag(t); next end
if t = self.scan_for_slash_or_dash(tokens[i]) then tokens[i].tag(t); next end
if t = self.scan_for_at(tokens[i]) then tokens[i].tag(t); next end
if t = self.scan_for_in(tokens[i]) then tokens[i].tag(t); next end
end
tokens
end
def self.scan_for_commas(token)
scanner = {/^,$/ => :comma}
scanner.keys.each do |scanner_item|
return SeparatorComma.new(scanner[scanner_item]) if scanner_item =~ token.word
end
return nil
end
def self.scan_for_slash_or_dash(token)
scanner = {/^-$/ => :dash,
/^\/$/ => :slash}
scanner.keys.each do |scanner_item|
return SeparatorSlashOrDash.new(scanner[scanner_item]) if scanner_item =~ token.word
end
return nil
end
def self.scan_for_at(token)
scanner = {/^(at|@)$/ => :at}
scanner.keys.each do |scanner_item|
return SeparatorAt.new(scanner[scanner_item]) if scanner_item =~ token.word
end
return nil
end
def self.scan_for_in(token)
scanner = {/^in$/ => :in}
scanner.keys.each do |scanner_item|
return SeparatorIn.new(scanner[scanner_item]) if scanner_item =~ token.word
end
return nil
end
def to_s
'separator'
end
end
class SeparatorComma < Separator #:nodoc:
def to_s
super << '-comma'
end
end
class SeparatorSlashOrDash < Separator #:nodoc:
def to_s
super << '-slashordash-' << @type.to_s
end
end
class SeparatorAt < Separator #:nodoc:
def to_s
super << '-at'
end
end
class SeparatorIn < Separator #:nodoc:
def to_s
super << '-in'
end
end
end

View file

@ -1,50 +0,0 @@
__END__
require 'test/unit'
class ParseNumbersTest < Test::Unit::TestCase
def test_parse
strings = {1 => 'one',
5 => 'five',
10 => 'ten',
11 => 'eleven',
12 => 'twelve',
13 => 'thirteen',
14 => 'fourteen',
15 => 'fifteen',
16 => 'sixteen',
17 => 'seventeen',
18 => 'eighteen',
19 => 'nineteen',
20 => 'twenty',
27 => 'twenty seven',
31 => 'thirty-one',
59 => 'fifty nine',
100 => 'a hundred',
100 => 'one hundred',
150 => 'one hundred and fifty',
150 => 'one fifty',
200 => 'two-hundred',
500 => '5 hundred',
999 => 'nine hundred and ninety nine',
1_000 => 'one thousand',
1_200 => 'twelve hundred',
1_200 => 'one thousand two hundred',
17_000 => 'seventeen thousand',
21_473 => 'twentyone-thousand-four-hundred-and-seventy-three',
74_002 => 'seventy four thousand and two',
99_999 => 'ninety nine thousand nine hundred ninety nine',
100_000 => '100 thousand',
250_000 => 'two hundred fifty thousand',
1_000_000 => 'one million',
1_250_007 => 'one million two hundred fifty thousand and seven',
1_000_000_000 => 'one billion',
1_000_000_001 => 'one billion and one'}
strings.keys.sort.each do |key|
assert_equal key, JW::NumberMagick.convert(strings[key])
end
end
end

View file

@ -1,9 +0,0 @@
require 'test/unit'
tests = Dir["#{File.dirname(__FILE__)}/test_*.rb"]
tests.delete_if { |o| o =~ /test_parsing/ }
tests.each do |file|
require file
end
require File.dirname(__FILE__) + '/test_parsing.rb'

View file

@ -1,50 +0,0 @@
require 'chronic'
require 'test/unit'
class TestChronic < Test::Unit::TestCase
def setup
# Wed Aug 16 14:00:00 UTC 2006
@now = Time.local(2006, 8, 16, 14, 0, 0, 0)
end
def test_post_normalize_am_pm_aliases
# affect wanted patterns
tokens = [Chronic::Token.new("5:00"), Chronic::Token.new("morning")]
tokens[0].tag(Chronic::RepeaterTime.new("5:00"))
tokens[1].tag(Chronic::RepeaterDayPortion.new(:morning))
assert_equal :morning, tokens[1].tags[0].type
tokens = Chronic.dealias_and_disambiguate_times(tokens, {})
assert_equal :am, tokens[1].tags[0].type
assert_equal 2, tokens.size
# don't affect unwanted patterns
tokens = [Chronic::Token.new("friday"), Chronic::Token.new("morning")]
tokens[0].tag(Chronic::RepeaterDayName.new(:friday))
tokens[1].tag(Chronic::RepeaterDayPortion.new(:morning))
assert_equal :morning, tokens[1].tags[0].type
tokens = Chronic.dealias_and_disambiguate_times(tokens, {})
assert_equal :morning, tokens[1].tags[0].type
assert_equal 2, tokens.size
end
def test_guess
span = Chronic::Span.new(Time.local(2006, 8, 16, 0), Time.local(2006, 8, 17, 0))
assert_equal Time.local(2006, 8, 16, 12), Chronic.guess(span)
span = Chronic::Span.new(Time.local(2006, 8, 16, 0), Time.local(2006, 8, 17, 0, 0, 1))
assert_equal Time.local(2006, 8, 16, 12), Chronic.guess(span)
span = Chronic::Span.new(Time.local(2006, 11), Time.local(2006, 12))
assert_equal Time.local(2006, 11, 16), Chronic.guess(span)
end
end

View file

@ -1,110 +0,0 @@
require 'chronic'
require 'test/unit'
class TestHandler < Test::Unit::TestCase
def setup
# Wed Aug 16 14:00:00 UTC 2006
@now = Time.local(2006, 8, 16, 14, 0, 0, 0)
end
def test_handler_class_1
handler = Chronic::Handler.new([:repeater], :handler)
tokens = [Chronic::Token.new('friday')]
tokens[0].tag(Chronic::RepeaterDayName.new(:friday))
assert handler.match(tokens, Chronic.definitions)
tokens << Chronic::Token.new('afternoon')
tokens[1].tag(Chronic::RepeaterDayPortion.new(:afternoon))
assert !handler.match(tokens, Chronic.definitions)
end
def test_handler_class_2
handler = Chronic::Handler.new([:repeater, :repeater?], :handler)
tokens = [Chronic::Token.new('friday')]
tokens[0].tag(Chronic::RepeaterDayName.new(:friday))
assert handler.match(tokens, Chronic.definitions)
tokens << Chronic::Token.new('afternoon')
tokens[1].tag(Chronic::RepeaterDayPortion.new(:afternoon))
assert handler.match(tokens, Chronic.definitions)
tokens << Chronic::Token.new('afternoon')
tokens[2].tag(Chronic::RepeaterDayPortion.new(:afternoon))
assert !handler.match(tokens, Chronic.definitions)
end
def test_handler_class_3
handler = Chronic::Handler.new([:repeater, 'time?'], :handler)
tokens = [Chronic::Token.new('friday')]
tokens[0].tag(Chronic::RepeaterDayName.new(:friday))
assert handler.match(tokens, Chronic.definitions)
tokens << Chronic::Token.new('afternoon')
tokens[1].tag(Chronic::RepeaterDayPortion.new(:afternoon))
assert !handler.match(tokens, Chronic.definitions)
end
def test_handler_class_4
handler = Chronic::Handler.new([:repeater_month_name, :scalar_day, 'time?'], :handler)
tokens = [Chronic::Token.new('may')]
tokens[0].tag(Chronic::RepeaterMonthName.new(:may))
assert !handler.match(tokens, Chronic.definitions)
tokens << Chronic::Token.new('27')
tokens[1].tag(Chronic::ScalarDay.new(27))
assert handler.match(tokens, Chronic.definitions)
end
def test_handler_class_5
handler = Chronic::Handler.new([:repeater, 'time?'], :handler)
tokens = [Chronic::Token.new('friday')]
tokens[0].tag(Chronic::RepeaterDayName.new(:friday))
assert handler.match(tokens, Chronic.definitions)
tokens << Chronic::Token.new('5:00')
tokens[1].tag(Chronic::RepeaterTime.new('5:00'))
assert handler.match(tokens, Chronic.definitions)
tokens << Chronic::Token.new('pm')
tokens[2].tag(Chronic::RepeaterDayPortion.new(:pm))
assert handler.match(tokens, Chronic.definitions)
end
def test_handler_class_6
handler = Chronic::Handler.new([:scalar, :repeater, :pointer], :handler)
tokens = [Chronic::Token.new('3'),
Chronic::Token.new('years'),
Chronic::Token.new('past')]
tokens[0].tag(Chronic::Scalar.new(3))
tokens[1].tag(Chronic::RepeaterYear.new(:year))
tokens[2].tag(Chronic::Pointer.new(:past))
assert handler.match(tokens, Chronic.definitions)
end
def test_constantize
handler = Chronic::Handler.new([], :handler)
assert_equal Chronic::RepeaterTime, handler.constantize(:repeater_time)
end
end

View file

@ -1,52 +0,0 @@
require 'chronic'
require 'test/unit'
class TestRepeaterDayName < Test::Unit::TestCase
def setup
@now = Time.local(2006, 8, 16, 14, 0, 0, 0)
end
def test_match
token = Chronic::Token.new('saturday')
repeater = Chronic::Repeater.scan_for_day_names(token)
assert_equal Chronic::RepeaterDayName, repeater.class
assert_equal :saturday, repeater.type
token = Chronic::Token.new('sunday')
repeater = Chronic::Repeater.scan_for_day_names(token)
assert_equal Chronic::RepeaterDayName, repeater.class
assert_equal :sunday, repeater.type
end
def test_next_future
mondays = Chronic::RepeaterDayName.new(:monday)
mondays.start = @now
span = mondays.next(:future)
assert_equal Time.local(2006, 8, 21), span.begin
assert_equal Time.local(2006, 8, 22), span.end
span = mondays.next(:future)
assert_equal Time.local(2006, 8, 28), span.begin
assert_equal Time.local(2006, 8, 29), span.end
end
def test_next_past
mondays = Chronic::RepeaterDayName.new(:monday)
mondays.start = @now
span = mondays.next(:past)
assert_equal Time.local(2006, 8, 14), span.begin
assert_equal Time.local(2006, 8, 15), span.end
span = mondays.next(:past)
assert_equal Time.local(2006, 8, 7), span.begin
assert_equal Time.local(2006, 8, 8), span.end
end
end

View file

@ -1,63 +0,0 @@
require 'chronic'
require 'test/unit'
class TestRepeaterFortnight < Test::Unit::TestCase
def setup
@now = Time.local(2006, 8, 16, 14, 0, 0, 0)
end
def test_next_future
fortnights = Chronic::RepeaterFortnight.new(:fortnight)
fortnights.start = @now
next_fortnight = fortnights.next(:future)
assert_equal Time.local(2006, 8, 20), next_fortnight.begin
assert_equal Time.local(2006, 9, 3), next_fortnight.end
next_next_fortnight = fortnights.next(:future)
assert_equal Time.local(2006, 9, 3), next_next_fortnight.begin
assert_equal Time.local(2006, 9, 17), next_next_fortnight.end
end
def test_next_past
fortnights = Chronic::RepeaterFortnight.new(:fortnight)
fortnights.start = @now
last_fortnight = fortnights.next(:past)
assert_equal Time.local(2006, 7, 30), last_fortnight.begin
assert_equal Time.local(2006, 8, 13), last_fortnight.end
last_last_fortnight = fortnights.next(:past)
assert_equal Time.local(2006, 7, 16), last_last_fortnight.begin
assert_equal Time.local(2006, 7, 30), last_last_fortnight.end
end
def test_this_future
fortnights = Chronic::RepeaterFortnight.new(:fortnight)
fortnights.start = @now
this_fortnight = fortnights.this(:future)
assert_equal Time.local(2006, 8, 16, 15), this_fortnight.begin
assert_equal Time.local(2006, 8, 27), this_fortnight.end
end
def test_this_past
fortnights = Chronic::RepeaterFortnight.new(:fortnight)
fortnights.start = @now
this_fortnight = fortnights.this(:past)
assert_equal Time.local(2006, 8, 13, 0), this_fortnight.begin
assert_equal Time.local(2006, 8, 16, 14), this_fortnight.end
end
def test_offset
span = Chronic::Span.new(@now, @now + 1)
offset_span = Chronic::RepeaterWeek.new(:week).offset(span, 3, :future)
assert_equal Time.local(2006, 9, 6, 14), offset_span.begin
assert_equal Time.local(2006, 9, 6, 14, 0, 1), offset_span.end
end
end

View file

@ -1,65 +0,0 @@
require 'chronic'
require 'test/unit'
class TestRepeaterHour < Test::Unit::TestCase
def setup
@now = Time.local(2006, 8, 16, 14, 0, 0, 0)
end
def test_next_future
hours = Chronic::RepeaterHour.new(:hour)
hours.start = @now
next_hour = hours.next(:future)
assert_equal Time.local(2006, 8, 16, 15), next_hour.begin
assert_equal Time.local(2006, 8, 16, 16), next_hour.end
next_next_hour = hours.next(:future)
assert_equal Time.local(2006, 8, 16, 16), next_next_hour.begin
assert_equal Time.local(2006, 8, 16, 17), next_next_hour.end
end
def test_next_past
hours = Chronic::RepeaterHour.new(:hour)
hours.start = @now
past_hour = hours.next(:past)
assert_equal Time.local(2006, 8, 16, 13), past_hour.begin
assert_equal Time.local(2006, 8, 16, 14), past_hour.end
past_past_hour = hours.next(:past)
assert_equal Time.local(2006, 8, 16, 12), past_past_hour.begin
assert_equal Time.local(2006, 8, 16, 13), past_past_hour.end
end
def test_this
@now = Time.local(2006, 8, 16, 14, 30)
hours = Chronic::RepeaterHour.new(:hour)
hours.start = @now
this_hour = hours.this(:future)
assert_equal Time.local(2006, 8, 16, 14, 31), this_hour.begin
assert_equal Time.local(2006, 8, 16, 15), this_hour.end
this_hour = hours.this(:past)
assert_equal Time.local(2006, 8, 16, 14), this_hour.begin
assert_equal Time.local(2006, 8, 16, 14, 30), this_hour.end
end
def test_offset
span = Chronic::Span.new(@now, @now + 1)
offset_span = Chronic::RepeaterHour.new(:hour).offset(span, 3, :future)
assert_equal Time.local(2006, 8, 16, 17), offset_span.begin
assert_equal Time.local(2006, 8, 16, 17, 0, 1), offset_span.end
offset_span = Chronic::RepeaterHour.new(:hour).offset(span, 24, :past)
assert_equal Time.local(2006, 8, 15, 14), offset_span.begin
assert_equal Time.local(2006, 8, 15, 14, 0, 1), offset_span.end
end
end

View file

@ -1,47 +0,0 @@
require 'chronic'
require 'test/unit'
class TestRepeaterMonth < Test::Unit::TestCase
def setup
# Wed Aug 16 14:00:00 2006
@now = Time.local(2006, 8, 16, 14, 0, 0, 0)
end
def test_offset_by
# future
time = Chronic::RepeaterMonth.new(:month).offset_by(@now, 1, :future)
assert_equal Time.local(2006, 9, 16, 14), time
time = Chronic::RepeaterMonth.new(:month).offset_by(@now, 5, :future)
assert_equal Time.local(2007, 1, 16, 14), time
# past
time = Chronic::RepeaterMonth.new(:month).offset_by(@now, 1, :past)
assert_equal Time.local(2006, 7, 16, 14), time
time = Chronic::RepeaterMonth.new(:month).offset_by(@now, 10, :past)
assert_equal Time.local(2005, 10, 16, 14), time
end
def test_offset
# future
span = Chronic::Span.new(@now, @now + 60)
offset_span = Chronic::RepeaterMonth.new(:month).offset(span, 1, :future)
assert_equal Time.local(2006, 9, 16, 14), offset_span.begin
assert_equal Time.local(2006, 9, 16, 14, 1), offset_span.end
# past
span = Chronic::Span.new(@now, @now + 60)
offset_span = Chronic::RepeaterMonth.new(:month).offset(span, 1, :past)
assert_equal Time.local(2006, 7, 16, 14), offset_span.begin
assert_equal Time.local(2006, 7, 16, 14, 1), offset_span.end
end
end

View file

@ -1,57 +0,0 @@
require 'chronic'
require 'test/unit'
class TestRepeaterMonthName < Test::Unit::TestCase
def setup
# Wed Aug 16 14:00:00 2006
@now = Time.local(2006, 8, 16, 14, 0, 0, 0)
end
def test_next
# future
mays = Chronic::RepeaterMonthName.new(:may)
mays.start = @now
next_may = mays.next(:future)
assert_equal Time.local(2007, 5), next_may.begin
assert_equal Time.local(2007, 6), next_may.end
next_next_may = mays.next(:future)
assert_equal Time.local(2008, 5), next_next_may.begin
assert_equal Time.local(2008, 6), next_next_may.end
decembers = Chronic::RepeaterMonthName.new(:december)
decembers.start = @now
next_december = decembers.next(:future)
assert_equal Time.local(2006, 12), next_december.begin
assert_equal Time.local(2007, 1), next_december.end
# past
mays = Chronic::RepeaterMonthName.new(:may)
mays.start = @now
assert_equal Time.local(2006, 5), mays.next(:past).begin
assert_equal Time.local(2005, 5), mays.next(:past).begin
end
def test_this
octobers = Chronic::RepeaterMonthName.new(:october)
octobers.start = @now
this_october = octobers.this(:future)
assert_equal Time.local(2006, 10, 1), this_october.begin
assert_equal Time.local(2006, 11, 1), this_october.end
aprils = Chronic::RepeaterMonthName.new(:april)
aprils.start = @now
this_april = aprils.this(:past)
assert_equal Time.local(2006, 4, 1), this_april.begin
assert_equal Time.local(2006, 5, 1), this_april.end
end
end

View file

@ -1,72 +0,0 @@
require 'chronic'
require 'test/unit'
class TestRepeaterTime < Test::Unit::TestCase
def setup
# Wed Aug 16 14:00:00 2006
@now = Time.local(2006, 8, 16, 14, 0, 0, 0)
end
def test_next_future
t = Chronic::RepeaterTime.new('4:00')
t.start = @now
assert_equal Time.local(2006, 8, 16, 16), t.next(:future).begin
assert_equal Time.local(2006, 8, 17, 4), t.next(:future).begin
t = Chronic::RepeaterTime.new('13:00')
t.start = @now
assert_equal Time.local(2006, 8, 17, 13), t.next(:future).begin
assert_equal Time.local(2006, 8, 18, 13), t.next(:future).begin
t = Chronic::RepeaterTime.new('0400')
t.start = @now
assert_equal Time.local(2006, 8, 17, 4), t.next(:future).begin
assert_equal Time.local(2006, 8, 18, 4), t.next(:future).begin
end
def test_next_past
t = Chronic::RepeaterTime.new('4:00')
t.start = @now
assert_equal Time.local(2006, 8, 16, 4), t.next(:past).begin
assert_equal Time.local(2006, 8, 15, 16), t.next(:past).begin
t = Chronic::RepeaterTime.new('13:00')
t.start = @now
assert_equal Time.local(2006, 8, 16, 13), t.next(:past).begin
assert_equal Time.local(2006, 8, 15, 13), t.next(:past).begin
end
def test_type
t1 = Chronic::RepeaterTime.new('4')
assert_equal 14_400, t1.type.time
t1 = Chronic::RepeaterTime.new('14')
assert_equal 50_400, t1.type.time
t1 = Chronic::RepeaterTime.new('4:00')
assert_equal 14_400, t1.type.time
t1 = Chronic::RepeaterTime.new('4:30')
assert_equal 16_200, t1.type.time
t1 = Chronic::RepeaterTime.new('1400')
assert_equal 50_400, t1.type.time
t1 = Chronic::RepeaterTime.new('0400')
assert_equal 14_400, t1.type.time
t1 = Chronic::RepeaterTime.new('04')
assert_equal 14_400, t1.type.time
t1 = Chronic::RepeaterTime.new('400')
assert_equal 14_400, t1.type.time
end
end

View file

@ -1,63 +0,0 @@
require 'chronic'
require 'test/unit'
class TestRepeaterWeek < Test::Unit::TestCase
def setup
@now = Time.local(2006, 8, 16, 14, 0, 0, 0)
end
def test_next_future
weeks = Chronic::RepeaterWeek.new(:week)
weeks.start = @now
next_week = weeks.next(:future)
assert_equal Time.local(2006, 8, 20), next_week.begin
assert_equal Time.local(2006, 8, 27), next_week.end
next_next_week = weeks.next(:future)
assert_equal Time.local(2006, 8, 27), next_next_week.begin
assert_equal Time.local(2006, 9, 3), next_next_week.end
end
def test_next_past
weeks = Chronic::RepeaterWeek.new(:week)
weeks.start = @now
last_week = weeks.next(:past)
assert_equal Time.local(2006, 8, 6), last_week.begin
assert_equal Time.local(2006, 8, 13), last_week.end
last_last_week = weeks.next(:past)
assert_equal Time.local(2006, 7, 30), last_last_week.begin
assert_equal Time.local(2006, 8, 6), last_last_week.end
end
def test_this_future
weeks = Chronic::RepeaterWeek.new(:week)
weeks.start = @now
this_week = weeks.this(:future)
assert_equal Time.local(2006, 8, 16, 15), this_week.begin
assert_equal Time.local(2006, 8, 20), this_week.end
end
def test_this_past
weeks = Chronic::RepeaterWeek.new(:week)
weeks.start = @now
this_week = weeks.this(:past)
assert_equal Time.local(2006, 8, 13, 0), this_week.begin
assert_equal Time.local(2006, 8, 16, 14), this_week.end
end
def test_offset
span = Chronic::Span.new(@now, @now + 1)
offset_span = Chronic::RepeaterWeek.new(:week).offset(span, 3, :future)
assert_equal Time.local(2006, 9, 6, 14), offset_span.begin
assert_equal Time.local(2006, 9, 6, 14, 0, 1), offset_span.end
end
end

View file

@ -1,63 +0,0 @@
require 'chronic'
require 'test/unit'
class TestRepeaterYear < Test::Unit::TestCase
def setup
@now = Time.local(2006, 8, 16, 14, 0, 0, 0)
end
def test_next_future
years = Chronic::RepeaterYear.new(:year)
years.start = @now
next_year = years.next(:future)
assert_equal Time.local(2007, 1, 1), next_year.begin
assert_equal Time.local(2008, 1, 1), next_year.end
next_next_year = years.next(:future)
assert_equal Time.local(2008, 1, 1), next_next_year.begin
assert_equal Time.local(2009, 1, 1), next_next_year.end
end
def test_next_past
years = Chronic::RepeaterYear.new(:year)
years.start = @now
last_year = years.next(:past)
assert_equal Time.local(2005, 1, 1), last_year.begin
assert_equal Time.local(2006, 1, 1), last_year.end
last_last_year = years.next(:past)
assert_equal Time.local(2004, 1, 1), last_last_year.begin
assert_equal Time.local(2005, 1, 1), last_last_year.end
end
def test_this
years = Chronic::RepeaterYear.new(:year)
years.start = @now
this_year = years.this(:future)
assert_equal Time.local(2006, 8, 17), this_year.begin
assert_equal Time.local(2007, 1, 1), this_year.end
this_year = years.this(:past)
assert_equal Time.local(2006, 1, 1), this_year.begin
assert_equal Time.local(2006, 8, 16), this_year.end
end
def test_offset
span = Chronic::Span.new(@now, @now + 1)
offset_span = Chronic::RepeaterYear.new(:year).offset(span, 3, :future)
assert_equal Time.local(2009, 8, 16, 14), offset_span.begin
assert_equal Time.local(2009, 8, 16, 14, 0, 1), offset_span.end
offset_span = Chronic::RepeaterYear.new(:year).offset(span, 10, :past)
assert_equal Time.local(1996, 8, 16, 14), offset_span.begin
assert_equal Time.local(1996, 8, 16, 14, 0, 1), offset_span.end
end
end

View file

@ -1,24 +0,0 @@
require 'chronic'
require 'test/unit'
class TestSpan < Test::Unit::TestCase
def setup
# Wed Aug 16 14:00:00 UTC 2006
@now = Time.local(2006, 8, 16, 14, 0, 0, 0)
end
def test_span_width
span = Chronic::Span.new(Time.local(2006, 8, 16, 0), Time.local(2006, 8, 17, 0))
assert_equal (60 * 60 * 24), span.width
end
def test_span_math
s = Chronic::Span.new(1, 2)
assert_equal 2, (s + 1).begin
assert_equal 3, (s + 1).end
assert_equal 0, (s - 1).begin
assert_equal 1, (s - 1).end
end
end

View file

@ -1,26 +0,0 @@
require 'chronic'
require 'test/unit'
class TestToken < Test::Unit::TestCase
def setup
# Wed Aug 16 14:00:00 UTC 2006
@now = Time.local(2006, 8, 16, 14, 0, 0, 0)
end
def test_token
token = Chronic::Token.new('foo')
assert_equal 0, token.tags.size
assert !token.tagged?
token.tag("mytag")
assert_equal 1, token.tags.size
assert token.tagged?
assert_equal String, token.get_tag(String).class
token.tag(5)
assert_equal 2, token.tags.size
token.untag(String)
assert_equal 1, token.tags.size
assert_equal 'foo', token.word
end
end

View file

@ -1,478 +0,0 @@
require 'chronic'
require 'test/unit'
class TestParsing < Test::Unit::TestCase
def setup
# Wed Aug 16 14:00:00 UTC 2006
@time_2006_08_16_14_00_00 = Time.local(2006, 8, 16, 14, 0, 0, 0)
@time_2006_08_16_03_00_00 = Time.local(2006, 8, 16, 3, 0, 0, 0)
Chronic.debug = false
end
def test__parse_guess_dates
# rm_sd
time = Chronic.parse("may 27", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2007, 5, 27, 12), time
time = Chronic.parse("may 28", :now => @time_2006_08_16_14_00_00, :context => :past)
assert_equal Time.local(2006, 5, 28, 12), time
time = Chronic.parse("may 28 5pm", :now => @time_2006_08_16_14_00_00, :context => :past)
assert_equal Time.local(2006, 5, 28, 17), time
time = Chronic.parse("may 28 at 5pm", :now => @time_2006_08_16_14_00_00, :context => :past)
assert_equal Time.local(2006, 5, 28, 17), time
# rm_od
time = Chronic.parse("may 27th", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2007, 5, 27, 12), time
time = Chronic.parse("may 27th", :now => @time_2006_08_16_14_00_00, :context => :past)
assert_equal Time.local(2006, 5, 27, 12), time
time = Chronic.parse("may 27th 5:00 pm", :now => @time_2006_08_16_14_00_00, :context => :past)
assert_equal Time.local(2006, 5, 27, 17), time
time = Chronic.parse("may 27th at 5pm", :now => @time_2006_08_16_14_00_00, :context => :past)
assert_equal Time.local(2006, 5, 27, 17), time
time = Chronic.parse("may 27th at 5", :now => @time_2006_08_16_14_00_00, :ambiguous_time_range => :none)
assert_equal Time.local(2007, 5, 27, 5), time
# rm_sy
time = Chronic.parse("June 1979", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(1979, 6, 16, 0), time
time = Chronic.parse("dec 79", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(1979, 12, 16, 12), time
# rm_sd_sy
time = Chronic.parse("jan 3 2010", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2010, 1, 3, 12), time
time = Chronic.parse("jan 3 2010 midnight", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2010, 1, 4, 0), time
time = Chronic.parse("jan 3 2010 at midnight", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2010, 1, 4, 0), time
time = Chronic.parse("jan 3 2010 at 4", :now => @time_2006_08_16_14_00_00, :ambiguous_time_range => :none)
assert_equal Time.local(2010, 1, 3, 4), time
#time = Chronic.parse("January 12, '00", :now => @time_2006_08_16_14_00_00)
#assert_equal Time.local(2000, 1, 12, 12), time
time = Chronic.parse("may 27 79", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(1979, 5, 27, 12), time
time = Chronic.parse("may 27 79 4:30", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(1979, 5, 27, 16, 30), time
time = Chronic.parse("may 27 79 at 4:30", :now => @time_2006_08_16_14_00_00, :ambiguous_time_range => :none)
assert_equal Time.local(1979, 5, 27, 4, 30), time
# sd_rm_sy
time = Chronic.parse("3 jan 2010", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2010, 1, 3, 12), time
time = Chronic.parse("3 jan 2010 4pm", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2010, 1, 3, 16), time
# sm_sd_sy
time = Chronic.parse("5/27/1979", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(1979, 5, 27, 12), time
time = Chronic.parse("5/27/1979 4am", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(1979, 5, 27, 4), time
# sd_sm_sy
time = Chronic.parse("27/5/1979", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(1979, 5, 27, 12), time
time = Chronic.parse("27/5/1979 @ 0700", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(1979, 5, 27, 7), time
# sm_sy
time = Chronic.parse("05/06", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 5, 16, 12), time
time = Chronic.parse("12/06", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 12, 16, 12), time
time = Chronic.parse("13/06", :now => @time_2006_08_16_14_00_00)
assert_equal nil, time
# sy_sm_sd
time = Chronic.parse("2000-1-1", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2000, 1, 1, 12), time
time = Chronic.parse("2006-08-20", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 20, 12), time
time = Chronic.parse("2006-08-20 7pm", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 20, 19), time
time = Chronic.parse("2006-08-20 03:00", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 20, 3), time
# rm_sd_rt
#time = Chronic.parse("jan 5 13:00", :now => @time_2006_08_16_14_00_00)
#assert_equal Time.local(2007, 1, 5, 13), time
# due to limitations of the Time class, these don't work
time = Chronic.parse("may 40", :now => @time_2006_08_16_14_00_00)
assert_equal nil, time
time = Chronic.parse("may 27 40", :now => @time_2006_08_16_14_00_00)
assert_equal nil, time
time = Chronic.parse("1800-08-20", :now => @time_2006_08_16_14_00_00)
assert_equal nil, time
end
def test_parse_guess_r
time = Chronic.parse("friday", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 18, 12), time
time = Chronic.parse("5", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 16, 17), time
time = Chronic.parse("5", :now => @time_2006_08_16_03_00_00, :ambiguous_time_range => :none)
assert_equal Time.local(2006, 8, 16, 5), time
time = Chronic.parse("13:00", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 17, 13), time
time = Chronic.parse("13:45", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 17, 13, 45), time
time = Chronic.parse("november", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 11, 16), time
end
def test_parse_guess_rr
time = Chronic.parse("friday 13:00", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 18, 13), time
time = Chronic.parse("monday 4:00", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 21, 16), time
time = Chronic.parse("sat 4:00", :now => @time_2006_08_16_14_00_00, :ambiguous_time_range => :none)
assert_equal Time.local(2006, 8, 19, 4), time
time = Chronic.parse("sunday 4:20", :now => @time_2006_08_16_14_00_00, :ambiguous_time_range => :none)
assert_equal Time.local(2006, 8, 20, 4, 20), time
time = Chronic.parse("4 pm", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 16, 16), time
time = Chronic.parse("4 am", :now => @time_2006_08_16_14_00_00, :ambiguous_time_range => :none)
assert_equal Time.local(2006, 8, 16, 4), time
time = Chronic.parse("4:00 in the morning", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 16, 4), time
#time = Chronic.parse("november 4", :now => @time_2006_08_16_14_00_00)
#assert_equal Time.local(2006, 11, 4, 12), time
end
def test_parse_guess_rrr
time = Chronic.parse("friday 1 pm", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 18, 13), time
time = Chronic.parse("friday 11 at night", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 18, 23), time
time = Chronic.parse("friday 11 in the evening", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 18, 23), time
time = Chronic.parse("sunday 6am", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 20, 6), time
end
def test_parse_guess_gr
# year
time = Chronic.parse("this year", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 10, 24, 12, 30), time
time = Chronic.parse("this year", :now => @time_2006_08_16_14_00_00, :context => :past)
assert_equal Time.local(2006, 4, 24, 12, 30), time
# month
time = Chronic.parse("this month", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 24, 12), time
time = Chronic.parse("this month", :now => @time_2006_08_16_14_00_00, :context => :past)
assert_equal Time.local(2006, 8, 8, 12), time
# month name
time = Chronic.parse("last november", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2005, 11, 16), time
# fortnight
time = Chronic.parse("this fortnight", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 21, 19, 30), time
time = Chronic.parse("this fortnight", :now => @time_2006_08_16_14_00_00, :context => :past)
assert_equal Time.local(2006, 8, 14, 19), time
# week
time = Chronic.parse("this week", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 18, 7, 30), time
time = Chronic.parse("this week", :now => @time_2006_08_16_14_00_00, :context => :past)
assert_equal Time.local(2006, 8, 14, 19), time
# day
time = Chronic.parse("this day", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 16, 19, 30), time
time = Chronic.parse("this day", :now => @time_2006_08_16_14_00_00, :context => :past)
assert_equal Time.local(2006, 8, 16, 7), time
time = Chronic.parse("today", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 16, 19, 30), time
time = Chronic.parse("yesterday", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 15, 12), time
time = Chronic.parse("tomorrow", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 17, 12), time
# day name
time = Chronic.parse("this tuesday", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 22, 12), time
time = Chronic.parse("next tuesday", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 22, 12), time
time = Chronic.parse("last tuesday", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 15, 12), time
time = Chronic.parse("this wed", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 23, 12), time
time = Chronic.parse("next wed", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 23, 12), time
time = Chronic.parse("last wed", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 9, 12), time
# day portion
time = Chronic.parse("this morning", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 16, 9), time
time = Chronic.parse("tonight", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 16, 22), time
# second
time = Chronic.parse("this second", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 16, 14), time
time = Chronic.parse("this second", :now => @time_2006_08_16_14_00_00, :context => :past)
assert_equal Time.local(2006, 8, 16, 14), time
time = Chronic.parse("next second", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 16, 14, 0, 1), time
time = Chronic.parse("last second", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 16, 13, 59, 59), time
end
def test_parse_guess_grr
time = Chronic.parse("yesterday at 4:00", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 15, 16), time
time = Chronic.parse("yesterday at 4:00", :now => @time_2006_08_16_14_00_00, :ambiguous_time_range => :none)
assert_equal Time.local(2006, 8, 15, 4), time
time = Chronic.parse("last friday at 4:00", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 11, 16), time
time = Chronic.parse("next wed 4:00", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 23, 16), time
time = Chronic.parse("yesterday afternoon", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 15, 15), time
time = Chronic.parse("last week tuesday", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 8, 12), time
end
def test_parse_guess_grrr
time = Chronic.parse("today at 6:00pm", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 16, 18), time
time = Chronic.parse("this day 1800", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 16, 18), time
time = Chronic.parse("yesterday at 4:00pm", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 15, 16), time
end
def test_parse_guess_rgr
time = Chronic.parse("afternoon yesterday", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 15, 15), time
time = Chronic.parse("tuesday last week", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 8, 12), time
end
def test_parse_guess_s_r_p
# past
time = Chronic.parse("3 years ago", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2003, 8, 16, 14, 30, 30), time
time = Chronic.parse("1 month ago", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 7, 16, 14, 30, 30), time
time = Chronic.parse("1 fortnight ago", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 2, 14, 30, 30), time
time = Chronic.parse("2 fortnights ago", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 7, 19, 14, 30, 30), time
time = Chronic.parse("3 weeks ago", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 7, 26, 14, 30, 30), time
time = Chronic.parse("3 days ago", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 13, 14, 0, 30), time
#time = Chronic.parse("1 monday ago", :now => @time_2006_08_16_14_00_00)
#assert_equal Time.local(2006, 8, 14, 12), time
time = Chronic.parse("5 mornings ago", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 12, 9), time
time = Chronic.parse("7 hours ago", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 16, 7, 0, 30), time
time = Chronic.parse("3 minutes ago", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 16, 13, 57), time
time = Chronic.parse("20 seconds before now", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 16, 13, 59, 40), time
# future
time = Chronic.parse("3 years from now", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2009, 8, 16, 14, 30, 30), time
time = Chronic.parse("6 months hence", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2007, 2, 16, 14, 30, 30), time
time = Chronic.parse("3 fortnights hence", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 9, 27, 14, 30, 30), time
time = Chronic.parse("1 week from now", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 23, 14, 30, 30), time
time = Chronic.parse("1 day hence", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 17, 14, 0, 30), time
time = Chronic.parse("5 mornings hence", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 21, 9), time
time = Chronic.parse("1 hour from now", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 16, 15, 0, 30), time
time = Chronic.parse("20 minutes hence", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 16, 14, 20), time
time = Chronic.parse("20 seconds from now", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 16, 14, 0, 20), time
end
def test_parse_guess_p_s_r
time = Chronic.parse("in 3 hours", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 16, 17, 0, 30), time
end
def test_parse_guess_s_r_p_a
# past
time = Chronic.parse("3 years ago tomorrow", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2003, 8, 17, 12), time
time = Chronic.parse("3 years ago this friday", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2003, 8, 18, 12), time
time = Chronic.parse("3 months ago saturday at 5:00 pm", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 5, 19, 17), time
time = Chronic.parse("2 days from this second", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 18, 14), time
time = Chronic.parse("7 hours before tomorrow at midnight", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 17, 17), time
# future
end
def test_parse_guess_o_r_s_r
time = Chronic.parse("3rd wednesday in november", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 11, 15, 12), time
time = Chronic.parse("10th wednesday in november", :now => @time_2006_08_16_14_00_00)
assert_equal nil, time
end
def test_parse_guess_o_r_g_r
time = Chronic.parse("3rd month next year", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2007, 3, 16, 12, 30), time
time = Chronic.parse("3rd thursday this september", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 9, 21, 12), time
time = Chronic.parse("4th day last week", :now => @time_2006_08_16_14_00_00)
assert_equal Time.local(2006, 8, 9, 12), time
end
def test_parse_guess_nonsense
time = Chronic.parse("some stupid nonsense", :now => @time_2006_08_16_14_00_00)
assert_equal nil, time
end
def test_parse_span
span = Chronic.parse("friday", :now => @time_2006_08_16_14_00_00, :guess => false)
assert_equal Time.local(2006, 8, 18), span.begin
assert_equal Time.local(2006, 8, 19), span.end
span = Chronic.parse("november", :now => @time_2006_08_16_14_00_00, :guess => false)
assert_equal Time.local(2006, 11), span.begin
assert_equal Time.local(2006, 12), span.end
end
def test_argument_validation
assert_raise(Chronic::InvalidArgumentException) do
time = Chronic.parse("may 27", :foo => :bar)
end
assert_raise(Chronic::InvalidArgumentException) do
time = Chronic.parse("may 27", :context => :bar)
end
end
end

View file

@ -0,0 +1,184 @@
Academic Free License (AFL) v. 3.0
This Academic Free License (the "License") applies to any original work
of authorship (the "Original Work") whose owner (the "Licensor") has
placed the following licensing notice adjacent to the copyright notice
for the Original Work:
Licensed under the Academic Free License version 3.0
1) Grant of Copyright License. Licensor grants You a worldwide,
royalty-free, non-exclusive, sublicensable license, for the duration of
the copyright, to do the following:
a) to reproduce the Original Work in copies, either alone or as part of
a collective work;
b) to translate, adapt, alter, transform, modify, or arrange the
Original Work, thereby creating derivative works ("Derivative Works")
based upon the Original Work;
c) to distribute or communicate copies of the Original Work and
Derivative Works to the public, under any license of your choice that
does not contradict the terms and conditions, including Licensor's
reserved rights and remedies, in this Academic Free License;
d) to perform the Original Work publicly; and
e) to display the Original Work publicly.
2) Grant of Patent License. Licensor grants You a worldwide,
royalty-free, non-exclusive, sublicensable license, under patent claims
owned or controlled by the Licensor that are embodied in the Original
Work as furnished by the Licensor, for the duration of the patents, to
make, use, sell, offer for sale, have made, and import the Original Work
and Derivative Works.
3) Grant of Source Code License. The term "Source Code" means the
preferred form of the Original Work for making modifications to it and
all available documentation describing how to modify the Original Work.
Licensor agrees to provide a machine-readable copy of the Source Code of
the Original Work along with each copy of the Original Work that
Licensor distributes. Licensor reserves the right to satisfy this
obligation by placing a machine-readable copy of the Source Code in an
information repository reasonably calculated to permit inexpensive and
convenient access by You for as long as Licensor continues to distribute
the Original Work.
4) Exclusions From License Grant. Neither the names of Licensor, nor the
names of any contributors to the Original Work, nor any of their
trademarks or service marks, may be used to endorse or promote products
derived from this Original Work without express prior permission of the
Licensor. Except as expressly stated herein, nothing in this License
grants any license to Licensor's trademarks, copyrights, patents, trade
secrets or any other intellectual property. No patent license is granted
to make, use, sell, offer for sale, have made, or import embodiments of
any patent claims other than the licensed claims defined in Section 2.
No license is granted to the trademarks of Licensor even if such marks
are included in the Original Work. Nothing in this License shall be
interpreted to prohibit Licensor from licensing under terms different
from this License any Original Work that Licensor otherwise would have a
right to license.
5) External Deployment. The term "External Deployment" means the use,
distribution, or communication of the Original Work or Derivative Works
in any way such that the Original Work or Derivative Works may be used
by anyone other than You, whether those works are distributed or
communicated to those persons or made available as an application
intended for use over a network. As an express condition for the grants
of license hereunder, You must treat any External Deployment by You of
the Original Work or a Derivative Work as a distribution under section
1(c).
6) Attribution Rights. You must retain, in the Source Code of any
Derivative Works that You create, all copyright, patent, or trademark
notices from the Source Code of the Original Work, as well as any
notices of licensing and any descriptive text identified therein as an
"Attribution Notice." You must cause the Source Code for any Derivative
Works that You create to carry a prominent Attribution Notice reasonably
calculated to inform recipients that You have modified the Original
Work.
7) Warranty of Provenance and Disclaimer of Warranty. Licensor warrants
that the copyright in and to the Original Work and the patent rights
granted herein by Licensor are owned by the Licensor or are sublicensed
to You under the terms of this License with the permission of the
contributor(s) of those copyrights and patent rights. Except as
expressly stated in the immediately preceding sentence, the Original
Work is provided under this License on an "AS IS" BASIS and WITHOUT
WARRANTY, either express or implied, including, without limitation, the
warranties of non-infringement, merchantability or fitness for a
particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL
WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential
part of this License. No license to the Original Work is granted by this
License except under this disclaimer.
8) Limitation of Liability. Under no circumstances and under no legal
theory, whether in tort (including negligence), contract, or otherwise,
shall the Licensor be liable to anyone for any indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or the use of the Original Work including,
without limitation, damages for loss of goodwill, work stoppage,
computer failure or malfunction, or any and all other commercial damages
or losses. This limitation of liability shall not apply to the extent
applicable law prohibits such limitation.
9) Acceptance and Termination. If, at any time, You expressly assented
to this License, that assent indicates your clear and irrevocable
acceptance of this License and all of its terms and conditions. If You
distribute or communicate copies of the Original Work or a Derivative
Work, You must make a reasonable effort under the circumstances to
obtain the express assent of recipients to the terms of this License.
This License conditions your rights to undertake the activities listed
in Section 1, including your right to create Derivative Works based upon
the Original Work, and doing so without honoring these terms and
conditions is prohibited by copyright law and international treaty.
Nothing in this License is intended to affect copyright exceptions and
limitations (including "fair use" or "fair dealing"). This License shall
terminate immediately and You may no longer exercise any of the rights
granted to You by this License upon your failure to honor the conditions
in Section 1(c).
10) Termination for Patent Action. This License shall terminate
automatically and You may no longer exercise any of the rights granted
to You by this License as of the date You commence an action, including
a cross-claim or counterclaim, against Licensor or any licensee alleging
that the Original Work infringes a patent. This termination provision
shall not apply for an action alleging patent infringement by
combinations of the Original Work with other software or hardware.
11) Jurisdiction, Venue and Governing Law. Any action or suit relating
to this License may be brought only in the courts of a jurisdiction
wherein the Licensor resides or in which Licensor conducts its primary
business, and under the laws of that jurisdiction excluding its
conflict-of-law provisions. The application of the United Nations
Convention on Contracts for the International Sale of Goods is expressly
excluded. Any use of the Original Work outside the scope of this License
or after its termination shall be subject to the requirements and
penalties of copyright or patent law in the appropriate jurisdiction.
This section shall survive the termination of this License.
12) Attorneys' Fees. In any action to enforce the terms of this License
or seeking damages relating thereto, the prevailing party shall be
entitled to recover its costs and expenses, including, without
limitation, reasonable attorneys' fees and costs incurred in connection
with such action, including any appeal of such action. This section
shall survive the termination of this License.
13) Miscellaneous. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable.
14) Definition of "You" in This License. "You" throughout this License,
whether in upper or lower case, means an individual or a legal entity
exercising rights under, and complying with all of the terms of, this
License. For legal entities, "You" includes any entity that controls, is
controlled by, or is under common control with you. For purposes of this
definition, "control" means (i) the power, direct or indirect, to cause
the direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
15) Right to Use. You may use the Original Work in all ways not
otherwise restricted or conditioned by this License or by law, and
Licensor promises not to interfere with or be responsible for such uses
by You.
16) Modification of This License. This License is Copyright (c) 2005
Lawrence Rosen. Permission is granted to copy, distribute, or
communicate this License without modification. Nothing in this License
permits You to modify this License as applied to the Original Work or to
Derivative Works. However, You may modify the text of this License and
copy, distribute or communicate your modified version (the "Modified
License") and apply it to other original works of authorship subject to
the following conditions: (i) You may not indicate in any way that your
Modified License is the "Academic Free License" or "AFL" and you may not
use those names in the name of your Modified License; (ii) You must
replace the notice specified in the first paragraph above with the
notice "Licensed under <insert your license name here>" or with a notice
of your own that is not confusingly similar to the notice in this
License; and (iii) You may not claim that your original works are open
source software unless your Modified License has been approved by Open
Source Initiative (OSI) and You comply with its license review and
certification process.

View file

@ -0,0 +1,46 @@
Self-referential, polymorphic has_many :through helper
Copyright 2006 Evan Weaver (see the LICENSE file)
"model :parent_class" may be required in some controllers or perhaps models in order for reloading to work properly, since the parent setup must be executed on the child every time the child class is reloaded.
Usage and help:
http://blog.evanweaver.com/articles/2006/06/02/has_many_polymorphs
Also see the source code, although it's probably not going to be super helpful to you.
Changelog:
22. api change; prefix on methods is now singular when using :rename_individual_collections
21. add configuration option to cache polymorphic classes in development mode
20. collection methods (push, delete, clear) now on individual collections
19.2. disjoint collection sides bugfix, don't raise on new records
19.1. double classify bugfix
19. large changes to properly support double polymorphism
18.2. bugfix to make sure the type gets checked on doubly polymorphic parents
18.1. bugfix for sqlite3 child attribute retrieval
18. bugfix for instantiating attributes of namespaced models
17.1. bugfix for double polymorphic relationships
17. double polymorphic relationships (includes new API method)
16. namespaced model support
15. bugfix for postgres and mysql under 1.1.6; refactored tests (thanks hildofur); properly handles legacy table names set with set_table_name()
14. sti support added (use the child class names, not the base class)
13. bug regarding table names with underscores in SQL query fixed
12.1. license change
12. file_column bug fixed
11. tests written; after_find and after_initialize now correctly called
10. bugfix
9. rollback
8. SQL performance enhancements added
7. rewrote singletons as full-fledged proxy class so that marshalling works (e.g. in the session)
6. caching added
5. fixed dependency reloading problem in development mode
4. license change
3. added :dependent support on the join table
1-2. no changelog
Known problems:
1. Plugin's test fixtures do not load properly for non-edge postgres, invalidating the tests.
2. quote_value() hack is stupid.

View file

@ -0,0 +1 @@
require 'has_many_polymorphs'

View file

@ -0,0 +1,581 @@
# self-referential, polymorphic has_many :through plugin
# http://blog.evanweaver.com/articles/2006/06/02/has_many_polymorphs
# operates via magic dust, and courage
if defined? Rails::Configuration
class Rails::Configuration
def has_many_polymorphs_cache_classes= *args
::ActiveRecord::Associations::ClassMethods.has_many_polymorphs_cache_classes = *args
end
end
end
module ActiveRecord
if ENV['RAILS_ENV'] =~ /development|test/ and ENV['USER'] == 'eweaver'
# enable this condition to get awesome association debugging
# you will get a folder "generated_models" in the current dir containing valid Ruby files
# explaining all ActiveRecord relationships set up by the plugin, as well as listing the
# line in the plugin that made each particular macro call
class << Base
COLLECTION_METHODS = [:belongs_to, :has_many, :has_and_belongs_to_many, :has_one].each do |method_name|
alias_method "original_#{method_name}".to_sym, method_name
undef_method method_name
end
unless defined? GENERATED_CODE_DIR
# automatic code generation for debugging... bitches
GENERATED_CODE_DIR = "generated_models"
system "rm -rf #{GENERATED_CODE_DIR}"
Dir.mkdir GENERATED_CODE_DIR
alias :original_method_missing :method_missing
def method_missing(method_name, *args, &block)
if COLLECTION_METHODS.include? method_name.to_sym
Dir.chdir GENERATED_CODE_DIR do
filename = "#{ActiveRecord::Associations::ClassMethods.demodulate(self.name.underscore)}.rb"
contents = File.open(filename).read rescue "\nclass #{self.name}\n\nend\n"
line = caller[1][/\:(\d+)\:/, 1]
contents[-5..-5] = "\n #{method_name} #{args[0..-2].inspect[1..-2]},\n #{args[-1].inspect[1..-2].gsub(" :", "\n :").gsub("=>", " => ")}\n#{ block ? " #{block.inspect.sub(/\@.*\//, '@')}\n" : ""} # called from line #{line}\n\n"
File.open(filename, "w") do |file|
file.puts contents
end
end
# doesn't handle blocks
self.send("original_#{method_name}", *args, &block)
else
self.send(:original_method_missing, method_name, *args, &block)
end
end
end
end
# and we want to track the reloader's shenanigans
(::Dependencies.log_activity = true) rescue nil
end
module Associations
module ClassMethods
mattr_accessor :has_many_polymorphs_cache_classes
def acts_as_double_polymorphic_join opts
raise RuntimeError, "Couldn't understand #{opts.inspect} options in acts_as_double_polymorphic_join. Please only specify the two relationships and their member classes; there are no options to set. " unless opts.length == 2
join_name = self.name.tableize.to_sym
opts.each do |polymorphs, children|
parent_hash_key = (opts.keys - [polymorphs]).first # parents are the entries in the _other_ children array
begin
parent_foreign_key = self.reflect_on_association(parent_hash_key.to_s.singularize.to_sym).primary_key_name
rescue NoMethodError
raise RuntimeError, "Couldn't find 'belongs_to' association for :#{parent_hash_key.to_s.singularize} in #{self.name}." unless parent_foreign_key
end
parents = opts[parent_hash_key]
conflicts = (children & parents) # set intersection
parents.each do |parent_name|
parent_class = parent_name.to_s.classify.constantize
reverse_polymorph = parent_hash_key.to_s.singularize
polymorph = polymorphs.to_s.singularize
parent_class.send(:has_many_polymorphs,
polymorphs, {:double => true,
:from => children,
:as => parent_hash_key.to_s.singularize.to_sym,
:through => join_name,
:dependent => :destroy,
:foreign_key => parent_foreign_key,
:foreign_type_key => parent_foreign_key.to_s.sub(/_id$/, '_type'),
:reverse_polymorph => reverse_polymorph,
:conflicts => conflicts,
:rename_individual_collections => false})
if conflicts.include? parent_name
# unify the alternate sides of the conflicting children
(conflicts).each do |method_name|
unless parent_class.instance_methods.include?(method_name)
parent_class.send(:define_method, method_name) do
(self.send("#{reverse_polymorph}_#{method_name}") +
self.send("#{polymorph}_#{method_name}")).freeze
end
end
end
# unify the join model
unless parent_class.instance_methods.include?(join_name)
parent_class.send(:define_method, join_name) do
(self.send("#{join_name}_as_#{reverse_polymorph}") +
self.send("#{join_name}_as_#{polymorph}")).freeze
end
end
end
end
end
end
def has_many_polymorphs(polymorphs, options, &block)
options.assert_valid_keys(:from, :acts_as, :as, :through, :foreign_key, :dependent, :double,
:rename_individual_collections, :foreign_type_key, :reverse_polymorph, :conflicts)
# the way this deals with extra parameters to the associations could use some work
options[:as] ||= options[:acts_as] ||= self.table_name.singularize.to_sym
# foreign keys follow the table name, not the class name in Rails 2.0
options[:foreign_key] ||= "#{options[:as].to_s}_id"
# no conflicts by default
options[:conflicts] ||= []
# construct the join table name
options[:through] ||= join_table((options[:as].to_s.pluralize or self.table_name), polymorphs)
if options[:reverse_polymorph]
options[:through_with_reverse_polymorph] = "#{options[:through]}_as_#{options[:reverse_polymorph]}".to_sym
else
options[:through_with_reverse_polymorph] = options[:through]
end
options[:join_class_name] ||= options[:through].to_s.classify
# the class must have_many on the join_table
opts = {:foreign_key => options[:foreign_key], :dependent => options[:dependent],
:class_name => options[:join_class_name]}
if options[:foreign_type_key]
opts[:conditions] = "#{options[:foreign_type_key]} = #{quote_value self.base_class.name}"
end
has_many demodulate(options[:through_with_reverse_polymorph]), opts
polymorph = polymorphs.to_s.singularize.to_sym
# add the base_class method to the join_table so that STI will work transparently
inject_before_save_into_join_table(options[:join_class_name], polymorph)
# get some reusable info
children, child_associations = {}, {}
options[:from].each do |child_plural|
children[child_plural] = child_plural.to_s.singularize.to_sym
child_associations[child_plural] = (options[:rename_individual_collections] ? "#{polymorph}_#{child_plural}".to_sym : child_plural)
end
# get our models out of the reloadable lists, if requested
if self.has_many_polymorphs_cache_classes
klasses = [self.name, options[:join_class_name], *children.values.map{|x| x.to_s.classify}]
klasses += basify_sti_classnames(klasses).keys.to_a.compact.uniq.map{|x| x.to_s.classify}
klasses.uniq!
klasses.each {|s| logger.debug "Ejecting #{s.inspect} from the autoload lists"}
begin
Dependencies.autoloaded_constants -= klasses
Dependencies.explicitly_unloadable_constants -= klasses
rescue NoMethodError
raise "Rails 1.2.0 or later is required to set config.has_many_polymorphs_cache_classes = true"
end
end
# auto-inject individually named associations for the children into the join model
create_virtual_associations_for_join_to_individual_children(children, polymorph, options)
# iterate through the polymorphic children, running the parent class's :has_many on each one
create_has_many_through_associations_for_parent_to_children(children, child_associations, polymorphs, polymorph, options)
# auto-inject the regular polymorphic associations into the child classes
create_has_many_through_associations_for_children_to_parent(children, polymorph, options)
create_general_collection_association_for_parent(polymorphs, polymorph, basify_sti_classnames(children), options, &block)
end
def self.demodulate(s)
s.to_s.gsub('/', '_').to_sym
end
protected
def demodulate(s)
ActiveRecord::Associations::ClassMethods.demodulate(s)
end
def basify_sti_classnames(hash)
# this blows
result = {}
hash.each do |plural, singular|
klass = plural.to_s.classify.constantize
if klass != klass.base_class
result[klass.base_class.table_name.to_sym] = klass.base_class.table_name.singularize.to_sym
else
result[plural] = singular
end
end
result
end
def inject_before_save_into_join_table(join_class_name, polymorph)
sti_hook = "sti_class_rewrite"
rewrite_procedure = %[
self.send(:#{polymorph}_type=, self.#{polymorph}_type.constantize.base_class.name)
]
# this also blows, and should be abstracted. alias_method_chain is not enough.
join_class_name.constantize.class_eval %[
unless instance_methods.include? "before_save_with_#{sti_hook}"
if instance_methods.include? "before_save"
alias_method :before_save_without_#{sti_hook}, :before_save
def before_save_with_#{sti_hook}
before_save_without_#{sti_hook}
#{rewrite_procedure}
end
else
def before_save_with_#{sti_hook}
#{rewrite_procedure}
end
end
alias_method :before_save, :before_save_with_#{sti_hook}
end
]
end
def create_virtual_associations_for_join_to_individual_children(children, polymorph, options)
children.each do |child_plural, child|
options[:join_class_name].constantize.instance_eval do
association_name = child.to_s
association_name += "_as_#{polymorph}" if options[:conflicts].include?(child_plural)
association = demodulate(association_name)
opts = {:class_name => child.to_s.classify,
:foreign_key => "#{polymorph}_id" }
unless self.reflect_on_all_associations.map(&:name).include? association
belongs_to association, opts
end
end
end
end
def create_has_many_through_associations_for_children_to_parent(children, polymorph, options)
children.each do |child_plural, child|
if child == options[:as]
raise RuntimeError, "You can't have a self-referential polymorphic has_many :through without renaming the non-polymorphic foreign key in the join model."
end
parent = self
child.to_s.classify.constantize.instance_eval do
# this shouldn't be called at all during doubles; there is no way to traverse to a
# double polymorphic parent (XXX is that right?)
unless options[:double] or options[:conflicts].include? self.name.tableize.to_sym
begin
require_dependency parent.name.underscore # XXX why is this here?
rescue MissingSourceFile
end
# the join table
through = demodulate(options[:through_with_reverse_polymorph]).to_s
through += "_as_child" if parent == self
through = through.to_sym
has_many through, :as => polymorph,
:class_name => options[:through].to_s.classify,
:dependent => options[:dependent]
association = options[:as].to_s.pluralize
association += "_of_#{polymorph.to_s.pluralize}" if options[:rename_individual_collections] # XXX check this
# the polymorphic parent association
has_many association.to_sym, :through => through,
:class_name => parent.name,
:source => options[:as],
:foreign_key => options[:foreign_key]
end
end
end
end
def create_has_many_through_associations_for_parent_to_children(children, child_associations, polymorphs, polymorph, options)
children.each do |child_plural, child|
#puts ":source => #{child}"
association = demodulate(child_associations[child_plural]).to_s
source = demodulate(child).to_s
if options[:conflicts].include? child_plural
# XXX what?
association = "#{polymorph}_#{association}" if options[:conflicts].include? self.name.tableize.to_sym
source += "_as_#{polymorph}"
end
# activerecord is broken when you try to anonymously extend an association in a namespaced model,
extension = self.class_eval %[
module #{association.classify + "AssociationExtension"}
def push *args
proxy_owner.send(:#{polymorphs}).send(:push, *args).select{|x| x.is_a? #{child.to_s.classify}}
end
alias :<< :push
def delete *args
proxy_owner.send(:#{polymorphs}).send(:delete, *args)
end
def clear
proxy_owner.send(:#{polymorphs}).send(:clear, #{child.to_s.classify})
end
self # required
end]
has_many association.to_sym, :through => demodulate(options[:through_with_reverse_polymorph]),
:source => source.to_sym,
:conditions => ["#{options[:join_class_name].constantize.table_name}.#{polymorph}_type = ?", child.to_s.classify.constantize.base_class.name],
:extend => extension
end
end
def create_general_collection_association_for_parent(collection_name, polymorph, children, options, &block)
# we need to explicitly rename all the columns because we are fetching all the children objects at once.
# if multiple objects have a 'title' column, for instance, there will be a collision and we will potentially
# lose data. if we alias the fields and then break them up later, there are no collisions.
join_model = options[:through].to_s.classify.constantize
# figure out what fields we wanna grab
select_fields = []
children.each do |plural, singular|
klass = plural.to_s.classify.constantize
klass.columns.map(&:name).each do |name|
select_fields << "#{klass.table_name}.#{name} as #{demodulate plural}_#{name}"
end
end
# now get the join model fields
join_model.columns.map(&:name).each do |name|
select_fields << "#{join_model.table_name}.#{name} as #{join_model.table_name}_#{name}"
end
from_table = self.table_name
left_joins = children.keys.map do |n|
klass = n.to_s.classify.constantize
"LEFT JOIN #{klass.table_name} ON #{join_model.table_name}.#{polymorph}_id = #{klass.table_name}.#{klass.primary_key} AND #{join_model.table_name}.#{polymorph}_type = '#{n.to_s.classify}'"
end
sql_query = 'SELECT ' + select_fields.join(', ') + " FROM #{join_model.table_name}" +
"\nJOIN #{from_table} as polymorphic_parent ON #{join_model.table_name}.#{options[:foreign_key]} = polymorphic_parent.#{self.primary_key}\n" +
left_joins.join("\n") + "\nWHERE "
if options[:foreign_type_key]
sql_query +="#{join_model.table_name}.#{options[:foreign_type_key]} = #{quote_value self.base_class.name} AND "
end
# for sqlite3 you have to reference the left-most table in WHERE clauses or rows with NULL
# join results sometimes get silently dropped. it's stupid.
sql_query += "#{join_model.table_name}.#{options[:foreign_key]} "
#puts("Built collection property query:\n #{sql_query}")
class_eval do
attr_accessor "#{collection_name}_cache"
cattr_accessor "#{collection_name}_options"
define_method(collection_name) do
if collection_name_cache = instance_variable_get("@#{collection_name}_cache")
#puts("Cache hit on #{collection_name}")
collection_name_cache
else
#puts("Cache miss on #{collection_name}")
rows = connection.select_all("#{sql_query}" + (new_record? ? "IS NULL" : "= #{self.id}"))
# this gives us a hash with keys for each object type
objectified = objectify_polymorphic_array(rows, "#{join_model}", "#{polymorph}_type")
# locally cache the different object types found
# this doesn't work... yet.
objectified.each do |key, array|
instance_variable_set("@#{ActiveRecord::Associations::ClassMethods.demodulate(key)}", array)
end
proxy_object = HasManyPolymorphsProxyCollection.new(objectified[:all], self, send("#{collection_name}_options"))
(class << proxy_object; self end).send(:class_eval, &block) if block_given?
instance_variable_set("@#{collection_name}_cache", proxy_object)
end
end
# in order not to break tests, see if we have been defined already
unless instance_methods.include? "reload_with_#{collection_name}"
define_method("reload_with_#{collection_name}") do
send("reload_without_#{collection_name}")
instance_variable_set("@#{collection_name}_cache", nil)
self
end
alias_method "reload_without_#{collection_name}", :reload
alias_method :reload, "reload_with_#{collection_name}"
end
end
send("#{collection_name}_options=",
options.merge(:collection_name => collection_name,
:type_key => "#{polymorph}_type",
:id_key => "#{polymorph}_id"))
# puts("Defined the collection proxy.\n#{collection_name}\n")
end
def join_table(a, b)
[a.to_s, b.to_s].sort.join("_").to_sym
end
unless self.respond_to? :quote_value
# hack it in (very badly) for Rails 1.1.6 people
def quote_value s
"'#{s.inspect[1..-2]}'"
end
end
end
################################################
# decided to leave this alone unless it becomes clear that there is some benefit
# in deriving from AssociationProxy
#
# the benefit would be custom finders on the collection, perhaps...
class HasManyPolymorphsProxyCollection < Array
alias :array_delete :delete
alias :array_push :push
alias :count :length
def initialize(contents, parent, options)
@parent = parent
@options = options
@join_class = options[:join_class_name].constantize
return if contents.blank?
super(contents)
end
def push(objs, args={})
objs = [objs] unless objs.is_a? Array
objs.each do |obj|
data = {@options[:foreign_key] => @parent.id,
@options[:type_key] => obj.class.base_class.to_s, @options[:id_key] => obj.id}
data.merge!({@options[:foreign_type_key] => @parent.class.base_class.to_s}) if @options[:foreign_type_key] # for double polymorphs
conditions_string = data.keys.map(&:to_s).push("").join(" = ? AND ")[0..-6]
if @join_class.find(:first, :conditions => [conditions_string] + data.values).blank?
@join_class.new(data).save!
end
end
if args[:reload]
reload
else
# we have to do this funky stuff instead of just array difference because +/.uniq returns a regular array,
# which doesn't have our special methods and configuration anymore
unless (difference = objs - collection).blank?
@parent.send("#{@options[:collection_name]}_cache=".to_sym, collection.array_push(*difference))
end
end
@parent.send(@options[:collection_name])
end
alias :<< :push
def delete(objs, args={})
if objs
objs = [objs] unless objs.is_a? Array
elsif args[:clear]
objs = collection
objs = objs.select{|obj| obj.is_a? args[:klass]} if args[:klass]
else
raise RuntimeError, "Invalid delete parameters (has_many_polymorphs)."
end
records = []
objs.each do |obj|
records += join_records.select do |record|
record.send(@options[:type_key]) == obj.class.base_class.to_s and
record.send(@options[:id_key]) == obj.id
end
end
reload if args[:reload]
unless records.blank?
records.map(&:destroy)
# XXX could be faster if we reversed the loops
deleted_items = collection.select do |item|
records.select {|join_record|
join_record.send(@options[:type_key]) == item.class.base_class.name and
join_record.send(@options[:id_key]) == item.id
}.length > 0
end
# keep the cache fresh, while we're at it. see comment in .push
deleted_items.each { |item| collection.array_delete(item) }
@parent.send("#{@options[:collection_name]}_cache=", collection)
return deleted_items unless deleted_items.empty?
end
nil
end
def clear(klass = nil)
result = delete(nil, :clear => true, :klass => klass)
return result if result
collection
end
def reload
# reset the cache, postponing reloading from the db until we really need it
@parent.reload
end
private
def join_records
@parent.send(ActiveRecord::Associations::ClassMethods.demodulate(@options[:through]))
end
def collection
@parent.send(@options[:collection_name])
end
end
end
class Base
# turns an array of hashes (db rows) into a hash consisting of :all (array of everything) and
# a hash key for each class type it finds, e.g. :posts and :comments
private
def objectify_polymorphic_array(array, join_model, type_field)
join_model = join_model.constantize
arrays_hash = {}
array.each do |element|
klass = element["#{join_model.table_name}_#{type_field}"].constantize
association = ActiveRecord::Associations::ClassMethods.demodulate(klass.name.pluralize.underscore.downcase)
hash = {}
# puts "Class #{klass.inspect}"
# puts "Association name: #{association.inspect}"
element.each do |key, value|
# puts "key #{key} - value #{value.inspect}"
if key =~ /^#{association}_(.+)/
hash[$1] = value
# puts "#{$1.inspect} assigned #{value.inspect}"
end
end
object = klass.instantiate(hash)
arrays_hash[:all] ||= []
arrays_hash[association] ||= []
arrays_hash[:all] << object
arrays_hash[association] << object
end
arrays_hash
end
end
end
#require 'ruby-debug'
#Debugger.start

View file

@ -0,0 +1,8 @@
swimmy:
id: 1
name: Swimmy
speed: 10
jaws:
id: 2
name: Jaws
speed: 20

View file

@ -0,0 +1,3 @@
shamu:
id: 1
name: Shamu

View file

@ -0,0 +1,6 @@
rover:
id: 1
name: Rover
spot:
id: 2
name: Spot

View file

@ -0,0 +1,8 @@
chloe:
id: 1
cat_type: Kitten
name: Chloe
alice:
id: 2
cat_type: Kitten
name: Alice

View file

@ -0,0 +1,3 @@
froggy:
id: 1
name: Froggy

View file

@ -0,0 +1,6 @@
kibbles:
the_petfood_primary_key: 1
name: Kibbles
bits:
the_petfood_primary_key: 2
name: Bits

View file

@ -0,0 +1,6 @@
puma:
id: 1
name: Puma
jacrazy:
id: 2
name: Jacrazy

View file

@ -0,0 +1,4 @@
class Aquatic::Fish < ActiveRecord::Base
# attr_accessor :after_find_test, :after_initialize_test
end

View file

@ -0,0 +1,7 @@
class Aquatic::PupilsWhale < ActiveRecord::Base
set_table_name "little_whale_pupils"
belongs_to :whale, :class_name => "Aquatic::Whale", :foreign_key => "whale_id"
belongs_to :aquatic_pupil, :polymorphic => true
end

View file

@ -0,0 +1,11 @@
# see http://dev.rubyonrails.org/ticket/5935
module Aquatic; end
require 'aquatic/fish'
require 'aquatic/pupils_whale'
class Aquatic::Whale < ActiveRecord::Base
has_many_polymorphs(:aquatic_pupils, :from => [:dogs, :"aquatic/fish"],
:through => "aquatic/pupils_whales") do
def blow; "result"; end
end
end

View file

@ -0,0 +1,13 @@
class BeautifulFightRelationship < ActiveRecord::Base
set_table_name 'keep_your_enemies_close'
belongs_to :enemy, :polymorphic => true
belongs_to :protector, :polymorphic => true
# polymorphic relationships with column names different from the relationship name
# are not supported by Rails
acts_as_double_polymorphic_join :enemies => [:dogs, :kittens, :frogs],
:protectors => [:wild_boars, :kittens, :"aquatic/fish", :dogs]
end

View file

@ -0,0 +1,5 @@
class Cat < ActiveRecord::Base
# STI base class
self.inheritance_column = 'cat_type'
end

View file

@ -0,0 +1,16 @@
class Dog < ActiveRecord::Base
attr_accessor :after_find_test, :after_initialize_test
set_table_name "bow_wows"
def after_find
@after_find_test = true
# puts "After find called on #{name}."
end
def after_initialize
@after_initialize_test = true
end
end

View file

@ -0,0 +1,10 @@
class EatersFoodstuff < ActiveRecord::Base
belongs_to :foodstuff, :class_name => "Petfood", :foreign_key => "foodstuff_id"
belongs_to :eater, :polymorphic => true
def before_save
self.some_attribute = 3
end
end

View file

@ -0,0 +1,4 @@
class Frog < ActiveRecord::Base
end

View file

@ -0,0 +1,3 @@
class Kitten < Cat
# has_many :eaters_parents, :dependent => true, :as => 'eater'
end

View file

@ -0,0 +1,21 @@
# see http://dev.rubyonrails.org/ticket/5935
require 'eaters_foodstuff'
require 'petfood'
require 'cat'
module Aquatic; end
require 'aquatic/fish'
require 'dog'
require 'wild_boar'
require 'kitten'
require 'tabby'
class Petfood < ActiveRecord::Base
set_primary_key 'the_petfood_primary_key'
has_many_polymorphs :eaters,
:from => [:dogs, :petfoods, :wild_boars, :kittens,
:tabbies, :"aquatic/fish"],
:dependent => :destroy,
:rename_individual_collections => true,
:acts_as => :foodstuff,
:foreign_key => "foodstuff_id"
end

View file

@ -0,0 +1,2 @@
class Tabby < Cat
end

View file

@ -0,0 +1,3 @@
class WildBoar < ActiveRecord::Base
end

View file

@ -0,0 +1,52 @@
ActiveRecord::Schema.define(:version => 0) do
create_table :petfoods, :force => true, :primary_key => :the_petfood_primary_key do |t|
t.column :name, :string
end
create_table :bow_wows, :force => true do |t|
t.column :name, :string
end
create_table :cats, :force => true do |t|
t.column :name, :string
t.column :cat_type, :string
end
create_table :frogs, :force => true do |t|
t.column :name, :string
end
create_table :wild_boars, :force => true do |t|
t.column :name, :string
end
create_table :eaters_foodstuffs, :force => true do |t|
t.column :foodstuff_id, :integer
t.column :eater_id, :integer
t.column :some_attribute, :integer, :default => 0
t.column :eater_type, :string
end
create_table :fish, :force => true do |t|
t.column :name, :string
t.column :speed, :integer
end
create_table :whales, :force => true do |t|
t.column :name, :string
end
create_table :little_whale_pupils, :force => true do |t|
t.column :whale_id, :integer
t.column :aquatic_pupil_id, :integer
t.column :aquatic_pupil_type, :string
end
create_table :keep_your_enemies_close, :force => true do |t|
t.column :enemy_id, :integer
t.column :enemy_type, :string
t.column :protector_id, :integer
t.column :protector_type, :string
end
end

View file

@ -0,0 +1,23 @@
require 'pathname'
# default test helper
begin
require File.dirname(__FILE__) + '/../../../../test/test_helper'
rescue LoadError
require '~/projects/miscellaneous/cookbook/test/test_helper'
end
Inflector.inflections {|i| i.irregular 'fish', 'fish' }
# fixtures
$LOAD_PATH.unshift(Test::Unit::TestCase.fixture_path = File.dirname(__FILE__) + "/fixtures/")
# models
$LOAD_PATH.unshift("#{Pathname.new(__FILE__).dirname.to_s}/models")
class Test::Unit::TestCase
self.use_transactional_fixtures = true # must stay true for tests to run on postgres or sqlite3
self.use_instantiated_fixtures = false
end
# test schema
load(File.dirname(__FILE__) + "/schema.rb")

View file

@ -0,0 +1,487 @@
require File.dirname(__FILE__) + '/../test_helper'
class PolymorphTest < Test::Unit::TestCase
fixtures :cats, :bow_wows, :frogs, :wild_boars, :eaters_foodstuffs, :petfoods,
:"aquatic/fish", :"aquatic/whales", :"aquatic/little_whale_pupils",
:keep_your_enemies_close
require 'beautiful_fight_relationship'
# to-do: finder queries on the collection
# order-mask column on the join table for polymorphic order
# rework load order so you could push and pop without ever loading the whole collection
# so that limit works in a sane way
def setup
@kibbles = Petfood.find(1)
@bits = Petfood.find(2)
@shamu = Aquatic::Whale.find(1)
@swimmy = Aquatic::Fish.find(1)
@rover = Dog.find(1)
@spot = Dog.find(2)
@puma = WildBoar.find(1)
@chloe = Kitten.find(1)
@alice = Kitten.find(2)
@froggy = Frog.find(1)
@join_count = EatersFoodstuff.count
@l = @kibbles.eaters.length
@m = @bits.eaters.count
end
def test_all_relationship_validities
# q = []
# ObjectSpace.each_object(Class){|c| q << c if c.ancestors.include? ActiveRecord::Base }
# q.each{|c| puts "#{c.name}.reflect_on_all_associations.map &:check_validity! "}
Petfood.reflect_on_all_associations.map &:check_validity!
Tabby.reflect_on_all_associations.map &:check_validity!
Kitten.reflect_on_all_associations.map &:check_validity!
Dog.reflect_on_all_associations.map &:check_validity!
Aquatic::Fish.reflect_on_all_associations.map &:check_validity!
EatersFoodstuff.reflect_on_all_associations.map &:check_validity!
WildBoar.reflect_on_all_associations.map &:check_validity!
Frog.reflect_on_all_associations.map &:check_validity!
Aquatic::Whale.reflect_on_all_associations.map &:check_validity!
Cat.reflect_on_all_associations.map &:check_validity!
Aquatic::PupilsWhale.reflect_on_all_associations.map &:check_validity!
BeautifulFightRelationship.reflect_on_all_associations.map &:check_validity!
end
def test_assignment
assert @kibbles.eaters.blank?
assert @kibbles.eaters.push(Cat.find_by_name('Chloe'))
assert_equal @l += 1, @kibbles.eaters.count
@kibbles.reload
assert_equal @l, @kibbles.eaters.count
end
def test_duplicate_assignment
# try to add a duplicate item
@kibbles.eaters.push(@alice)
assert @kibbles.eaters.include?(@alice)
@kibbles.eaters.push(@alice)
assert_equal @l + 1, @kibbles.eaters.count
assert_equal @join_count + 1, EatersFoodstuff.count
@kibbles.reload
assert_equal @l + 1, @kibbles.eaters.count
assert_equal @join_count + 1, EatersFoodstuff.count
end
def test_create_and_push
assert @kibbles.eaters.push(@spot)
assert_equal @l += 1, @kibbles.eaters.count
assert @kibbles.eaters << @rover
assert @kibbles.eaters << Kitten.create(:name => "Miranda")
assert_equal @l += 2, @kibbles.eaters.length
@kibbles.reload
assert_equal @l, @kibbles.eaters.length
# test that ids and new flags were set appropriately
assert_not_nil @kibbles.eaters[0].id
assert !@kibbles.eaters[1].new_record?
end
def test_reload
assert @kibbles.reload
assert @kibbles.eaters.reload
end
def test_add_join_record
assert_equal Kitten, @chloe.class
assert @join_record = EatersFoodstuff.new(:foodstuff_id => @bits.id, :eater_id => @chloe.id, :eater_type => @chloe.class.name )
assert @join_record.save!
assert @join_record.id
assert_equal @join_count + 1, EatersFoodstuff.count
# has the parent changed if we don't reload?
assert_equal @m, @bits.eaters.count
# if we do reload, is the new association there?
# XXX no, because TestCase breaks reload. it works fine in the app.
assert_equal Petfood, @bits.eaters.reload.class
assert_equal @m + 1, @bits.eaters.count
assert @bits.eaters.include?(@chloe)
# puts "XXX #{EatersFoodstuff.count}"
end
def test_add_unsaved
# add an unsaved item
assert @bits.eaters << Kitten.new(:name => "Bridget")
assert_nil Kitten.find_by_name("Bridget")
assert_equal @m + 1, @bits.eaters.count
assert @bits.save
@bits.reload
assert_equal @m + 1, @bits.eaters.count
end
def test_self_reference
assert @kibbles.eaters << @bits
assert_equal @l += 1, @kibbles.eaters.count
assert @kibbles.eaters.include?(@bits)
@kibbles.reload
assert @kibbles.foodstuffs_of_eaters.blank?
@bits.reload
assert @bits.foodstuffs_of_eaters.include?(@kibbles)
assert_equal [@kibbles], @bits.foodstuffs_of_eaters
end
def test_remove
assert @kibbles.eaters << @chloe
@kibbles.reload
assert @kibbles.eaters.delete(@kibbles.eaters[0])
assert_equal @l, @kibbles.eaters.count
end
def test_destroy
assert @kibbles.eaters.push(@chloe)
@kibbles.reload
assert @kibbles.eaters.length > 0
assert @kibbles.eaters[0].destroy
@kibbles.reload
assert_equal @l, @kibbles.eaters.count
end
def test_clear
@kibbles.eaters << [@chloe, @spot, @rover]
@kibbles.reload
assert_equal 3, @kibbles.eaters.clear.size
assert @kibbles.eaters.blank?
@kibbles.reload
assert @kibbles.eaters.blank?
assert_equal 0, @kibbles.eaters.clear.size
end
def test_individual_collections
assert @kibbles.eaters.push(@chloe)
# check if individual collections work
assert_equal @kibbles.eater_kittens.length, 1
assert @kibbles.eater_dogs
assert 1, @rover.eaters_foodstuffs.count
end
def test_invididual_collections_push
assert_equal [@chloe], (@kibbles.eater_kittens << @chloe)
@kibbles.reload
assert @kibbles.eaters.include?(@chloe)
assert @kibbles.eater_kittens.include?(@chloe)
assert !@kibbles.eater_dogs.include?(@chloe)
end
def test_invididual_collections_delete
@kibbles.eaters << [@chloe, @spot, @rover]
@kibbles.reload
assert_equal [@chloe], @kibbles.eater_kittens.delete(@chloe)
assert @kibbles.eater_kittens.empty?
assert !@kibbles.eater_kittens.delete(@chloe)
@kibbles.reload
assert @kibbles.eater_kittens.empty?
assert @kibbles.eater_dogs.include?(@spot)
end
def test_invididual_collections_clear
@kibbles.eaters << [@chloe, @spot, @rover]
@kibbles.reload
assert_equal [@chloe], @kibbles.eater_kittens.clear
assert @kibbles.eater_kittens.empty?
assert_equal 2, @kibbles.eaters.size
@kibbles.reload
assert @kibbles.eater_kittens.empty?
assert_equal 2, @kibbles.eaters.size
assert !@kibbles.eater_kittens.include?(@chloe)
assert !@kibbles.eaters.include?(@chloe)
end
def test_childrens_individual_collections
assert Cat.find_by_name('Chloe').eaters_foodstuffs
assert @kibbles.eaters_foodstuffs
end
def test_self_referential_join_tables
# check that the self-reference join tables go the right ways
assert_equal @l, @kibbles.eaters_foodstuffs.count
assert_equal @kibbles.eaters_foodstuffs.count, @kibbles.eaters_foodstuffs_as_child.count
end
def test_dependent
assert @kibbles.eaters << @chloe
@kibbles.reload
# delete ourself and see if :dependent was obeyed
dependent_rows = @kibbles.eaters_foodstuffs
assert_equal dependent_rows.length, @kibbles.eaters.count
@join_count = EatersFoodstuff.count
@kibbles.destroy
assert_equal @join_count - dependent_rows.length, EatersFoodstuff.count
assert_equal 0, EatersFoodstuff.find(:all, :conditions => ['foodstuff_id = ?', 1] ).length
end
def test_normal_callbacks
assert @rover.respond_to?(:after_initialize)
assert @rover.respond_to?(:after_find)
assert @rover.after_initialize_test
assert @rover.after_find_test
end
def test_our_callbacks
assert 0, @bits.eaters.count
assert @bits.eaters.push(@rover)
@bits.save
# puts "Testing callbacks."
@bits2 = Petfood.find_by_name("Bits")
@bits.reload
assert rover = @bits2.eaters.select { |x| x.name == "Rover" }[0]
assert rover.after_initialize_test
assert rover.after_find_test
# puts "Done."
end
def test_number_of_join_records
assert EatersFoodstuff.create(:foodstuff_id => 1, :eater_id => 1, :eater_type => "Cat")
@join_count = EatersFoodstuff.count
assert @join_count > 0
end
def test_number_of_regular_records
dogs = Dog.count
assert Dog.new(:name => "Auggie").save!
assert dogs + 1, Dog.count
end
def test_attributes_come_through_when_child_has_underscore_in_table_name
@join_record = EatersFoodstuff.new(:foodstuff_id => @bits.id, :eater_id => @puma.id, :eater_type => @puma.class.name)
@join_record.save!
@bits.eaters.reload
assert_equal 'Puma', @puma.name
assert_equal 'Puma', @bits.eaters.first.name
end
def test_before_save_on_join_table_is_not_clobbered_by_sti_base_class_fix
assert @kibbles.eaters << @chloe
assert_equal 3, @kibbles.eaters_foodstuffs.first.some_attribute
end
def test_creating_namespaced_relationship
assert @shamu.aquatic_pupils.empty?
@shamu.aquatic_pupils << @swimmy
assert_equal 1, @shamu.aquatic_pupils.length
@shamu.reload
assert_equal 1, @shamu.aquatic_pupils.length
end
def test_namespaced_polymorphic_collection
@shamu.aquatic_pupils << @swimmy
assert @shamu.aquatic_pupils.include?(@swimmy)
@shamu.reload
assert @shamu.aquatic_pupils.include?(@swimmy)
@shamu.aquatic_pupils << @spot
assert @shamu.dogs.include?(@spot)
assert @shamu.aquatic_pupils.include?(@swimmy)
assert_equal @swimmy, @shamu.aquatic_fish.first
assert_equal 10, @shamu.aquatic_fish.first.speed
end
def test_deleting_namespaced_relationship
@shamu.aquatic_pupils << @swimmy
@shamu.aquatic_pupils << @spot
@shamu.reload
@shamu.aquatic_pupils.delete @spot
assert !@shamu.dogs.include?(@spot)
assert !@shamu.aquatic_pupils.include?(@spot)
assert_equal 1, @shamu.aquatic_pupils.length
end
def test_unrenamed_parent_of_namespaced_child
@shamu.aquatic_pupils << @swimmy
assert_equal [@shamu], @swimmy.whales
end
def test_empty_double_collections
assert @puma.enemies.empty?
assert @froggy.protectors.empty?
assert @alice.enemies.empty?
assert @spot.protectors.empty?
assert @alice.beautiful_fight_relationships_as_enemy.empty?
assert @alice.beautiful_fight_relationships_as_protector.empty?
assert @alice.beautiful_fight_relationships.empty?
end
def test_double_collection_assignment
@alice.enemies << @spot
@alice.reload
@spot.reload
assert @spot.protectors.include?(@alice)
assert @alice.enemies.include?(@spot)
assert !@alice.protectors.include?(@alice)
assert_equal 1, @alice.beautiful_fight_relationships_as_protector.size
assert_equal 0, @alice.beautiful_fight_relationships_as_enemy.size
assert_equal 1, @alice.beautiful_fight_relationships.size
# self reference
assert_equal 1, @alice.enemies.length
@alice.enemies.push @alice
assert @alice.enemies.include?(@alice)
assert_equal 2, @alice.enemies.length
@alice.reload
assert_equal 2, @alice.beautiful_fight_relationships_as_protector.size
assert_equal 1, @alice.beautiful_fight_relationships_as_enemy.size
assert_equal 3, @alice.beautiful_fight_relationships.size
end
def test_double_collection_deletion
@alice.enemies << @spot
@alice.reload
assert @alice.enemies.include?(@spot)
@alice.enemies.delete(@spot)
assert !@alice.enemies.include?(@spot)
assert @alice.enemies.empty?
@alice.reload
assert !@alice.enemies.include?(@spot)
assert @alice.enemies.empty?
assert_equal 0, @alice.beautiful_fight_relationships.size
end
def test_double_collection_deletion_from_opposite_side
@alice.protectors << @puma
@alice.reload
assert @alice.protectors.include?(@puma)
@alice.protectors.delete(@puma)
assert !@alice.protectors.include?(@puma)
assert @alice.protectors.empty?
@alice.reload
assert !@alice.protectors.include?(@puma)
assert @alice.protectors.empty?
assert_equal 0, @alice.beautiful_fight_relationships.size
end
def test_individual_collections_created_for_double_relationship
assert @alice.dogs.empty?
@alice.enemies << @spot
assert @alice.enemies.include?(@spot)
assert !@alice.kittens.include?(@alice)
assert !@alice.dogs.include?(@spot)
@alice.reload
assert @alice.dogs.include?(@spot)
assert !WildBoar.find(@alice.id).dogs.include?(@spot) # make sure the parent type is checked
end
def test_individual_collections_created_for_double_relationship_from_opposite_side
assert @alice.wild_boars.empty?
@alice.protectors << @puma
assert @alice.protectors.include?(@puma)
assert !@alice.wild_boars.include?(@puma)
@alice.reload
assert @alice.wild_boars.include?(@puma)
assert !Dog.find(@alice.id).wild_boars.include?(@puma) # make sure the parent type is checked
end
def test_self_referential_individual_collections_created_for_double_relationship
@alice.enemies << @alice
@alice.reload
assert @alice.enemy_kittens.include?(@alice)
assert @alice.protector_kittens.include?(@alice)
assert @alice.kittens.include?(@alice)
assert_equal 2, @alice.kittens.size
@alice.enemies << (@chloe = Kitten.find_by_name('Chloe'))
@alice.reload
assert @alice.enemy_kittens.include?(@chloe)
assert !@alice.protector_kittens.include?(@chloe)
assert @alice.kittens.include?(@chloe)
assert_equal 3, @alice.kittens.size
end
def test_child_of_polymorphic_join_can_reach_parent
@alice.enemies << @spot
@alice.reload
assert @spot.protectors.include?(@alice)
end
def test_double_collection_deletion_from_child_polymorphic_join
@alice.enemies << @spot
@spot.protectors.delete(@alice)
assert !@spot.protectors.include?(@alice)
@alice.reload
assert !@alice.enemies.include?(@spot)
BeautifulFightRelationship.create(:protector_id => 2, :protector_type => "Dog", :enemy_id => @spot.id, :enemy_type => @spot.class.name)
@alice.enemies << @spot
@spot.protectors.delete(@alice)
assert !@spot.protectors.include?(@alice)
end
def test_hmp_passed_block_manipulates_proxy_class
assert_equal "result", @shamu.aquatic_pupils.blow
assert_raises(NoMethodError) { @kibbles.eaters.blow }
end
def test_collection_query_on_unsaved_record
assert Dog.new.enemies.empty?
assert Dog.new.foodstuffs_of_eaters.empty?
end
def test_double_invididual_collections_push
assert_equal [@chloe], (@spot.protector_kittens << @chloe)
@spot.reload
assert @spot.protectors.include?(@chloe)
assert @spot.protector_kittens.include?(@chloe)
assert !@spot.protector_dogs.include?(@chloe)
assert_equal [@froggy], (@spot.frogs << @froggy)
@spot.reload
assert @spot.enemies.include?(@froggy)
assert @spot.frogs.include?(@froggy)
assert !@spot.enemy_dogs.include?(@froggy)
end
def test_double_invididual_collections_delete
@spot.protectors << [@chloe, @puma]
@spot.reload
assert_equal [@chloe], @spot.protector_kittens.delete(@chloe)
assert @spot.protector_kittens.empty?
assert !@spot.protector_kittens.delete(@chloe)
@spot.reload
assert @spot.protector_kittens.empty?
assert @spot.wild_boars.include?(@puma)
end
def test_double_invididual_collections_clear
@spot.protectors << [@chloe, @puma, @alice]
@spot.reload
assert_equal [@chloe, @alice], @spot.protector_kittens.clear.sort_by(&:id)
assert @spot.protector_kittens.empty?
assert_equal 1, @spot.protectors.size
@spot.reload
assert @spot.protector_kittens.empty?
assert_equal 1, @spot.protectors.size
assert !@spot.protector_kittens.include?(@chloe)
assert !@spot.protectors.include?(@chloe)
assert !@spot.protector_kittens.include?(@alice)
assert !@spot.protectors.include?(@alice)
end
end