sort_by_str 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.document +5 -0
- data/.gitignore +5 -0
- data/LICENSE +13 -0
- data/README.rdoc +35 -0
- data/Rakefile +56 -0
- data/VERSION +1 -0
- data/lib/sort_by_str.rb +64 -0
- data/test/test_helper.rb +11 -0
- data/test/test_sort_by_str.rb +55 -0
- metadata +73 -0
data/.document
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
Copyright (c) 2010 The New York Times Company
|
2
|
+
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
you may not use this library except in compliance with the License.
|
5
|
+
You may obtain a copy of the License at
|
6
|
+
|
7
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
|
9
|
+
Unless required by applicable law or agreed to in writing, software
|
10
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
See the License for the specific language governing permissions and
|
13
|
+
limitations under the License.
|
data/README.rdoc
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
= sort_by_str
|
2
|
+
|
3
|
+
SQL-like sorts on your Enumerables.
|
4
|
+
|
5
|
+
== Getting Started
|
6
|
+
|
7
|
+
gem install sort_by_str
|
8
|
+
require 'sort_by_str'
|
9
|
+
|
10
|
+
== Usage
|
11
|
+
|
12
|
+
a = Date.parse('2010-08-22')
|
13
|
+
b = Date.parse('2010-08-23)
|
14
|
+
|
15
|
+
[a,b].sort_by_str('year ASC, day DESC')
|
16
|
+
=> [b,a]
|
17
|
+
|
18
|
+
Call sort_by_str on your Enumerable with a SQL-style sort expression containing a list of fields.
|
19
|
+
|
20
|
+
A basic expression might look like like
|
21
|
+
'year, month, day'.
|
22
|
+
|
23
|
+
Optional ASC (ascending) or DESC (descending) modifiers can also be used:
|
24
|
+
'year ASC, day DESC'
|
25
|
+
|
26
|
+
<tt>send</tt> is used to extract values for comparison, so any valid method name
|
27
|
+
can be used in the expression.
|
28
|
+
|
29
|
+
== Author
|
30
|
+
|
31
|
+
Ben Koski, bkoski@nytimes.com
|
32
|
+
|
33
|
+
== Copyright
|
34
|
+
|
35
|
+
Copyright (c) 2010 The New York Times Company. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "sort_by_str"
|
8
|
+
gem.summary = %Q{SQL-like sorts on your Enumerables}
|
9
|
+
gem.description = %Q{SQL-like sorts on your Enumerables}
|
10
|
+
gem.email = "bkoski@nytimes.com"
|
11
|
+
gem.homepage = "http://github.com/nytimes/sort_by_str"
|
12
|
+
gem.authors = ["Ben Koski"]
|
13
|
+
gem.add_development_dependency "thoughtbot-shoulda"
|
14
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
15
|
+
end
|
16
|
+
rescue LoadError
|
17
|
+
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
|
18
|
+
end
|
19
|
+
|
20
|
+
require 'rake/testtask'
|
21
|
+
Rake::TestTask.new(:test) do |test|
|
22
|
+
test.libs << 'lib' << 'test'
|
23
|
+
test.pattern = 'test/**/*_test.rb'
|
24
|
+
test.verbose = true
|
25
|
+
end
|
26
|
+
|
27
|
+
begin
|
28
|
+
require 'rcov/rcovtask'
|
29
|
+
Rcov::RcovTask.new do |test|
|
30
|
+
test.libs << 'test'
|
31
|
+
test.pattern = 'test/**/*_test.rb'
|
32
|
+
test.verbose = true
|
33
|
+
end
|
34
|
+
rescue LoadError
|
35
|
+
task :rcov do
|
36
|
+
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
task :test => :check_dependencies
|
41
|
+
|
42
|
+
task :default => :test
|
43
|
+
|
44
|
+
require 'rake/rdoctask'
|
45
|
+
Rake::RDocTask.new do |rdoc|
|
46
|
+
if File.exist?('VERSION')
|
47
|
+
version = File.read('VERSION')
|
48
|
+
else
|
49
|
+
version = ""
|
50
|
+
end
|
51
|
+
|
52
|
+
rdoc.rdoc_dir = 'rdoc'
|
53
|
+
rdoc.title = "sort_by_str #{version}"
|
54
|
+
rdoc.rdoc_files.include('README*')
|
55
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
56
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.5.0
|
data/lib/sort_by_str.rb
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
module Enumerable
|
2
|
+
|
3
|
+
# Similar to sort_by, sort_by_str accepts a string containing a SQL-style sort expression.
|
4
|
+
#
|
5
|
+
# The simplest sort expression is a comma separated list of fields: <tt>'month,day,year'</tt>
|
6
|
+
#
|
7
|
+
# But can also include optional ASC (ascending) or DESC (descending) order modifiers.
|
8
|
+
# For example: <tt>'year DESC, month ASC, day ASC'</tt>
|
9
|
+
# If an order modifier is omitted for a field, an ASC sort is assumed.
|
10
|
+
#
|
11
|
+
# Field values are checked for comparision using a <tt>send</tt>, so any method name can be used.
|
12
|
+
def sort_by_str str
|
13
|
+
sort_parts = str.split(',').collect { |p| p.split(' ') }
|
14
|
+
|
15
|
+
# double-check structure of parsed data
|
16
|
+
raise ArgumentError, "'#{str}' doesn't appear correctly formatted." if sort_parts.empty?
|
17
|
+
sort_parts.each { |p| raise ArgumentError, "'#{str}' doesn't appear correctly formatted." if p.length > 2 }
|
18
|
+
|
19
|
+
# split into list of fields and sort directions for those fields
|
20
|
+
fields = sort_parts.collect { |p| p.first }
|
21
|
+
sort_directions = sort_parts.collect { |p| p.last.upcase == 'DESC' ? :desc : :asc } # if unspecified assumed to be an ASC sort
|
22
|
+
|
23
|
+
# From here we follow pattern of Schwartzian Transform used in sort_by (http://bit.ly/aPEsNO).
|
24
|
+
# This means that rather than doing a sort! directly on self, we cache values to be sorted into an array.
|
25
|
+
# So for example, given:
|
26
|
+
# a = Date.today
|
27
|
+
# b = Date.today + 1
|
28
|
+
# [a,b].sort_by_str('year ASC, day DESC')
|
29
|
+
#
|
30
|
+
# we transform to
|
31
|
+
# [[a, 2010, 22],[b,2010,23]]
|
32
|
+
# and then proceed with sort.
|
33
|
+
# Following sort, we collect list of first elements to produce final, sorted array.
|
34
|
+
# This prevents sort attrs from being called multiple times during sort
|
35
|
+
# which could be problematic if sort attr is an expensive calcuation.
|
36
|
+
|
37
|
+
# 1. collect data into intermediate arrays, data element first
|
38
|
+
sort_data = collect do |element|
|
39
|
+
data = [element]
|
40
|
+
fields.each { |f| data << element.send(f) }
|
41
|
+
data
|
42
|
+
end
|
43
|
+
|
44
|
+
# 2. actually sort the data -- a,b are limited to a[1..-1] so as to exclude first [data] element
|
45
|
+
sort_data.sort! do |a,b|
|
46
|
+
cmp = 0
|
47
|
+
sort_directions.zip(a[1..-1], b[1..-1]) do |field_data|
|
48
|
+
direction, a_val, b_val = field_data
|
49
|
+
if direction == :desc
|
50
|
+
cmp = b_val <=> a_val
|
51
|
+
else
|
52
|
+
cmp = a_val <=> b_val
|
53
|
+
end
|
54
|
+
break if cmp != 0
|
55
|
+
end
|
56
|
+
|
57
|
+
cmp
|
58
|
+
end
|
59
|
+
|
60
|
+
# 3. now that data is sorted, reduce back to list of data elements
|
61
|
+
sort_data.collect { |element| element.first }
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'test/unit'
|
3
|
+
require 'shoulda'
|
4
|
+
require 'ostruct'
|
5
|
+
|
6
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
7
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
8
|
+
require 'sort_by_str'
|
9
|
+
|
10
|
+
class Test::Unit::TestCase
|
11
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class SortByStrTest < Test::Unit::TestCase
|
4
|
+
|
5
|
+
context "with valid sort expression" do
|
6
|
+
|
7
|
+
setup do
|
8
|
+
@red_10 = OpenStruct.new(:color => 'red', :size => 10)
|
9
|
+
@blue_10 = OpenStruct.new(:color => 'blue', :size => 10)
|
10
|
+
@red_15 = OpenStruct.new(:color => 'red', :size => 15)
|
11
|
+
|
12
|
+
@all_data = [@red_10, @blue_10, @red_15]
|
13
|
+
end
|
14
|
+
|
15
|
+
should "sort correctly on a single-field sort" do
|
16
|
+
assert_equal [@blue_10, @red_15], [@red_15, @blue_10].sort_by_str('size')
|
17
|
+
end
|
18
|
+
|
19
|
+
should "sort correctly on a multi-field sort" do
|
20
|
+
assert_equal [@blue_10, @red_10, @red_15], @all_data.sort_by_str('size, color')
|
21
|
+
end
|
22
|
+
|
23
|
+
should "respect DESC modifier" do
|
24
|
+
assert_equal [@red_15, @blue_10, @red_10], @all_data.sort_by_str('size DESC, color')
|
25
|
+
end
|
26
|
+
|
27
|
+
should "respect ASC modifier" do
|
28
|
+
assert_equal [@blue_10, @red_10, @red_15], @all_data.sort_by_str('size ASC, color')
|
29
|
+
end
|
30
|
+
|
31
|
+
should "sort correctly even if extra spaces are supplied" do
|
32
|
+
assert_equal [@blue_10, @red_10, @red_15], @all_data.sort_by_str('size ASC , color ')
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
context "with invalid sort expression" do
|
38
|
+
setup do
|
39
|
+
@data = [OpenStruct.new(:size => 10), OpenStruct.new(:size => 15)]
|
40
|
+
end
|
41
|
+
|
42
|
+
should "raise an ArgumentError if too many tokens are supplied for a field" do
|
43
|
+
assert_raises(ArgumentError) { @data.sort('size ASC DESC')}
|
44
|
+
end
|
45
|
+
|
46
|
+
should "raise an ArgumentError if no fields are supplied" do
|
47
|
+
assert_raises(ArgumentError) { @data.sort('') }
|
48
|
+
end
|
49
|
+
|
50
|
+
should "raise an ArgumentError if no tokens are supplied for a field" do
|
51
|
+
assert_raises(ArgumentError) { @data.sort('size ASC, ,size DESC')}
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
metadata
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sort_by_str
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.5.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ben Koski
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2010-08-24 00:00:00 -04:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: thoughtbot-shoulda
|
17
|
+
type: :development
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: "0"
|
24
|
+
version:
|
25
|
+
description: SQL-like sorts on your Enumerables
|
26
|
+
email: bkoski@nytimes.com
|
27
|
+
executables: []
|
28
|
+
|
29
|
+
extensions: []
|
30
|
+
|
31
|
+
extra_rdoc_files:
|
32
|
+
- LICENSE
|
33
|
+
- README.rdoc
|
34
|
+
files:
|
35
|
+
- .document
|
36
|
+
- .gitignore
|
37
|
+
- LICENSE
|
38
|
+
- README.rdoc
|
39
|
+
- Rakefile
|
40
|
+
- VERSION
|
41
|
+
- lib/sort_by_str.rb
|
42
|
+
- test/test_helper.rb
|
43
|
+
has_rdoc: true
|
44
|
+
homepage: http://github.com/nytimes/sort_by_str
|
45
|
+
licenses: []
|
46
|
+
|
47
|
+
post_install_message:
|
48
|
+
rdoc_options:
|
49
|
+
- --charset=UTF-8
|
50
|
+
require_paths:
|
51
|
+
- lib
|
52
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: "0"
|
57
|
+
version:
|
58
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: "0"
|
63
|
+
version:
|
64
|
+
requirements: []
|
65
|
+
|
66
|
+
rubyforge_project:
|
67
|
+
rubygems_version: 1.3.5
|
68
|
+
signing_key:
|
69
|
+
specification_version: 3
|
70
|
+
summary: SQL-like sorts on your Enumerables
|
71
|
+
test_files:
|
72
|
+
- test/test_helper.rb
|
73
|
+
- test/test_sort_by_str.rb
|