Skip to content

Commit 9733a20

Browse files
regex
1 parent 0846cd3 commit 9733a20

8 files changed

Lines changed: 485 additions & 17 deletions

File tree

lib/code-ruby.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
require_relative "code/extensions/true_class"
3434
require_relative "code/extensions/false_class"
3535
require_relative "code/extensions/string"
36+
require_relative "code/extensions/regexp"
3637
require_relative "code/extensions/symbol"
3738
require_relative "code/extensions/integer"
3839
require_relative "code/extensions/float"

lib/code/extensions/regexp.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# frozen_string_literal: true
2+
3+
class Regexp
4+
def to_code
5+
Code::Object::Regex.new(self)
6+
end
7+
end

lib/code/object/global.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,16 @@ class Global < Object
227227
"returns the Parameter class when called without arguments, otherwise calls Parameter.new.",
228228
examples: %w[Parameter Parameter.new Parameter.new(nothing)]
229229
},
230+
"Regex" => {
231+
name: "Regex",
232+
description:
233+
"returns the Regex class when called without arguments, otherwise calls Regex.new.",
234+
examples: [
235+
"Regex",
236+
"Regex.new(\"a|b|c\")",
237+
"Regex(\"^hello\", ignore_case: true)"
238+
]
239+
},
230240
"Range" => {
231241
name: "Range",
232242
description:
@@ -417,6 +427,7 @@ def self.documented_classes
417427
"Number" => Number,
418428
"Object" => Object,
419429
"Parameter" => Parameter,
430+
"Regex" => Regex,
420431
"Range" => Range,
421432
"Smtp" => Smtp,
422433
"String" => String,
@@ -550,6 +561,13 @@ def call(**args)
550561
else
551562
Class.new(Range)
552563
end
564+
when "Regex"
565+
sig(args) { Object.repeat }
566+
if code_arguments.any?
567+
Regex.new(*code_arguments.raw)
568+
else
569+
Class.new(Regex)
570+
end
553571
when "String"
554572
sig(args) { Object.repeat }
555573
if code_arguments.any?

lib/code/object/list.rb

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -724,6 +724,24 @@ class List < Object
724724
"[].filter((x) => { x })"
725725
]
726726
},
727+
"grep" => {
728+
name: "grep",
729+
description: "returns string items matched by a regex.",
730+
examples: [
731+
"[\"ant\", \"bat\", \"cat\"].grep(Regex.new(\"a\"))",
732+
"[\"ant\", \"bat\"].grep(Regex.new(\"^b\"))",
733+
"[\"ant\", \"bat\"].grep(Regex.new(\"z\"))"
734+
]
735+
},
736+
"grep_not" => {
737+
name: "grep_not",
738+
description: "returns string items not matched by a regex.",
739+
examples: [
740+
"[\"ant\", \"bat\", \"cat\"].grep_not(Regex.new(\"a\"))",
741+
"[\"ant\", \"bat\"].grep_not(Regex.new(\"^b\"))",
742+
"[\"ant\", \"bat\"].grep_not(Regex.new(\"z\"))"
743+
]
744+
},
727745
"select!" => {
728746
name: "select!",
729747
description:
@@ -3218,6 +3236,12 @@ def call(**args)
32183236
when "select", "filter"
32193237
sig(args) { (Function | Class).maybe }
32203238
code_select(code_value, **globals)
3239+
when "grep"
3240+
sig(args) { [Regex, (Function | Class).maybe] }
3241+
code_grep(*code_arguments.raw, **globals)
3242+
when "grep_not"
3243+
sig(args) { [Regex, (Function | Class).maybe] }
3244+
code_grep_not(*code_arguments.raw, **globals)
32213245
when "select!", "filter!"
32223246
sig(args) { (Function | Class).maybe }
32233247
code_select!(code_value, **globals)
@@ -4889,6 +4913,62 @@ def code_select!(argument = nil, **globals)
48894913
e.code_value
48904914
end
48914915

4916+
def code_grep(regex, argument = nil, **globals)
4917+
code_regex = regex.to_code
4918+
code_argument = argument.to_code
4919+
results = []
4920+
4921+
raw.each.with_index do |code_element, index|
4922+
next unless code_element.is_a?(String) &&
4923+
code_regex.raw.match?(code_element.raw)
4924+
4925+
results << if code_argument.is_a?(Function)
4926+
code_argument.call(
4927+
arguments: List.new([code_element, Integer.new(index), self]),
4928+
**globals
4929+
)
4930+
elsif code_argument.is_a?(Class)
4931+
code_argument.raw.new(code_element)
4932+
else
4933+
code_element
4934+
end
4935+
rescue Error::Next => e
4936+
results << e.code_value
4937+
end
4938+
4939+
List.new(results)
4940+
rescue Error::Break => e
4941+
e.code_value
4942+
end
4943+
4944+
def code_grep_not(regex, argument = nil, **globals)
4945+
code_regex = regex.to_code
4946+
code_argument = argument.to_code
4947+
results = []
4948+
4949+
raw.each.with_index do |code_element, index|
4950+
next if code_element.is_a?(String) &&
4951+
code_regex.raw.match?(code_element.raw)
4952+
4953+
results << if code_argument.is_a?(Function)
4954+
code_argument.call(
4955+
arguments: List.new([code_element, Integer.new(index), self]),
4956+
**globals
4957+
)
4958+
elsif code_argument.is_a?(Class)
4959+
code_argument.raw.new(code_element)
4960+
else
4961+
code_element
4962+
end
4963+
rescue Error::Next => e
4964+
results << e.code_value
4965+
end
4966+
4967+
List.new(results)
4968+
rescue Error::Break => e
4969+
e.code_value
4970+
end
4971+
48924972
def code_reject(argument = nil, **globals)
48934973
code_argument = argument.to_code
48944974

lib/code/object/regex.rb

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
# frozen_string_literal: true
2+
3+
class Code
4+
class Object
5+
class Regex < Object
6+
CLASS_DOCUMENTATION = {
7+
name: "Regex",
8+
description:
9+
"represents a regular expression pattern with explicit matching options.",
10+
examples: [
11+
"Regex",
12+
"Regex.new(\"a|b|c\")",
13+
"Regex.new(\"^hello\", ignore_case: true)"
14+
]
15+
}.freeze
16+
INSTANCE_FUNCTIONS = {
17+
"match?" => {
18+
name: "match?",
19+
description: "returns whether the regex matches a string.",
20+
examples: [
21+
"Regex.new(\"a|b|c\").match?(\"b\")",
22+
"Regex.new(\"^hello\").match?(\"hello world\")",
23+
"Regex.new(\"^hello\").match?(\"say hello\")"
24+
]
25+
},
26+
"source" => {
27+
name: "source",
28+
description: "returns the regex pattern source.",
29+
examples: [
30+
"Regex.new(\"a|b|c\").source",
31+
"Regex.new(\"^hello\").source",
32+
"Regex.new(\"hello\", ignore_case: true).source"
33+
]
34+
},
35+
"options" => {
36+
name: "options",
37+
description: "returns the enabled regex options.",
38+
examples: [
39+
"Regex.new(\"hello\").options",
40+
"Regex.new(\"hello\", ignore_case: true).options.ignore_case",
41+
"Regex.new(\"hello\", multiple_lines: true).options.multiple_lines"
42+
]
43+
},
44+
"ignore_case?" => {
45+
name: "ignore_case?",
46+
description: "returns whether the regex ignores case.",
47+
examples: [
48+
"Regex.new(\"hello\").ignore_case?",
49+
"Regex.new(\"hello\", ignore_case: true).ignore_case?",
50+
"Regex.new(\"hello\", ignore_case: false).ignore_case?"
51+
]
52+
},
53+
"extended?" => {
54+
name: "extended?",
55+
description: "returns whether the regex ignores whitespace and comments.",
56+
examples: [
57+
"Regex.new(\"hello\").extended?",
58+
"Regex.new(\"hello\", extended: true).extended?",
59+
"Regex.new(\"hello\", extended: false).extended?"
60+
]
61+
},
62+
"multiple_lines?" => {
63+
name: "multiple_lines?",
64+
description: "returns whether the regex matches across multiple lines.",
65+
examples: [
66+
"Regex.new(\"hello\").multiple_lines?",
67+
"Regex.new(\"hello\", multiple_lines: true).multiple_lines?",
68+
"Regex.new(\"hello\", multiple_lines: false).multiple_lines?"
69+
]
70+
},
71+
"fixed_encoding?" => {
72+
name: "fixed_encoding?",
73+
description: "returns whether the regex has fixed encoding.",
74+
examples: [
75+
"Regex.new(\"hello\").fixed_encoding?",
76+
"Regex.new(\"hello\", fixed_encoding: true).fixed_encoding?",
77+
"Regex.new(\"hello\", fixed_encoding: false).fixed_encoding?"
78+
]
79+
},
80+
"no_encoding?" => {
81+
name: "no_encoding?",
82+
description: "returns whether the regex has no encoding.",
83+
examples: [
84+
"Regex.new(\"hello\").no_encoding?",
85+
"Regex.new(\"hello\", no_encoding: true).no_encoding?",
86+
"Regex.new(\"hello\", no_encoding: false).no_encoding?"
87+
]
88+
}
89+
}.freeze
90+
OPTION_FLAGS = {
91+
"ignore_case" => ::Regexp::IGNORECASE,
92+
"extended" => ::Regexp::EXTENDED,
93+
"multiple_lines" => ::Regexp::MULTILINE,
94+
"fixed_encoding" => ::Regexp::FIXEDENCODING,
95+
"no_encoding" => ::Regexp::NOENCODING
96+
}.freeze
97+
98+
def self.function_documentation(scope)
99+
return INSTANCE_FUNCTIONS if scope == :instance
100+
101+
{}
102+
end
103+
104+
def initialize(*args, **kargs, &_block)
105+
raise Error, "Regex.new: pattern is required" if args.empty?
106+
raise Error, "Regex.new: expected 1-2 arguments" if args.size > 2
107+
108+
pattern = args.first
109+
option_values = {}
110+
111+
if args[1]
112+
case args[1]
113+
when Dictionary
114+
args[1].raw.each do |key, value|
115+
option_values[key.to_s] =
116+
value.is_an?(Object) ? value.truthy? : !!value
117+
end
118+
when ::Hash
119+
args[1].each do |key, value|
120+
option_values[key.to_s] =
121+
value.is_an?(Object) ? value.truthy? : !!value
122+
end
123+
else
124+
raise Error, "Regex.new: options must be a dictionary"
125+
end
126+
end
127+
128+
kargs.each do |key, value|
129+
option_values[key.to_s] =
130+
value.is_an?(Object) ? value.truthy? : !!value
131+
end
132+
133+
options =
134+
option_values.reduce(0) do |flags, (name, enabled)|
135+
flag = OPTION_FLAGS[name]
136+
raise Error, "unknown regex option: #{name}" unless flag
137+
138+
enabled ? flags | flag : flags
139+
end
140+
141+
self.raw =
142+
if pattern.is_a?(Regex)
143+
::Regexp.new(pattern.raw.source, pattern.raw.options | options)
144+
elsif pattern.is_a?(::Regexp)
145+
::Regexp.new(pattern.source, pattern.options | options)
146+
else
147+
::Regexp.new(pattern.to_s, options)
148+
end
149+
rescue ::RegexpError => e
150+
raise Error, "invalid regex: #{e.message}"
151+
end
152+
153+
def call(**args)
154+
code_operator = args.fetch(:operator, nil).to_code
155+
code_arguments = args.fetch(:arguments, List.new).to_code
156+
code_value = code_arguments.code_first
157+
158+
case code_operator.to_s
159+
when "match?"
160+
sig(args) { String }
161+
code_match?(code_value)
162+
when "source"
163+
sig(args)
164+
code_source
165+
when "options"
166+
sig(args)
167+
code_options
168+
when "ignore_case?"
169+
sig(args)
170+
code_ignore_case?
171+
when "extended?"
172+
sig(args)
173+
code_extended?
174+
when "multiple_lines?"
175+
sig(args)
176+
code_multiple_lines?
177+
when "fixed_encoding?"
178+
sig(args)
179+
code_fixed_encoding?
180+
when "no_encoding?"
181+
sig(args)
182+
code_no_encoding?
183+
else
184+
super
185+
end
186+
end
187+
188+
def code_match?(string)
189+
Boolean.new(raw.match?(string.to_s))
190+
end
191+
192+
def code_source
193+
String.new(raw.source)
194+
end
195+
196+
def code_options
197+
Dictionary.new(
198+
OPTION_FLAGS.transform_values { |flag| (raw.options & flag) == flag }
199+
)
200+
end
201+
202+
def code_ignore_case?
203+
Boolean.new((raw.options & ::Regexp::IGNORECASE) == ::Regexp::IGNORECASE)
204+
end
205+
206+
def code_extended?
207+
Boolean.new((raw.options & ::Regexp::EXTENDED) == ::Regexp::EXTENDED)
208+
end
209+
210+
def code_multiple_lines?
211+
Boolean.new((raw.options & ::Regexp::MULTILINE) == ::Regexp::MULTILINE)
212+
end
213+
214+
def code_fixed_encoding?
215+
Boolean.new(
216+
(raw.options & ::Regexp::FIXEDENCODING) == ::Regexp::FIXEDENCODING
217+
)
218+
end
219+
220+
def code_no_encoding?
221+
Boolean.new((raw.options & ::Regexp::NOENCODING) == ::Regexp::NOENCODING)
222+
end
223+
224+
def code_to_string
225+
code_source
226+
end
227+
228+
def code_inspect
229+
String.new(raw.inspect)
230+
end
231+
232+
end
233+
end
234+
end

0 commit comments

Comments
 (0)