predicator 1.2.1 → 2.0.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +42 -0
- data/{LICENSE.txt → LICENSE} +1 -1
- data/README.md +322 -46
- data/lib/predicator/ast.rb +74 -187
- data/lib/predicator/boundary.rb +237 -0
- data/lib/predicator/civil.rb +112 -0
- data/lib/predicator/duration_text.rb +92 -0
- data/lib/predicator/emitter/chains.rb +62 -0
- data/lib/predicator/emitter/statements.rb +86 -0
- data/lib/predicator/emitter.rb +187 -0
- data/lib/predicator/errors.rb +140 -0
- data/lib/predicator/evaluator/access.rb +108 -0
- data/lib/predicator/evaluator/arithmetic.rb +132 -0
- data/lib/predicator/evaluator/calls.rb +129 -0
- data/lib/predicator/evaluator/casts.rb +158 -0
- data/lib/predicator/evaluator/chronology.rb +88 -0
- data/lib/predicator/evaluator/compare.rb +158 -0
- data/lib/predicator/evaluator/json_functions.rb +167 -0
- data/lib/predicator/evaluator/json_scan.rb +303 -0
- data/lib/predicator/evaluator/library.rb +127 -0
- data/lib/predicator/evaluator/math_functions.rb +72 -0
- data/lib/predicator/evaluator/order.rb +120 -0
- data/lib/predicator/evaluator/rich_types.rb +146 -0
- data/lib/predicator/evaluator/statements.rb +151 -0
- data/lib/predicator/evaluator.rb +391 -117
- data/lib/predicator/float_text.rb +94 -0
- data/lib/predicator/iso_text.rb +99 -0
- data/lib/predicator/lexer/literals.rb +115 -0
- data/lib/predicator/lexer/numbers.rb +91 -0
- data/lib/predicator/lexer.rb +194 -0
- data/lib/predicator/parser/durations.rb +108 -0
- data/lib/predicator/parser/operators.rb +86 -0
- data/lib/predicator/parser/postfix.rb +57 -0
- data/lib/predicator/parser/primaries.rb +122 -0
- data/lib/predicator/parser/statements.rb +114 -0
- data/lib/predicator/parser/token_text.rb +52 -0
- data/lib/predicator/parser.rb +217 -438
- data/lib/predicator/result.rb +46 -0
- data/lib/predicator/tagged/reader.rb +139 -0
- data/lib/predicator/tagged/writer.rb +106 -0
- data/lib/predicator/tagged.rb +134 -0
- data/lib/predicator/values.rb +147 -0
- data/lib/predicator/version.rb +4 -1
- data/lib/predicator.rb +384 -14
- metadata +53 -147
- data/.gitignore +0 -11
- data/.rubocop.yml +0 -46
- data/.travis.yml +0 -5
- data/CODE_OF_CONDUCT.md +0 -49
- data/Gemfile +0 -4
- data/HISTORY.md +0 -51
- data/Rakefile +0 -24
- data/lib/predicator/context.rb +0 -18
- data/lib/predicator/lexer.rex +0 -69
- data/lib/predicator/lexer.rex.rb +0 -187
- data/lib/predicator/parser.y +0 -120
- data/lib/predicator/visitors/dot.rb +0 -113
- data/lib/predicator/visitors/each.rb +0 -16
- data/lib/predicator/visitors/instructions.rb +0 -183
- data/lib/predicator/visitors/string.rb +0 -76
- data/lib/predicator/visitors/visitor.rb +0 -100
- data/lib/predicator/visitors.rb +0 -5
- data/predicator.gemspec +0 -34
data/lib/predicator/ast.rb
CHANGED
|
@@ -1,190 +1,77 @@
|
|
|
1
|
-
|
|
2
|
-
module AST
|
|
3
|
-
class Node
|
|
4
|
-
include Enumerable
|
|
5
|
-
|
|
6
|
-
attr_accessor :left
|
|
7
|
-
|
|
8
|
-
def initialize left
|
|
9
|
-
@left = left
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def each &block
|
|
13
|
-
Visitors::Each.new(block).accept self
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def to_dot
|
|
17
|
-
Visitors::Dot.new.accept self
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def to_instructions
|
|
21
|
-
Visitors::Instructions.new.accept self
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def to_s
|
|
25
|
-
Visitors::String.new.accept self
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def type
|
|
29
|
-
raise NotImplementedError
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def variable?; false; end
|
|
33
|
-
def literal?; false; end
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
class Terminal < Node
|
|
37
|
-
alias :symbol :left
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
class Literal < Terminal
|
|
41
|
-
def type; :LITERAL; end
|
|
42
|
-
def literal?; true; end
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
class Variable < Terminal
|
|
46
|
-
def type; :VARIABLE; end
|
|
47
|
-
def variable?; true; end
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
%w[ True False Integer String Date Duration Blank Present ].each do |t|
|
|
51
|
-
class_eval <<-eoruby, __FILE__, __LINE__ + 1
|
|
52
|
-
class #{t} < Literal;
|
|
53
|
-
def type; :#{t.upcase}; end
|
|
54
|
-
end
|
|
55
|
-
eoruby
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
class Unary < Node
|
|
59
|
-
def children; [left] end
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
class IntegerArray < Unary
|
|
63
|
-
def type; :INTARRAY; end
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
class StringArray < Unary
|
|
67
|
-
def type; :STRARRAY; end
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
class Not < Unary
|
|
71
|
-
def type; :NOT; end
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
class Group < Unary
|
|
75
|
-
def type; :GROUP; end
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
class DateFromNow < Unary
|
|
79
|
-
def type; :DATEFROMNOW; end
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
class DateAgo < Unary
|
|
83
|
-
def type; :DATEAGO; end
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
class Binary < Node
|
|
87
|
-
attr_accessor :right
|
|
88
|
-
|
|
89
|
-
def initialize left, right
|
|
90
|
-
super left
|
|
91
|
-
@right = right
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def children; [left, right] end
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
class IntegerEqual < Binary
|
|
98
|
-
def type; :INTEQ; end
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
class StringEqual < Binary
|
|
102
|
-
def type; :STREQ; end
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
class DateEqual < Binary
|
|
106
|
-
def type; :DATEQ; end
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
class IntegerGreaterThan < Binary
|
|
110
|
-
def type; :INTGT; end
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
class StringGreaterThan < Binary
|
|
114
|
-
def type; :STRGT; end
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
class DateGreaterThan < Binary
|
|
118
|
-
def type; :DATGT; end
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
class IntegerLessThan < Binary
|
|
122
|
-
def type; :INTLT; end
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
class StringLessThan < Binary
|
|
126
|
-
def type; :STRLT; end
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
class DateLessThan < Binary
|
|
130
|
-
def type; :DATLT; end
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
class IntegerIn < Binary
|
|
134
|
-
def type; :INTIN; end
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
class StringIn < Binary
|
|
138
|
-
def type; :STRIN; end
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
class IntegerNotIn < Binary
|
|
142
|
-
def type; :INTNOTIN; end
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
class StringNotIn < Binary
|
|
146
|
-
def type; :STRNOTIN; end
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
class StringStartsWith < Binary
|
|
150
|
-
def type; :STRSTARTSWITH; end
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
class StringEndsWith < Binary
|
|
154
|
-
def type; :STRENDSWITH; end
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
class And < Binary
|
|
158
|
-
def type; :AND; end
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
class Or < Binary
|
|
162
|
-
def type; :OR; end
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
class Ternary < Node
|
|
166
|
-
attr_accessor :middle, :right
|
|
167
|
-
|
|
168
|
-
def initialize left, middle, right
|
|
169
|
-
super left
|
|
170
|
-
@middle = middle
|
|
171
|
-
@right = right
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
def children; [left, middle, right] end
|
|
175
|
-
end
|
|
176
|
-
|
|
177
|
-
class IntegerBetween < Ternary
|
|
178
|
-
def type; :INTBETWEEN; end
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
class DateBetween < Ternary
|
|
182
|
-
def type; :DATBETWEEN; end
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
class BooleanVariable < Unary
|
|
186
|
-
def type; :BOOL; end
|
|
187
|
-
end
|
|
1
|
+
# frozen_string_literal: true
|
|
188
2
|
|
|
3
|
+
module Predicator
|
|
4
|
+
# The syntax tree the parser builds: the reference's node kinds, with the
|
|
5
|
+
# reference's fields, as immutable values.
|
|
6
|
+
#
|
|
7
|
+
# Every node carries both of the reference's two kinds of source metadata,
|
|
8
|
+
# where the reference's parser carries one or the other per parse. The
|
|
9
|
+
# position is the {Predicator::Position} of the token that defines the
|
|
10
|
+
# node, which is not always its leftmost token: an operator node points at
|
|
11
|
+
# its operator, an access node at the property or the key rather than the
|
|
12
|
+
# punctuation before it, a cast at its type name, a relative date written
|
|
13
|
+
# after its duration at the word `ago` or `from`. The span is the source
|
|
14
|
+
# the node covers, a {Lexer::Span} with an exclusive end. A parenthesized
|
|
15
|
+
# expression keeps its node's position and widens its span to the
|
|
16
|
+
# parentheses.
|
|
17
|
+
#
|
|
18
|
+
# The kinds and their fields follow the reference's syntax tree:
|
|
19
|
+
#
|
|
20
|
+
# - {Literal}: a number, a boolean, null, undefined, a date or a datetime;
|
|
21
|
+
# its value is the predicator value ({Predicator::UNDEFINED}, nil, an
|
|
22
|
+
# Integer, a Float, a {Predicator::Date}, a {Predicator::DateTime}). A
|
|
23
|
+
# negative number is a {Unary} minus over a literal, never a negative
|
|
24
|
+
# literal.
|
|
25
|
+
# - {StringLiteral}: a string and the quote it was written with
|
|
26
|
+
# (`:double` or `:single`).
|
|
27
|
+
# - {Identifier}, {Comparison}, {Arithmetic}, {Membership}, {Unary},
|
|
28
|
+
# {LogicalAnd}, {LogicalOr}, {LogicalNot}, {ListLiteral},
|
|
29
|
+
# {ObjectLiteral} (whose entries are `[ObjectKey, node]` pairs),
|
|
30
|
+
# {FunctionCall}, {BracketAccess}, {PropertyAccess}, {Cast},
|
|
31
|
+
# {Duration} (whose units are `[amount, suffix]` pairs in the order
|
|
32
|
+
# written, a fraction already expanded into whole units) and
|
|
33
|
+
# {RelativeDate}.
|
|
34
|
+
# - The statement kinds a program holds: {Assignment}, {If} (an `else if`
|
|
35
|
+
# is an else {Block} holding one {If}, which borrows that `if`'s
|
|
36
|
+
# position and span), {While}, {Block} and {Program}. Any expression
|
|
37
|
+
# node also stands as a statement.
|
|
38
|
+
#
|
|
39
|
+
# The operators are the reference's: comparison `:gt`, `:lt`, `:gte`,
|
|
40
|
+
# `:lte`, `:equal_equal`, `:ne`, `:strict_eq`, `:strict_ne`; arithmetic
|
|
41
|
+
# `:add`, `:subtract`, `:multiply`, `:divide`, `:modulo`; membership
|
|
42
|
+
# `:in`, `:contains`; unary `:minus`, `:bang`; relative-date directions
|
|
43
|
+
# `:ago`, `:future` (written `from now`), `:next`, `:last`.
|
|
44
|
+
#
|
|
45
|
+
# The module is private to the package.
|
|
46
|
+
module Ast
|
|
47
|
+
# A node class with the fields given, then a position and a span.
|
|
48
|
+
def self.node(*fields) = Data.define(*fields, :position, :span)
|
|
49
|
+
private_class_method :node
|
|
50
|
+
|
|
51
|
+
Literal = node(:value)
|
|
52
|
+
StringLiteral = node(:value, :quote)
|
|
53
|
+
Identifier = node(:name)
|
|
54
|
+
Comparison = node(:operator, :left, :right)
|
|
55
|
+
Arithmetic = node(:operator, :left, :right)
|
|
56
|
+
Membership = node(:operator, :left, :right)
|
|
57
|
+
Unary = node(:operator, :operand)
|
|
58
|
+
LogicalAnd = node(:left, :right)
|
|
59
|
+
LogicalOr = node(:left, :right)
|
|
60
|
+
LogicalNot = node(:operand)
|
|
61
|
+
ListLiteral = node(:elements)
|
|
62
|
+
ObjectLiteral = node(:entries)
|
|
63
|
+
ObjectKey = node(:name, :style)
|
|
64
|
+
FunctionCall = node(:name, :arguments)
|
|
65
|
+
BracketAccess = node(:object, :key)
|
|
66
|
+
PropertyAccess = node(:object, :property)
|
|
67
|
+
Cast = node(:expression, :type_name)
|
|
68
|
+
Duration = node(:units)
|
|
69
|
+
RelativeDate = node(:duration, :direction)
|
|
70
|
+
Assignment = node(:target, :value)
|
|
71
|
+
Block = node(:statements)
|
|
72
|
+
If = node(:condition, :then_block, :else_block)
|
|
73
|
+
While = node(:condition, :body)
|
|
74
|
+
Program = node(:statements)
|
|
189
75
|
end
|
|
76
|
+
private_constant :Ast
|
|
190
77
|
end
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "values"
|
|
4
|
+
require_relative "errors"
|
|
5
|
+
|
|
6
|
+
module Predicator
|
|
7
|
+
# The value boundary: where a value a host hands in (a context, a literal
|
|
8
|
+
# operand of an instruction list it built, the answer of one of its
|
|
9
|
+
# functions) is checked against the value domain before an evaluation
|
|
10
|
+
# holds it, and where an argument handed to a host function is copied.
|
|
11
|
+
#
|
|
12
|
+
# The domain is nil, true and false, an Integer, a finite Float, a String
|
|
13
|
+
# in UTF-8 (or US-ASCII) that is valid in its encoding, an Array of
|
|
14
|
+
# values, a Hash of keys to values, {Predicator::UNDEFINED}, a
|
|
15
|
+
# {Predicator::Date}, a {Predicator::DateTime} and a {Predicator::Duration}.
|
|
16
|
+
# A map key is a String, an Integer, true, false, nil or
|
|
17
|
+
# {Predicator::UNDEFINED}, the keys a bracket access reads; a key of the
|
|
18
|
+
# context itself, the name a load reads, is a String.
|
|
19
|
+
# A value outside it is refused with an {EvaluationError}, never raised,
|
|
20
|
+
# under one of these reasons:
|
|
21
|
+
#
|
|
22
|
+
# - "unsupported_host_value": a value the domain has no member for (a
|
|
23
|
+
# Symbol, a Time, any other object), a map key outside the keys above,
|
|
24
|
+
# or a String that is not valid UTF-8 text;
|
|
25
|
+
# - "non_finite_number": a Float that is not finite;
|
|
26
|
+
# - "cyclic_value": a list or a map that contains itself;
|
|
27
|
+
# - "depth_limit_exceeded": lists and maps nested past {DEPTH_LIMIT}
|
|
28
|
+
# levels along any path, the outermost counting as the first, a list or
|
|
29
|
+
# map shared by several paths measured along each.
|
|
30
|
+
#
|
|
31
|
+
# The walks are iterative, so a value deeper than the stack is refused
|
|
32
|
+
# rather than exhausting it, and each list or map is walked once however
|
|
33
|
+
# many paths reach it. The module holds no state. It is private to the
|
|
34
|
+
# package.
|
|
35
|
+
module Boundary
|
|
36
|
+
# How deep lists and maps may nest in a value that crosses the boundary:
|
|
37
|
+
# 100 levels, the bound the tagged codec holds a text to.
|
|
38
|
+
DEPTH_LIMIT = 100
|
|
39
|
+
|
|
40
|
+
# The String encodings a String in the domain may carry.
|
|
41
|
+
TEXT_ENCODINGS = [Encoding::UTF_8, Encoding::US_ASCII].freeze
|
|
42
|
+
|
|
43
|
+
# The keys a map may have besides a String and an Integer.
|
|
44
|
+
MAP_KEY_ATOMS = [true, false, nil, UNDEFINED].freeze
|
|
45
|
+
|
|
46
|
+
module_function
|
|
47
|
+
|
|
48
|
+
# The refusal of what an evaluation is handed, or nil when all of it is
|
|
49
|
+
# in the domain: the context's keys, then its values, then every `lit`
|
|
50
|
+
# operand of the instruction list, in order.
|
|
51
|
+
#
|
|
52
|
+
# @param instructions [Array] the instruction list
|
|
53
|
+
# @param context [Hash] the context
|
|
54
|
+
# @return [EvaluationError, nil]
|
|
55
|
+
def input_refusal(instructions, context)
|
|
56
|
+
root_refusal(context) || refusal(context, "the context") ||
|
|
57
|
+
instructions.each_with_index.lazy.filter_map do |instruction, at|
|
|
58
|
+
next unless instruction.is_a?(Array) && instruction.first == "lit" && instruction.size == 2
|
|
59
|
+
|
|
60
|
+
refusal(instruction.last, "the literal at index #{at}")
|
|
61
|
+
end.first
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# The refusal of a context key that is not a String, or nil.
|
|
65
|
+
def root_refusal(context)
|
|
66
|
+
key = context.each_key.find { |name| !name.is_a?(String) }
|
|
67
|
+
return unless key || context.key?(key)
|
|
68
|
+
|
|
69
|
+
EvaluationError.new(reason: "unsupported_host_value",
|
|
70
|
+
message: "the context has the key #{key.inspect}, which is not a String")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# The refusal of a value, or nil when it is in the domain.
|
|
74
|
+
#
|
|
75
|
+
# @param value [Object]
|
|
76
|
+
# @param subject [String] what the value is, for the message
|
|
77
|
+
# @return [EvaluationError, nil]
|
|
78
|
+
def refusal(value, subject)
|
|
79
|
+
reason, detail = fault(value)
|
|
80
|
+
EvaluationError.new(reason:, message: "#{subject} #{detail}") if reason
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# The first fault of a value, as a reason and a phrase, or nil.
|
|
84
|
+
def fault(value) = Check.new.fault(value)
|
|
85
|
+
|
|
86
|
+
# The fault of a value that is not a list or a map, or nil.
|
|
87
|
+
def leaf_fault(value)
|
|
88
|
+
case value
|
|
89
|
+
when nil, true, false, Integer, Predicator::Date, Predicator::DateTime, Duration, Array, Hash then nil
|
|
90
|
+
when Float then ["non_finite_number", "holds the float #{value}, which is not finite"] unless value.finite?
|
|
91
|
+
when String then text_fault(value)
|
|
92
|
+
else ["unsupported_host_value", "holds #{value.inspect}, which is not a predicator value"] \
|
|
93
|
+
unless UNDEFINED.equal?(value)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def text_fault(text)
|
|
98
|
+
return if TEXT_ENCODINGS.include?(text.encoding) && text.valid_encoding?
|
|
99
|
+
|
|
100
|
+
["unsupported_host_value", "holds a String that is not valid UTF-8 text"]
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# The fault of a map's first key outside the keys a map may have, or
|
|
104
|
+
# nil.
|
|
105
|
+
def key_fault(map)
|
|
106
|
+
map.each_key do |key|
|
|
107
|
+
next if key.is_a?(Integer) || MAP_KEY_ATOMS.any? { |atom| atom.equal?(key) }
|
|
108
|
+
return ["unsupported_host_value", "has the key #{key.inspect}, which is not a map key"] unless key.is_a?(String)
|
|
109
|
+
|
|
110
|
+
found = text_fault(key)
|
|
111
|
+
return found if found
|
|
112
|
+
end
|
|
113
|
+
nil
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def container?(value) = value.is_a?(Array) || value.is_a?(Hash)
|
|
117
|
+
def members(container) = container.is_a?(Hash) ? container.values : container
|
|
118
|
+
|
|
119
|
+
# A copy of a value in the domain that shares no list, map or String
|
|
120
|
+
# with it, so that whatever is done to the copy leaves the value as it
|
|
121
|
+
# was. Every other member of the domain is immutable and is shared. A
|
|
122
|
+
# list or a map reached by two paths is copied once, and the walk is
|
|
123
|
+
# iterative.
|
|
124
|
+
#
|
|
125
|
+
# @param value [Object] a value in the domain, with no cycle
|
|
126
|
+
# @return [Object] the copy
|
|
127
|
+
def copy(value) = container?(value) ? Copy.new.copy(value) : copy_text(value)
|
|
128
|
+
|
|
129
|
+
def copy_text(value) = value.is_a?(String) ? value.dup : value
|
|
130
|
+
|
|
131
|
+
# One check of one value: a depth-first walk over an explicit stack. A
|
|
132
|
+
# list or a map is :open, on the path, from when its members are queued
|
|
133
|
+
# until the marker queued beneath them is reached; meeting an open one
|
|
134
|
+
# again is a cycle. From then on it holds its height, the levels it
|
|
135
|
+
# nests including itself, and is not walked again: a path that reaches
|
|
136
|
+
# it at a depth where depth plus height passes the limit by one is
|
|
137
|
+
# refused, so the limit holds along every path however much of the value
|
|
138
|
+
# is shared.
|
|
139
|
+
class Check
|
|
140
|
+
def initialize
|
|
141
|
+
@state = {}.compare_by_identity
|
|
142
|
+
@work = []
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def fault(root)
|
|
146
|
+
@work.push([root, 1])
|
|
147
|
+
until @work.empty?
|
|
148
|
+
value, depth = @work.pop
|
|
149
|
+
next leave(value) if depth == :leave
|
|
150
|
+
|
|
151
|
+
found = Boundary.leaf_fault(value) || container_fault(value, depth)
|
|
152
|
+
return found if found
|
|
153
|
+
end
|
|
154
|
+
nil
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
private
|
|
158
|
+
|
|
159
|
+
# Marks a container walked, with its height: one more than its
|
|
160
|
+
# tallest member's, a member that is not a list or a map counting 0.
|
|
161
|
+
def leave(container)
|
|
162
|
+
tallest = Boundary.members(container).map { |member| Boundary.container?(member) ? @state[member] : 0 }.max
|
|
163
|
+
@state[container] = 1 + (tallest || 0)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# The fault of a list or a map, or nil having queued its members; nil
|
|
167
|
+
# for any other value and for one already walked whose height fits.
|
|
168
|
+
def container_fault(value, depth)
|
|
169
|
+
return unless Boundary.container?(value)
|
|
170
|
+
|
|
171
|
+
state = @state[value]
|
|
172
|
+
return state_fault(state, depth) if state
|
|
173
|
+
|
|
174
|
+
return too_deep if depth > DEPTH_LIMIT
|
|
175
|
+
|
|
176
|
+
(value.is_a?(Hash) && Boundary.key_fault(value)) || enter(value, depth)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# The fault of meeting a container again: a cycle while it is open,
|
|
180
|
+
# too deep when its height from this depth passes the limit.
|
|
181
|
+
def state_fault(state, depth)
|
|
182
|
+
return ["cyclic_value", "holds a list or a map that contains itself"] if state == :open
|
|
183
|
+
|
|
184
|
+
too_deep if depth + state - 1 > DEPTH_LIMIT
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def too_deep = ["depth_limit_exceeded", "nests past #{DEPTH_LIMIT} levels"]
|
|
188
|
+
|
|
189
|
+
def enter(container, depth)
|
|
190
|
+
@state[container] = :open
|
|
191
|
+
@work.push([container, :leave])
|
|
192
|
+
Boundary.members(container).each { |member| @work.push([member, depth + 1]) }
|
|
193
|
+
nil
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
private_constant :Check
|
|
197
|
+
|
|
198
|
+
# One copy of one list or map: a depth-first walk over an explicit
|
|
199
|
+
# stack that builds each container after its members.
|
|
200
|
+
class Copy
|
|
201
|
+
def initialize
|
|
202
|
+
@copies = {}.compare_by_identity
|
|
203
|
+
@work = []
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def copy(root)
|
|
207
|
+
@work.push([root, false])
|
|
208
|
+
until @work.empty?
|
|
209
|
+
held, members_built = @work.pop
|
|
210
|
+
next @copies[held] = build(held) if members_built
|
|
211
|
+
|
|
212
|
+
enter(held) unless @copies.key?(held)
|
|
213
|
+
end
|
|
214
|
+
@copies[root]
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
private
|
|
218
|
+
|
|
219
|
+
def enter(container)
|
|
220
|
+
@copies[container] = nil
|
|
221
|
+
@work.push([container, true])
|
|
222
|
+
Boundary.members(container).each { |member| @work.push([member, false]) if Boundary.container?(member) }
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# The copy of one list or map, its members' copies already built.
|
|
226
|
+
def build(container)
|
|
227
|
+
return container.map { |member| take(member) } if container.is_a?(Array)
|
|
228
|
+
|
|
229
|
+
container.to_h { |key, member| [key.dup, take(member)] }
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def take(member) = @copies.key?(member) ? @copies[member] : Boundary.copy_text(member)
|
|
233
|
+
end
|
|
234
|
+
private_constant :Copy
|
|
235
|
+
end
|
|
236
|
+
private_constant :Boundary
|
|
237
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Predicator
|
|
4
|
+
# Calendar arithmetic on the proleptic Gregorian calendar, written by hand so
|
|
5
|
+
# that the package needs no date library: a civil date (year, month, day) to
|
|
6
|
+
# a count of days since 1970-01-01 and back, and the tests built on them.
|
|
7
|
+
#
|
|
8
|
+
# Every function but the four that convert a {Predicator::Date} or a
|
|
9
|
+
# {Predicator::DateTime} takes and answers Integers. The algorithms are the era-based ones published by
|
|
10
|
+
# Howard Hinnant ("chrono-Compatible Low-Level Date Algorithms"), exact for
|
|
11
|
+
# every Integer year, so a year before 1 or after 9999 converts as well as
|
|
12
|
+
# any other.
|
|
13
|
+
#
|
|
14
|
+
# The module holds no state. It is private to the package: the value classes
|
|
15
|
+
# in values.rb, the evaluator and the codec in tagged.rb are its callers.
|
|
16
|
+
module Civil
|
|
17
|
+
# Seconds in a civil day; an instant's day is its epoch second divided by
|
|
18
|
+
# this, rounded toward the past.
|
|
19
|
+
SECONDS_PER_DAY = 86_400
|
|
20
|
+
|
|
21
|
+
# The days in each month of a year that is not a leap year.
|
|
22
|
+
MONTH_LENGTHS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31].freeze
|
|
23
|
+
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
# The count of days from 1970-01-01 to the civil date year-month-day,
|
|
27
|
+
# negative before it. month and day are taken as given; a caller wanting
|
|
28
|
+
# to know they name a date asks civil?.
|
|
29
|
+
def days_from_civil(year, month, day)
|
|
30
|
+
shifted = month <= 2 ? year - 1 : year
|
|
31
|
+
era = shifted.div(400)
|
|
32
|
+
(era * 146_097) + day_of_era(shifted - (era * 400), day_of_year(month, day)) - 719_468
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# The civil date days after 1970-01-01, as [year, month, day].
|
|
36
|
+
def civil_from_days(days)
|
|
37
|
+
shifted = days + 719_468
|
|
38
|
+
era = shifted.div(146_097)
|
|
39
|
+
era_day = shifted - (era * 146_097)
|
|
40
|
+
era_year = year_of_era(era_day)
|
|
41
|
+
month, day = month_and_day(era_day - day_of_era(era_year, 0))
|
|
42
|
+
[era_year + (era * 400) + (month <= 2 ? 1 : 0), month, day]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# The day of a year that starts on March 1st.
|
|
46
|
+
def day_of_year(month, day) = (((153 * (month + (month > 2 ? -3 : 9))) + 2) / 5) + day - 1
|
|
47
|
+
|
|
48
|
+
# The day of a 400-year era, from the year of the era and the day of
|
|
49
|
+
# that (March-based) year.
|
|
50
|
+
def day_of_era(year_of_era, day_of_year)
|
|
51
|
+
(year_of_era * 365) + (year_of_era / 4) - (year_of_era / 100) + day_of_year
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# The year of a 400-year era a day of the era falls in.
|
|
55
|
+
def year_of_era(day_of_era)
|
|
56
|
+
(day_of_era - (day_of_era / 1460) + (day_of_era / 36_524) - (day_of_era / 146_096)) / 365
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# The civil month and day of a day of a March-based year.
|
|
60
|
+
def month_and_day(day_of_year)
|
|
61
|
+
month_from_march = ((5 * day_of_year) + 2) / 153
|
|
62
|
+
day = day_of_year - (((153 * month_from_march) + 2) / 5) + 1
|
|
63
|
+
[month_from_march + (month_from_march < 10 ? 3 : -9), day]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Whether part is an Integer from 0 to top.
|
|
67
|
+
def within?(part, top) = part.is_a?(Integer) && part.between?(0, top)
|
|
68
|
+
|
|
69
|
+
# Whether year, month and day are Integers naming a date on the calendar:
|
|
70
|
+
# a month from 1 to 12 and a day that month has in that year. It reads
|
|
71
|
+
# the month lengths and the leap-year rule, not the day count, so the two
|
|
72
|
+
# conversions above are checked against it rather than by it.
|
|
73
|
+
def civil?(year, month, day)
|
|
74
|
+
return false unless [year, month, day].all?(Integer) && month.between?(1, 12)
|
|
75
|
+
|
|
76
|
+
day.between?(1, days_in_month(year, month))
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# The {Predicator::Date} days after 1970-01-01.
|
|
80
|
+
def date_of_days(days) = Predicator::Date.new(*civil_from_days(days))
|
|
81
|
+
|
|
82
|
+
# The count of days from 1970-01-01 to a {Predicator::Date}, negative
|
|
83
|
+
# before it.
|
|
84
|
+
def days_of(date) = days_from_civil(date.year, date.month, date.day)
|
|
85
|
+
|
|
86
|
+
# The {Predicator::DateTime} at a {Predicator::Date} and a time of day,
|
|
87
|
+
# in UTC.
|
|
88
|
+
def instant_at(date, hour = 0, minute = 0, second = 0, microsecond: 0)
|
|
89
|
+
clock = [hour, minute, second]
|
|
90
|
+
unless date.is_a?(Predicator::Date) && clock.zip([23, 59, 59]).all? { |part, top| within?(part, top) }
|
|
91
|
+
raise ArgumentError, "#{date.inspect} at #{clock.inspect} does not name a time of day on a date"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
seconds = (days_of(date) * SECONDS_PER_DAY) + (hour * 3600) + (minute * 60) + second
|
|
95
|
+
Predicator::DateTime.new(epoch_seconds: seconds, microsecond:)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# The civil {Predicator::Date} of a {Predicator::DateTime}, in UTC.
|
|
99
|
+
def date_of(instant) = date_of_days(instant.epoch_seconds.div(SECONDS_PER_DAY))
|
|
100
|
+
|
|
101
|
+
# The days in a month of a year.
|
|
102
|
+
def days_in_month(year, month)
|
|
103
|
+
return 29 if month == 2 && leap?(year)
|
|
104
|
+
|
|
105
|
+
MONTH_LENGTHS[month - 1]
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Whether a year is a leap year on the proleptic Gregorian calendar.
|
|
109
|
+
def leap?(year) = (year % 4).zero? && (!(year % 100).zero? || (year % 400).zero?)
|
|
110
|
+
end
|
|
111
|
+
private_constant :Civil
|
|
112
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Predicator
|
|
4
|
+
# A duration as text in the language's duration-literal grammar, both
|
|
5
|
+
# ways, as the reference writes and reads it.
|
|
6
|
+
#
|
|
7
|
+
# Written, the units come largest first, each as its amount and its
|
|
8
|
+
# suffix (`y`, `mo`, `w`, `d`, `h`, `m`, `s`, `ms`); a unit that is not
|
|
9
|
+
# positive is left out, and a duration with nothing left to write is
|
|
10
|
+
# `0s`.
|
|
11
|
+
#
|
|
12
|
+
# Read, the whole text is one or more components, each digits, an
|
|
13
|
+
# optional fraction (a point and digits) and a suffix, with no sign, no
|
|
14
|
+
# space and no empty text; `mo` and `ms` are read before `m`. A repeated
|
|
15
|
+
# unit adds to what came before. A fraction is exact or refused: it is
|
|
16
|
+
# worth a whole number of milliseconds (a month 30 days, a year 365) or
|
|
17
|
+
# the text is not a duration, and that remainder is spent largest first
|
|
18
|
+
# through days, hours, minutes, seconds and milliseconds only, the whole
|
|
19
|
+
# part keeping its own unit.
|
|
20
|
+
#
|
|
21
|
+
# The module holds no state. It is private to the package.
|
|
22
|
+
module DurationText
|
|
23
|
+
# Each unit's suffix and its worth in milliseconds, largest unit first.
|
|
24
|
+
UNITS = {
|
|
25
|
+
years: ["y", 31_536_000_000], months: ["mo", 2_592_000_000], weeks: ["w", 604_800_000],
|
|
26
|
+
days: ["d", 86_400_000], hours: ["h", 3_600_000], minutes: ["m", 60_000], seconds: ["s", 1000],
|
|
27
|
+
milliseconds: ["ms", 1]
|
|
28
|
+
}.freeze
|
|
29
|
+
|
|
30
|
+
# The unit each suffix names.
|
|
31
|
+
SUFFIXES = UNITS.to_h { |unit, (suffix, _)| [suffix, unit] }.freeze
|
|
32
|
+
|
|
33
|
+
# The units a fraction's remainder is spent into.
|
|
34
|
+
REMAINDER_UNITS = %i[days hours minutes seconds milliseconds].freeze
|
|
35
|
+
|
|
36
|
+
# One component, and the whole text as a run of them.
|
|
37
|
+
COMPONENT = /([0-9]+)(?:\.([0-9]+))?(mo|ms|y|w|d|h|m|s)/
|
|
38
|
+
TEXT = /\A(?:[0-9]+(?:\.[0-9]+)?(?:mo|ms|y|w|d|h|m|s))+\z/
|
|
39
|
+
|
|
40
|
+
module_function
|
|
41
|
+
|
|
42
|
+
# @param duration [Duration]
|
|
43
|
+
# @return [String]
|
|
44
|
+
def write(duration)
|
|
45
|
+
parts = UNITS.filter_map do |unit, (suffix, _)|
|
|
46
|
+
amount = duration.public_send(unit)
|
|
47
|
+
"#{amount}#{suffix}" if amount.positive?
|
|
48
|
+
end
|
|
49
|
+
parts.empty? ? "0s" : parts.join
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @param text [String]
|
|
53
|
+
# @return [Duration, nil] the duration the text names, or nil when it
|
|
54
|
+
# names none
|
|
55
|
+
def read(text)
|
|
56
|
+
return unless TEXT.match?(text)
|
|
57
|
+
|
|
58
|
+
pairs = text.scan(COMPONENT).map { |whole, fraction, suffix| component(whole, fraction, suffix) }
|
|
59
|
+
return if pairs.include?(nil)
|
|
60
|
+
|
|
61
|
+
units = Hash.new(0)
|
|
62
|
+
pairs.flatten(1).each { |unit, amount| units[unit] += amount }
|
|
63
|
+
Duration.new(**units)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# The unit pairs one component is worth, or nil when its fraction is not
|
|
67
|
+
# a whole number of milliseconds.
|
|
68
|
+
def component(whole, fraction, suffix)
|
|
69
|
+
whole = Integer(whole, 10)
|
|
70
|
+
unit = SUFFIXES.fetch(suffix)
|
|
71
|
+
fraction ? expand(whole, fraction, unit) : [[unit, whole]]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# The unit pairs a component with a fraction is worth, or nil when the
|
|
75
|
+
# fraction is not a whole number of milliseconds.
|
|
76
|
+
def expand(whole, fraction, unit)
|
|
77
|
+
worth = Integer(fraction, 10) * UNITS.fetch(unit).last
|
|
78
|
+
remainder, inexact = worth.divmod(10**fraction.size)
|
|
79
|
+
return unless inexact.zero?
|
|
80
|
+
|
|
81
|
+
[[unit, whole], *spend(remainder)]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def spend(milliseconds)
|
|
85
|
+
REMAINDER_UNITS.filter_map do |unit|
|
|
86
|
+
amount, milliseconds = milliseconds.divmod(UNITS.fetch(unit).last)
|
|
87
|
+
[unit, amount] if amount.positive?
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
private_constant :DurationText
|
|
92
|
+
end
|