namo.rb 0.31.7

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/Rakefile ADDED
@@ -0,0 +1,42 @@
1
+ # Rakefile
2
+
3
+ require 'rake/testtask'
4
+
5
+ Rake::TestTask.new(:test) do |t|
6
+ t.test_files = FileList['test/**/*_test.rb']
7
+ end
8
+
9
+ namespace :docs do
10
+ SOURCE_DOCS = %w{COMPARISON EXAMPLES README ROADMAP}
11
+
12
+ desc "Strip syntax highlighting from code blocks for printing"
13
+ task :md4print do
14
+ SOURCE_DOCS.each do |name|
15
+ sh "script/md4print #{name}.md"
16
+ sh "mv #{name}.print.md docs/"
17
+ end
18
+ end
19
+
20
+ desc "Render print-ready markdown to PDF"
21
+ task :md2pdf => :md4print do
22
+ Dir.glob('docs/*.print.md').each do |f|
23
+ pdf = f.sub(/\.md$/, '.pdf')
24
+ sh "pandoc #{f} --pdf-engine=xelatex --include-in-header script/print.preamble.tex -V geometry:margin=1in -V mainfont=Charter -V monofont=Menlo -o #{pdf}"
25
+ end
26
+ end
27
+
28
+ desc "Remove intermediate .print.md files"
29
+ task :clean do
30
+ rm_f Dir.glob('docs/*.print.md')
31
+ end
32
+
33
+ desc "Remove all generated docs (intermediates and PDFs)"
34
+ task :clobber => :clean do
35
+ rm_f Dir.glob('docs/*.print.pdf')
36
+ end
37
+
38
+ desc "Regenerate all derived docs"
39
+ task :gen => [:md2pdf, :clean]
40
+ end
41
+
42
+ task default: :test
@@ -0,0 +1,130 @@
1
+ # Namo/Collection.rb
2
+ # Namo::Collection
3
+
4
+ class Namo
5
+ class Collection < Namo
6
+ attr_reader :members
7
+
8
+ def <<(*constituents)
9
+ constituents.flatten.each do |constituent|
10
+ case constituent
11
+ when Namo then add_member(constituent)
12
+ when Module then attach(constituent)
13
+ when Hash, Row
14
+ raise ArgumentError, "a Collection's rows come from its members; add a member (a named Namo), not a loose row"
15
+ else raise TypeError, "can't append #{constituent.class} to a Collection"
16
+ end
17
+ end
18
+ @data = detail.data
19
+ self
20
+ end
21
+
22
+ def find(name)
23
+ @members.find{|member| member.name == name} unless name.nil?
24
+ end
25
+
26
+ def summary(dimension = nil, by: :member, reducer: :sum, &block)
27
+ raise ArgumentError, "summary needs a dimension or a block" unless dimension || block
28
+ rows = (
29
+ if block
30
+ @members.map{|member| block.call(member).merge(by => member.name)}
31
+ else
32
+ @members.map{|member| {by => member.name, dimension => member.values(dimension).send(reducer)}}
33
+ end
34
+ )
35
+ Namo.new(rows)
36
+ end
37
+
38
+ def detail(positional_by = nil, by: :member)
39
+ by = positional_by || by
40
+ rows = (
41
+ @members.flat_map do |member|
42
+ member.data.map{|row| row.key?(by) ? row : row.merge(by => member.name)}
43
+ end
44
+ )
45
+ Namo.new(rows, formulae: member_formulae)
46
+ end
47
+
48
+ def as_summary(dimension = nil, by: :member, reducer: :sum, &block)
49
+ @data = summary(dimension, by: by, reducer: reducer, &block).data
50
+ self
51
+ end
52
+
53
+ def as_detail(positional_by = nil, by: :member)
54
+ by = positional_by || by
55
+ view = detail(by)
56
+ @data = view.data
57
+ @formulae = @formulae.merge(view.formulae)
58
+ self
59
+ end
60
+
61
+ # The members are the substance, so they are what is rendered. A list of names
62
+ # and a row count described the derived view and called it the object, and the
63
+ # list was unbounded besides: 2,641 members emitted 22,704 characters on one
64
+ # line, which is the failure INSPECTED_ROWS was introduced to end.
65
+ #
66
+ # That budget is now spread across the members shown rather than spent on one
67
+ # member's rows: one member renders ten of its rows, ten members render one
68
+ # apiece, five render two.
69
+ def inspect
70
+ "#<#{self.class}#{inspected_name} #{inspected_members} #{@data.length} rows#{inspected_derived}>"
71
+ end
72
+
73
+ protected
74
+
75
+ # A row operation on a Collection yields a Namo: 0.18.0 has it read the detail
76
+ # view and behave as the detail, and the detail is a Namo. Built as a
77
+ # Collection it would be one with no members, answering summary with an empty
78
+ # Namo rather than saying it cannot.
79
+ def return_class
80
+ Namo
81
+ end
82
+
83
+ private
84
+
85
+ # The members' formulae, folded in member order so that a later member wins a
86
+ # name collision, as a later member wins a collision of names in <<. The
87
+ # detail view is the members' rows, so it must be able to answer what the
88
+ # members can answer of them; group_by hands every member the same formulae,
89
+ # so only an assembled Collection can have members which disagree.
90
+ def member_formulae
91
+ @members.map(&:formulae).reduce(Formulae.new){|merged, formulae| merged.merge(formulae)}
92
+ end
93
+
94
+ def inspected_members
95
+ return '[]' if @members.empty?
96
+ shown = [@members.length, INSPECTED_ROWS].min
97
+ each = [INSPECTED_ROWS / shown, 1].max
98
+ rendered = @members.first(shown).map{|member| " #{member.name.inspect} => #{inspected_member(member, each)}"}.join(",\n")
99
+ rendered += "\n ... #{@members.length - shown} more members" if @members.length > shown
100
+ "[\n#{rendered}\n]"
101
+ end
102
+
103
+ # A member showing a single row is rendered on one line: three lines of brackets
104
+ # to carry one row is the shape a Collection of many members lands in, and it
105
+ # cost more than the rows did. On what is rendered rather than on the limit,
106
+ # so a member holding one row of a larger allowance reads the same way.
107
+ def inspected_member(member, limit)
108
+ rows, remainder, unshown = member.inspected_data(limit)
109
+ elided = remainder > 0
110
+ suffix = inspected_derived_names(unshown)
111
+ if rows.length == 1
112
+ "[#{rows.join}#{elided ? ", ... #{remainder} more rows" : ''}]#{suffix}"
113
+ else
114
+ body = rows.map{|row| " #{row}"}.join(",\n")
115
+ body += "\n ... #{remainder} more rows" if elided
116
+ "[\n#{body}\n ]#{suffix}"
117
+ end
118
+ end
119
+
120
+ def initialize(positional_data = nil, data: [], formulae: {}, name: nil)
121
+ @members = []
122
+ super
123
+ end
124
+
125
+ def add_member(member)
126
+ @members.reject!{|existing| existing.name == member.name} unless member.name.nil?
127
+ @members << member
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,83 @@
1
+ # Namo/Enumerable.rb
2
+ # Namo::Enumerable
3
+
4
+ class Namo
5
+ module Enumerable
6
+ include ::Enumerable
7
+
8
+ def each(&block)
9
+ return enum_for(:each) unless block_given?
10
+ @data.each{|row_data| block.call(Row.new(row_data, @formulae, self))}
11
+ end
12
+
13
+ def select(&block)
14
+ return_class.new(@data.select{|row| block.call(Row.new(row, @formulae, self))}, formulae: @formulae.dup)
15
+ end
16
+ alias_method :filter, :select
17
+ alias_method :find_all, :select
18
+
19
+ def reject(&block)
20
+ return_class.new(@data.reject{|row| block.call(Row.new(row, @formulae, self))}, formulae: @formulae.dup)
21
+ end
22
+
23
+ def sort_by(&block)
24
+ return_class.new(@data.sort_by{|row| block.call(Row.new(row, @formulae, self))}, formulae: @formulae.dup)
25
+ end
26
+
27
+ def first(n = nil)
28
+ if n
29
+ return_class.new(@data.first(n), formulae: @formulae.dup)
30
+ else
31
+ @data.first ? Row.new(@data.first, @formulae, self) : nil
32
+ end
33
+ end
34
+
35
+ def last(n = nil)
36
+ if n
37
+ return_class.new(@data.last(n), formulae: @formulae.dup)
38
+ else
39
+ @data.last ? Row.new(@data.last, @formulae, self) : nil
40
+ end
41
+ end
42
+
43
+ def take(n)
44
+ return_class.new(@data.take(n), formulae: @formulae.dup)
45
+ end
46
+
47
+ def drop(n)
48
+ return_class.new(@data.drop(n), formulae: @formulae.dup)
49
+ end
50
+
51
+ def take_while(&block)
52
+ return_class.new(@data.take_while{|row| block.call(Row.new(row, @formulae, self))}, formulae: @formulae.dup)
53
+ end
54
+
55
+ def drop_while(&block)
56
+ return_class.new(@data.drop_while{|row| block.call(Row.new(row, @formulae, self))}, formulae: @formulae.dup)
57
+ end
58
+
59
+ def uniq(&block)
60
+ rows = block ? @data.uniq{|row| block.call(Row.new(row, @formulae, self))} : @data.uniq
61
+ return_class.new(rows, formulae: @formulae.dup)
62
+ end
63
+
64
+ def partition(&block)
65
+ matches, non_matches = @data.partition{|row| block.call(Row.new(row, @formulae, self))}
66
+ [
67
+ return_class.new(matches, formulae: @formulae.dup),
68
+ return_class.new(non_matches, formulae: @formulae.dup),
69
+ ]
70
+ end
71
+
72
+ def group_by(dimension)
73
+ collection = Collection.new
74
+ source = derived_dimensions.include?(dimension) ? self[*data_dimensions, dimension] : self
75
+ members = (
76
+ source.data.group_by{|row_data| row_data[dimension]}.map do |value, rows|
77
+ return_class.new(rows, formulae: source.formulae.dup, name: value)
78
+ end
79
+ )
80
+ collection << members
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,137 @@
1
+ # Namo/Formulae.rb
2
+ # Namo::Formulae
3
+
4
+ class Namo
5
+ class Formulae
6
+ include Enumerable
7
+
8
+ def [](name)
9
+ @store[name]
10
+ end
11
+
12
+ def []=(name, callable)
13
+ @store[name] = callable
14
+ end
15
+
16
+ def attach(modul)
17
+ raise_unless_formulary(modul)
18
+ modul.public_instance_methods(false).each{|name| bind(name, modul)}
19
+ self
20
+ end
21
+ alias_method :<<, :attach
22
+
23
+ def detach(constituent)
24
+ case constituent
25
+ when Symbol
26
+ @store.delete(constituent)
27
+ when Module
28
+ raise_unless_formulary(constituent)
29
+ constituent.public_instance_methods(false).each{|name| @store.delete(name)}
30
+ else
31
+ raise TypeError, "can't detach #{constituent.class} from a Formulae; expected a Symbol or a Module (formulary)"
32
+ end
33
+ self
34
+ end
35
+
36
+ def derive(name, row, namo, *arguments)
37
+ formula = self[name]
38
+ if collection_scoped?(name)
39
+ raise_unless_namo_context(name, namo)
40
+ formula.call(row, namo, *arguments)
41
+ else
42
+ formula.call(row)
43
+ end
44
+ end
45
+
46
+ def required_parameter_count(name)
47
+ formula = self[name]
48
+ formula.arity >= 0 ? formula.arity : -formula.arity - 1
49
+ end
50
+
51
+ def keys
52
+ @store.keys
53
+ end
54
+
55
+ def key?(name)
56
+ @store.key?(name)
57
+ end
58
+
59
+ def empty?
60
+ @store.empty?
61
+ end
62
+
63
+ def each(&block)
64
+ return enum_for(:each) unless block_given?
65
+ @store.each(&block)
66
+ end
67
+
68
+ def delete(name)
69
+ @store.delete(name)
70
+ end
71
+
72
+ def merge(other)
73
+ self.class.new(@store.merge(other.to_h))
74
+ end
75
+
76
+ def reject(&block)
77
+ self.class.new(@store.reject(&block))
78
+ end
79
+
80
+ def dup
81
+ self.class.new(@store.dup)
82
+ end
83
+
84
+ def to_h
85
+ @store.dup
86
+ end
87
+
88
+ def ==(other)
89
+ other.is_a?(Formulae) && keys.sort == other.keys.sort
90
+ end
91
+
92
+ def eql?(other)
93
+ self == other
94
+ end
95
+
96
+ def hash
97
+ keys.sort.hash
98
+ end
99
+
100
+ def inspect
101
+ "#<#{self.class} #{@store.keys.inspect}>"
102
+ end
103
+
104
+ private
105
+
106
+ def initialize(store = {})
107
+ @store = store
108
+ end
109
+
110
+ def host
111
+ @host ||= Object.new
112
+ end
113
+
114
+ def bind(name, modul)
115
+ raise_unless_formulary(modul)
116
+ host.extend(modul)
117
+ self[name] = modul.instance_method(name).bind(host)
118
+ self
119
+ end
120
+
121
+ def raise_unless_formulary(modul)
122
+ unless modul.include?(Namo::Formulary)
123
+ raise ArgumentError, "not a Namo::Formulary: #{modul}"
124
+ end
125
+ end
126
+
127
+ def collection_scoped?(name)
128
+ required_parameter_count(name) >= 2
129
+ end
130
+
131
+ def raise_unless_namo_context(name, namo)
132
+ unless namo
133
+ raise ArgumentError, "collection-scoped formula #{name.inspect} requires a Namo context, but this Row has none"
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,6 @@
1
+ # Namo/Formulary.rb
2
+ # Namo::Formulary
3
+
4
+ class Namo
5
+ module Formulary; end
6
+ end
@@ -0,0 +1,14 @@
1
+ # Namo/NegatedDimension.rb
2
+ # Namo::NegatedDimension
3
+
4
+ class Namo
5
+ class NegatedDimension
6
+ attr_reader :name
7
+
8
+ private
9
+
10
+ def initialize(name)
11
+ @name = name
12
+ end
13
+ end
14
+ end
data/lib/Namo/Row.rb ADDED
@@ -0,0 +1,93 @@
1
+ # Namo/Row.rb
2
+ # Namo::Row
3
+
4
+ class Namo
5
+ class Row
6
+ def [](name, *arguments)
7
+ raise_unless_expected_arguments(name, arguments)
8
+ if @formulae.key?(name)
9
+ @formulae.derive(name, self, @namo, *arguments)
10
+ else
11
+ @row[name]
12
+ end
13
+ end
14
+
15
+ def ==(other)
16
+ other.is_a?(Row) && @row == other.to_h
17
+ end
18
+
19
+ def eql?(other)
20
+ other.is_a?(Row) && @row.eql?(other.to_h)
21
+ end
22
+
23
+ def hash
24
+ @row.hash
25
+ end
26
+
27
+ def match?(selections)
28
+ selections.all? do |dimension, coordinate|
29
+ case coordinate
30
+ when Array, Range
31
+ coordinate.include?(self[dimension])
32
+ when Proc
33
+ coordinate.call(self[dimension])
34
+ when Regexp
35
+ coordinate.match?(self[dimension].to_s)
36
+ else
37
+ self[dimension] == coordinate
38
+ end
39
+ end
40
+ end
41
+
42
+ def to_h
43
+ @row
44
+ end
45
+
46
+ def inspect
47
+ "#<#{self.class} #{inspected_row}#{inspected_derived}>"
48
+ end
49
+
50
+ private
51
+
52
+ def inspected_row
53
+ @row.merge(inspected_derivations).inspect
54
+ end
55
+
56
+ # As Namo#inspected_derivations: the value where there is one to show, and
57
+ # the derived list to name it either way.
58
+ def inspected_derivations
59
+ @formulae.keys.each_with_object({}) do |dimension, derived|
60
+ next if @formulae.required_parameter_count(dimension) > 2
61
+ derived[dimension] = self[dimension]
62
+ rescue StandardError
63
+ next
64
+ end
65
+ end
66
+
67
+ def inspected_derived
68
+ @formulae.keys.empty? ? '' : " derived: #{@formulae.keys.inspect}"
69
+ end
70
+
71
+ def initialize(row, formulae, namo = nil)
72
+ @row = row
73
+ @formulae = formulae
74
+ @namo = namo
75
+ end
76
+
77
+ def expected_argument_counts(name)
78
+ return [0, 0] unless @formulae.key?(name)
79
+ count = @formulae.required_parameter_count(name)
80
+ return [0, 0] unless count >= 2
81
+ minimum = count - 2
82
+ maximum = @formulae[name].arity >= 0 ? minimum : nil
83
+ [minimum, maximum]
84
+ end
85
+
86
+ def raise_unless_expected_arguments(name, arguments)
87
+ minimum, maximum = expected_argument_counts(name)
88
+ return if arguments.length >= minimum && (maximum.nil? || arguments.length <= maximum)
89
+ expected = maximum.nil? ? "#{minimum}+" : minimum.to_s
90
+ raise ArgumentError, "wrong number of arguments for #{name.inspect} (given #{arguments.length}, expected #{expected})"
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,6 @@
1
+ # Namo/VERSION.rb
2
+ # Namo::VERSION
3
+
4
+ class Namo
5
+ VERSION = '0.31.7'
6
+ end
data/lib/Symbol.rb ADDED
@@ -0,0 +1,8 @@
1
+ # Symbol.rb
2
+ # Symbol#-@
3
+
4
+ class Symbol
5
+ def -@
6
+ Namo::NegatedDimension.new(self)
7
+ end
8
+ end