tagformula 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 3f6dd975b3faff6b1ecf509af6d3099c4a435043
4
+ data.tar.gz: a97caeec73a791266dbc8fe103dd0b4684ed2dc5
5
+ SHA512:
6
+ metadata.gz: c47f0b580bf0961ea0703522b5e1024c6d1ab35ef0e3c36269f2edc808abbf965a55596a5cf8df521b41cb7c86ee88d2d892f43d24e03c6576c030d18a130ae5
7
+ data.tar.gz: 8c7ccd4638de10725bdd450595cd99bb79f643dc0cf6d15519022282ec0d2266231fb7a4394914dea6e7dce61d5de2d59e39ea97b68b8106f70a09b2c1419674
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in tagformula.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,25 @@
1
+ Copyright (c) 2014 Geoff Youngs
2
+
3
+
4
+
5
+
6
+ MIT License
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining
9
+ a copy of this software and associated documentation files (the
10
+ "Software"), to deal in the Software without restriction, including
11
+ without limitation the rights to use, copy, modify, merge, publish,
12
+ distribute, sublicense, and/or sell copies of the Software, and to
13
+ permit persons to whom the Software is furnished to do so, subject to
14
+ the following conditions:
15
+
16
+ The above copyright notice and this permission notice shall be
17
+ included in all copies or substantial portions of the Software.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
20
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
21
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
22
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
23
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
24
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
25
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,60 @@
1
+ # Tagformula::Parser
2
+
3
+ Match tag formulas with simple boolean operations against lists of strings. e.g. the formula "foo & ! bar" would match ['foo'], but not ['foo', 'bar']
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'tagformula'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install tagformula
18
+
19
+ ## Usage
20
+
21
+ Tagformula::Parser matches arrays of strings against formulas which require the presence or absence of strings in the array. Conditions can be nested (in brackets), or negated (with a !) and can be 'and' ('&') or 'or' ('|') conditions.
22
+
23
+
24
+ require 'tagformula/parser'
25
+
26
+ formula = Tagformula::Parser.parse("alpha & beta")
27
+
28
+ formula.matches?(%w[alpha beta]) # => true
29
+ formula.matches?(%w[alpha gamma]) # => false
30
+
31
+ formula = Tagformula::Parser.parse("alpha & (beta | gamma)")
32
+
33
+ formula.matches?(%w[alpha beta]) # => true
34
+ formula.matches?(%w[alpha gamma]) # => true
35
+
36
+ formula = Tagformula::Parser.parse("alpha & !beta")
37
+
38
+ formula.matches?(%w[alpha beta]) # => false
39
+ formula.matches?(%w[alpha gamma]) # => true
40
+
41
+ Operators:
42
+
43
+ expr & expr requires both expressions to be present
44
+ expr | expr requires either expression to be present
45
+ expr1 & ! expr2 requires the expr1 to be present and expr2 to be absent.
46
+
47
+ Special words:
48
+
49
+ "true" and "yes" will always match as true.
50
+
51
+ "false" and "no" will always match as false.
52
+
53
+
54
+ ## Contributing
55
+
56
+ 1. Fork it ( http://github.com/geoffyoungs/tagformula/fork )
57
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
58
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
59
+ 4. Push to the branch (`git push origin my-new-feature`)
60
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
data/lib/tagformula.rb ADDED
@@ -0,0 +1,5 @@
1
+ require "tagformula/version"
2
+
3
+ module Tagformula
4
+ # Your code goes here...
5
+ end
@@ -0,0 +1,138 @@
1
+ require 'tagformula'
2
+ require 'strscan'
3
+
4
+ module Tagformula::Parser
5
+ class Group
6
+ def initialize()
7
+ @contents = []
8
+ end
9
+ def empty?
10
+ @contents.empty?
11
+ end
12
+ def last_entry
13
+ @contents.last
14
+ end
15
+ def push_condition(what)
16
+ if [:and,:or].include?(what) && (! @contents.last.is_a?(Symbol))
17
+ @contents << what
18
+ end
19
+ end
20
+ def push_entry(entry)
21
+ case entry
22
+ when Tag, Group
23
+ @contents << entry
24
+ end
25
+ end
26
+ def required_tags
27
+ @contents.map do |content|
28
+ if content.is_a?(Tag) && content.needed
29
+ content.tagname
30
+ elsif content.is_a?(Group)
31
+ content.required_tags
32
+ else
33
+ nil
34
+ end
35
+ end.flatten.compact
36
+ end
37
+ def matches?(taglist)
38
+ condition, op, i = nil, nil, 0
39
+ while @contents[i]
40
+ case @contents[i]
41
+ when Symbol
42
+ op = @contents[i]
43
+ when Group, Tag
44
+ res = @contents[i].matches?(taglist)
45
+ raise "Epic fail - res is neither true nor false" unless [true,false].include?(res)
46
+ case op
47
+ when :and
48
+ condition &&= res
49
+ when :or
50
+ condition ||= res
51
+ when nil
52
+ if i.zero?
53
+ condition = res
54
+ else
55
+ raise "Epic fail - apparently no operator between tags. How does this even happen?"
56
+ end
57
+ end
58
+ else
59
+ raise "Epic fail - invalid group entry" # epic fail
60
+ end
61
+ i += 1
62
+ end
63
+ condition
64
+ end
65
+ end
66
+ class Tag
67
+ attr_reader :tagname, :needed
68
+ def initialize(tagname, needed)
69
+ @tagname, @needed = tagname, needed
70
+ end
71
+ def matches?(taglist)
72
+ case @tagname
73
+ when 'true', 'false', 'yes', 'no'
74
+ @needed
75
+ else
76
+ taglist.include?(@tagname) ? @needed : ! @needed
77
+ end
78
+ end
79
+ end
80
+ module_function
81
+ def parse(str)
82
+ groups = [Group.new]
83
+ scanner = StringScanner.new(str)
84
+ until scanner.eos?
85
+ last = groups.last.last_entry
86
+
87
+ if scanner.scan(/\(/)
88
+ raise SyntaxError, "Unexpected tag at #{scanner.pos} - #{scanner.peek(scanner.rest_size)}" unless last.nil? or last.is_a?(Symbol)
89
+ push_group(groups)
90
+ elsif scanner.scan(/\)/)
91
+ raise SyntaxError, "Unexpected group end at #{scanner.pos} - #{scanner.peek(scanner.rest_size)}" if last.nil? or last.is_a?(Symbol)
92
+ grp = pop_group(groups)
93
+ raise SyntaxError, "Empty group at offset #{scanner.pos} - #{scanner.peek(scanner.rest_size)}" if grp.empty?
94
+ raise SyntaxError, "Unmatched braces at offset #{scanner.pos} - #{scanner.peek(scanner.rest_size)}" if groups.empty?
95
+ elsif scanner.scan(/([-0-9a-zA-Z.]+)/)
96
+ raise SyntaxError, "Unexpected tag at #{scanner.pos} - #{scanner.peek(scanner.rest_size)}" unless last.nil? or last.is_a?(Symbol)
97
+ push_tag(groups, scanner[1])
98
+ elsif scanner.scan(/[!~]\s*([-0-9a-zA-Z.]+)/)
99
+ raise SyntaxError, "Unexpected tag at #{scanner.pos} - #{scanner.peek(scanner.rest_size)}" unless last.nil? or last.is_a?(Symbol)
100
+ push_not_tag(groups, scanner[1])
101
+ elsif scanner.scan(/[,|]/)
102
+ raise SyntaxError, "Unexpected operator at #{scanner.pos} - #{scanner.peek(scanner.rest_size)}" if last.is_a?(Symbol)
103
+ push_or(groups)
104
+ elsif scanner.scan(/[&+]/)
105
+ raise SyntaxError, "Unexpected operator at #{scanner.pos} - #{scanner.peek(scanner.rest_size)}" if last.is_a?(Symbol)
106
+ push_and(groups)
107
+ elsif scanner.scan(/\s+/)
108
+ else
109
+ raise SyntaxError, "Error at offset #{scanner.pos} - #{scanner.peek(scanner.rest_size)}"
110
+ end
111
+ end
112
+ raise SyntaxError, "Unmatched braces at offset #{scanner.pos} - #{scanner.peek(scanner.rest_size)}" if groups.size != 1
113
+ groups[0]
114
+ end
115
+ class << self
116
+ private
117
+ def push_group(groups)
118
+ group = Group.new
119
+ groups.last.push_entry(group)
120
+ groups.push(group)
121
+ end
122
+ def pop_group(groups)
123
+ groups.pop()
124
+ end
125
+ def push_tag groups, tag
126
+ groups.last.push_entry(Tag.new(tag, true))
127
+ end
128
+ def push_not_tag groups, tag
129
+ groups.last.push_entry(Tag.new(tag, false))
130
+ end
131
+ def push_or groups
132
+ groups.last.push_condition(:or)
133
+ end
134
+ def push_and groups
135
+ groups.last.push_condition(:and)
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,3 @@
1
+ module Tagformula
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,39 @@
1
+ $: << File.dirname(__FILE__)+'/../lib'
2
+ require 'tagformula/parser'
3
+
4
+ describe Tagformula::Parser do
5
+ describe '#parse' do
6
+
7
+ it "parses simple formulas" do
8
+ formula = Tagformula::Parser.parse("alpha & beta")
9
+ expect(formula.required_tags).to include('alpha', 'beta')
10
+ expect(formula.required_tags).not_to include('gamma')
11
+ expect(formula.matches?(['alpha'])).to be_false
12
+ expect(formula.matches?(['alpha', 'beta'])).to be_true
13
+ end
14
+
15
+ it "parses more negated tag formulas" do
16
+ formula = Tagformula::Parser.parse("alpha & ! beta")
17
+ expect(formula.required_tags).to include('alpha')
18
+ expect(formula.required_tags).not_to include('beta')
19
+ expect(formula.matches?(['alpha', 'beta'])).to be_false
20
+ expect(formula.matches?(['alpha'])).to be_true
21
+ end
22
+
23
+ it "fails on invalid formulas" do
24
+ # Unclosed bracket
25
+ expect { Tagformula::Parser.parse("(alpha & beta") }.to raise_error
26
+ # Missing operator
27
+ expect { Tagformula::Parser.parse("alpha & beta gamma") }.to raise_error
28
+ # Empty group
29
+ expect { Tagformula::Parser.parse("() & alpha & beta") }.to raise_error
30
+ end
31
+
32
+ it "parses tags with periods" do
33
+ formula = Tagformula::Parser.parse("alpha.beta | gamma.delta")
34
+ expect(formula.required_tags).to include('alpha.beta')
35
+ expect(formula.matches?(['alpha.beta'])).to be_true
36
+ expect(formula.matches?(['epsilon'])).to be_false
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,24 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'tagformula/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "tagformula"
8
+ spec.version = Tagformula::VERSION
9
+ spec.authors = ["Geoff Youngs\n\n\n"]
10
+ spec.email = ["git@intersect-uk.co.uk"]
11
+ spec.summary = %q{Simple formula parser & matcher}
12
+ spec.description = %q{Match tag formulas with simple boolean operations against lists of strings. e.g. the formula "foo & ! bar" would match ['foo'], but not ['foo', 'bar'] }
13
+ spec.homepage = "http://github.com/geoffyoungs/tagformula-parser"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.5"
22
+ spec.add_development_dependency "rake", '~> 10.1'
23
+ spec.add_development_dependency "rspec", "~> 2.14"
24
+ end
metadata ADDED
@@ -0,0 +1,101 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tagformula
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - |+
8
+ Geoff Youngs
9
+
10
+
11
+ autorequire:
12
+ bindir: bin
13
+ cert_chain: []
14
+ date: 2014-02-25 00:00:00.000000000 Z
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: bundler
18
+ requirement: !ruby/object:Gem::Requirement
19
+ requirements:
20
+ - - ~>
21
+ - !ruby/object:Gem::Version
22
+ version: '1.5'
23
+ type: :development
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '1.5'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rake
32
+ requirement: !ruby/object:Gem::Requirement
33
+ requirements:
34
+ - - ~>
35
+ - !ruby/object:Gem::Version
36
+ version: '10.1'
37
+ type: :development
38
+ prerelease: false
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ version: '10.1'
44
+ - !ruby/object:Gem::Dependency
45
+ name: rspec
46
+ requirement: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ~>
49
+ - !ruby/object:Gem::Version
50
+ version: '2.14'
51
+ type: :development
52
+ prerelease: false
53
+ version_requirements: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ~>
56
+ - !ruby/object:Gem::Version
57
+ version: '2.14'
58
+ description: 'Match tag formulas with simple boolean operations against lists of strings. e.g.
59
+ the formula "foo & ! bar" would match [''foo''], but not [''foo'', ''bar''] '
60
+ email:
61
+ - git@intersect-uk.co.uk
62
+ executables: []
63
+ extensions: []
64
+ extra_rdoc_files: []
65
+ files:
66
+ - .gitignore
67
+ - Gemfile
68
+ - LICENSE.txt
69
+ - README.md
70
+ - Rakefile
71
+ - lib/tagformula.rb
72
+ - lib/tagformula/parser.rb
73
+ - lib/tagformula/version.rb
74
+ - spec/formula_spec.rb
75
+ - tagformula.gemspec
76
+ homepage: http://github.com/geoffyoungs/tagformula-parser
77
+ licenses:
78
+ - MIT
79
+ metadata: {}
80
+ post_install_message:
81
+ rdoc_options: []
82
+ require_paths:
83
+ - lib
84
+ required_ruby_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - '>='
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ required_rubygems_version: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ requirements: []
95
+ rubyforge_project:
96
+ rubygems_version: 2.2.1
97
+ signing_key:
98
+ specification_version: 4
99
+ summary: Simple formula parser & matcher
100
+ test_files:
101
+ - spec/formula_spec.rb