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
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Predicator
|
|
4
|
+
module Evaluator
|
|
5
|
+
# The five arithmetic opcodes, as the reference's ISA (docs/isa.md,
|
|
6
|
+
# section 5) states them: the handlers {Machine} runs for their rows of
|
|
7
|
+
# {INSTRUCTIONS}, and the rule each applies to its pair of operands.
|
|
8
|
+
#
|
|
9
|
+
# Integer and Float stay apart: two Integers answer an Integer, and a
|
|
10
|
+
# Float on either side answers a Float. A Float answer that is not finite
|
|
11
|
+
# (an overflow) has no member in the value domain, and is refused as an
|
|
12
|
+
# EvaluationError with the reason non_finite_number; the reference has no
|
|
13
|
+
# answer there of its own to follow. Integers have no bound.
|
|
14
|
+
#
|
|
15
|
+
# A pair no rule takes is a TypeMismatchError whose reason is the opcode,
|
|
16
|
+
# or the unbound-variable error when an operand is the undefined an
|
|
17
|
+
# unbound load pushed.
|
|
18
|
+
module Arithmetic
|
|
19
|
+
# What a pair no rule takes answers inside this module.
|
|
20
|
+
NO_RULE = Object.new.freeze
|
|
21
|
+
|
|
22
|
+
# What each opcode's refusal says it wanted.
|
|
23
|
+
WANTED = {
|
|
24
|
+
"add" => "a number or a string", "subtract" => "a number or a date", "multiply" => "a number",
|
|
25
|
+
"divide" => "a number", "modulo" => "an integer"
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def add(at) = arithmetic(at, "add") { |left, right| sum(left, right) }
|
|
31
|
+
def subtract(at) = arithmetic(at, "subtract") { |left, right| difference(left, right) }
|
|
32
|
+
def multiply(at) = arithmetic(at, "multiply") { |left, right| numbers?(left, right) ? left * right : NO_RULE }
|
|
33
|
+
|
|
34
|
+
# The zero check runs before the type check, for an Integer 0 and a
|
|
35
|
+
# Float 0.0 (negative zero included) alike. Two Integers divide by
|
|
36
|
+
# truncating toward zero.
|
|
37
|
+
def divide(at)
|
|
38
|
+
arithmetic(at, "divide", zero: ->(right) { number?(right) && right.zero? }) do |left, right|
|
|
39
|
+
next NO_RULE unless numbers?(left, right)
|
|
40
|
+
|
|
41
|
+
left.is_a?(Integer) && right.is_a?(Integer) ? truncated_quotient(left, right) : left / right
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Integers only, a Float operand included in the refusal. The zero
|
|
46
|
+
# check runs before the type check for an Integer 0 alone: a Float 0.0
|
|
47
|
+
# is a type mismatch, not modulo by zero. The remainder takes the sign
|
|
48
|
+
# of the dividend.
|
|
49
|
+
def modulo(at)
|
|
50
|
+
arithmetic(at, "modulo", zero: ->(right) { right.is_a?(Integer) && right.zero? }) do |left, right|
|
|
51
|
+
left.is_a?(Integer) && right.is_a?(Integer) ? left.remainder(right) : NO_RULE
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Pops right then left, refuses a zero divisor when zero says so, and
|
|
56
|
+
# pushes what the rule answers.
|
|
57
|
+
def arithmetic(at, opcode, zero: nil)
|
|
58
|
+
return insufficient(opcode) if @stack.size < 2
|
|
59
|
+
|
|
60
|
+
right = @stack.pop
|
|
61
|
+
left = @stack.pop
|
|
62
|
+
return by_zero(opcode) if zero&.call(right)
|
|
63
|
+
|
|
64
|
+
answer = yield(left, right)
|
|
65
|
+
return mismatch_pair(opcode, WANTED.fetch(opcode), left, right) if NO_RULE.equal?(answer)
|
|
66
|
+
return non_finite(opcode) if answer.is_a?(Float) && !answer.finite?
|
|
67
|
+
|
|
68
|
+
push(at, answer)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Numbers add. Strings join, and a number beside a string is written
|
|
72
|
+
# as text first. Lists join. A date or a datetime and a duration, in
|
|
73
|
+
# either order, move the date.
|
|
74
|
+
def sum(left, right)
|
|
75
|
+
return left + right if numbers?(left, right) || both?(left, right, String) || both?(left, right, Array)
|
|
76
|
+
return text(left) + text(right) if text_pair?(left, right)
|
|
77
|
+
|
|
78
|
+
shifted(left, right)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def text_pair?(left, right) = (left.is_a?(String) && number?(right)) || (number?(left) && right.is_a?(String))
|
|
82
|
+
|
|
83
|
+
def text(value) = value.is_a?(String) ? value : number_text(value)
|
|
84
|
+
|
|
85
|
+
# A date or a datetime moved forward by a duration on either side.
|
|
86
|
+
def shifted(left, right)
|
|
87
|
+
return Chronology.shift(left, right, 1) if Chronology.chronological?(left) && right.is_a?(Duration)
|
|
88
|
+
return Chronology.shift(right, left, 1) if left.is_a?(Duration) && Chronology.chronological?(right)
|
|
89
|
+
|
|
90
|
+
NO_RULE
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Numbers subtract. Two dates or datetimes answer the duration between
|
|
94
|
+
# them, and a date or a datetime minus a duration moves it back. No
|
|
95
|
+
# string or list rule: subtract never joins.
|
|
96
|
+
def difference(left, right)
|
|
97
|
+
return left - right if numbers?(left, right)
|
|
98
|
+
return Chronology.difference(left, right) if Chronology.chronological?(left) && Chronology.chronological?(right)
|
|
99
|
+
return Chronology.shift(left, right, -1) if Chronology.chronological?(left) && right.is_a?(Duration)
|
|
100
|
+
|
|
101
|
+
NO_RULE
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def number?(value) = value.is_a?(Integer) || value.is_a?(Float)
|
|
105
|
+
def numbers?(left, right) = number?(left) && number?(right)
|
|
106
|
+
def both?(left, right, type) = left.is_a?(type) && right.is_a?(type)
|
|
107
|
+
|
|
108
|
+
def number_text(number) = number.is_a?(Float) ? FloatText.spell(number) : number.to_s
|
|
109
|
+
|
|
110
|
+
def truncated_quotient(left, right)
|
|
111
|
+
whole = left.abs / right.abs
|
|
112
|
+
left.negative? == right.negative? ? whole : -whole
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# A refused pair: the operand reported is the undefined one, if either
|
|
116
|
+
# is, so that an unbound load is named.
|
|
117
|
+
def mismatch_pair(opcode, wanted, left, right)
|
|
118
|
+
mismatch(opcode, wanted, UNDEFINED.equal?(right) && !UNDEFINED.equal?(left) ? right : left)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def by_zero(opcode)
|
|
122
|
+
reason = opcode == "divide" ? "division_by_zero" : "modulo_by_zero"
|
|
123
|
+
EvaluationError.new(reason:, message: "#{opcode} was given a zero divisor")
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def non_finite(opcode)
|
|
127
|
+
EvaluationError.new(reason: "non_finite_number", message: "#{opcode} answered a float that is not finite")
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
private_constant :Arithmetic
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Predicator
|
|
4
|
+
module Evaluator
|
|
5
|
+
# The `call` opcode, as the reference's ISA (docs/isa.md, section 5)
|
|
6
|
+
# states it: the handler {Machine} runs for its row of {INSTRUCTIONS}.
|
|
7
|
+
#
|
|
8
|
+
# `call` pops its argument count of values, the deepest first, and pushes
|
|
9
|
+
# what the named function answers. The name is looked up first among the
|
|
10
|
+
# functions the caller passed to this evaluation, then in the package's
|
|
11
|
+
# library ({Library}); nothing else is reachable, and nothing is
|
|
12
|
+
# registered globally.
|
|
13
|
+
#
|
|
14
|
+
# Every refusal is an EvaluationError, and except for too few values on
|
|
15
|
+
# the stack (reason insufficient_arguments) its reason is its message,
|
|
16
|
+
# as the reference writes them:
|
|
17
|
+
#
|
|
18
|
+
# - a name neither holds: `Unknown function: NAME`;
|
|
19
|
+
# - a count of arguments the function does not take:
|
|
20
|
+
# `Function NAME() expects COUNTS arguments, got N`, where COUNTS is a
|
|
21
|
+
# count, the counts it takes joined by " or ", or "at least" a count;
|
|
22
|
+
# - a library function refusing its arguments: the library's sentence,
|
|
23
|
+
# e.g. `len() expects a string argument`;
|
|
24
|
+
# - a function that raises: `Function NAME() raised: ` and the
|
|
25
|
+
# exception's inspect.
|
|
26
|
+
#
|
|
27
|
+
# A host function is called with a copy of each argument
|
|
28
|
+
# ({Boundary.copy}), so that nothing it does to a list, a map or a
|
|
29
|
+
# String it is handed changes the context or a value the evaluation
|
|
30
|
+
# holds. Its answer is checked against the value domain: one outside it
|
|
31
|
+
# is refused with an EvaluationError carrying the boundary's reason
|
|
32
|
+
# (unsupported_host_value, non_finite_number, cyclic_value or
|
|
33
|
+
# depth_limit_exceeded), and nothing is pushed.
|
|
34
|
+
module Calls
|
|
35
|
+
# How a library function refuses its arguments: the message is the
|
|
36
|
+
# refusal's reason. Raised and rescued inside a call; it never leaves
|
|
37
|
+
# one.
|
|
38
|
+
class Refusal < StandardError; end
|
|
39
|
+
|
|
40
|
+
include Library
|
|
41
|
+
|
|
42
|
+
# The counts of arguments a host's callable takes, as a Range: its
|
|
43
|
+
# required positional parameters up to those and its optional ones,
|
|
44
|
+
# with no end when it takes a rest parameter. A Proc is read as a
|
|
45
|
+
# lambda would be, so a block's missing arguments are refused rather
|
|
46
|
+
# than read as null.
|
|
47
|
+
def self.counts(callable)
|
|
48
|
+
parameters = parameters_of(callable)
|
|
49
|
+
required = parameters.count { |kind, _| kind == :req }
|
|
50
|
+
return (required..) if parameters.any? { |kind, _| kind == :rest }
|
|
51
|
+
|
|
52
|
+
required..(required + parameters.count { |kind, _| kind == :opt })
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def self.parameters_of(callable)
|
|
56
|
+
case callable
|
|
57
|
+
when Proc then callable.parameters(lambda: true)
|
|
58
|
+
when Method then callable.parameters
|
|
59
|
+
else callable.method(:call).parameters
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
private_class_method :parameters_of
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
# Pops arg_count values, calls the named function with them, first
|
|
67
|
+
# argument deepest, and pushes its answer.
|
|
68
|
+
def call_function(at, name, arg_count)
|
|
69
|
+
if @stack.size < arg_count
|
|
70
|
+
return EvaluationError.new(
|
|
71
|
+
reason: "insufficient_arguments",
|
|
72
|
+
message: "Function #{name}() expects #{arg_count} arguments, but only #{@stack.size} values on stack"
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
args = @stack.pop(arg_count)
|
|
77
|
+
answer = dispatch(name, args)
|
|
78
|
+
answer.is_a?(Answered) ? push(at, answer.value) : answer
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# What a call that succeeded answered, kept apart from the error value
|
|
82
|
+
# a refused call answers, since a function may answer any value.
|
|
83
|
+
Answered = Data.define(:value)
|
|
84
|
+
private_constant :Answered
|
|
85
|
+
|
|
86
|
+
def dispatch(name, args)
|
|
87
|
+
callable, counts = resolve(name)
|
|
88
|
+
return call_error("Unknown function: #{name}") unless callable
|
|
89
|
+
return call_error(wrong_count(name, counts, args.size)) unless counts.include?(args.size)
|
|
90
|
+
return host_call(name, callable, args) if @functions.key?(name)
|
|
91
|
+
|
|
92
|
+
Answered.new(callable.call(*args))
|
|
93
|
+
rescue ClockFault then raise
|
|
94
|
+
rescue Refusal => e
|
|
95
|
+
call_error(e.message)
|
|
96
|
+
rescue StandardError => e
|
|
97
|
+
call_error("Function #{name}() raised: #{e.inspect}")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Calls a host function with a copy of each argument, answering its
|
|
101
|
+
# answer, or the refusal of one outside the value domain ({Boundary}).
|
|
102
|
+
def host_call(name, callable, args)
|
|
103
|
+
answer = callable.call(*args.map { |arg| Boundary.copy(arg) })
|
|
104
|
+
Boundary.refusal(answer, "Function #{name}() answered a value that") || Answered.new(answer)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# The callable a name reaches and the counts of arguments it takes: a
|
|
108
|
+
# host function first, then the library's.
|
|
109
|
+
def resolve(name)
|
|
110
|
+
host = @functions[name]
|
|
111
|
+
return [host, Calls.counts(host)] if host
|
|
112
|
+
|
|
113
|
+
entry = Library::FUNCTIONS[name]
|
|
114
|
+
[method(entry.handler), entry.counts] if entry
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def wrong_count(name, counts, given)
|
|
118
|
+
described =
|
|
119
|
+
if counts.end.nil? then "at least #{counts.begin}"
|
|
120
|
+
else counts.to_a.join(" or ")
|
|
121
|
+
end
|
|
122
|
+
"Function #{name}() expects #{described} arguments, got #{given}"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def call_error(message) = EvaluationError.new(reason: message, message:)
|
|
126
|
+
end
|
|
127
|
+
private_constant :Calls
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Predicator
|
|
4
|
+
module Evaluator
|
|
5
|
+
# The `cast` opcode, as the reference's ISA (docs/isa.md, section 5)
|
|
6
|
+
# states it: the handler {Machine} runs for `cast`, and the conversion
|
|
7
|
+
# matrix it reads through.
|
|
8
|
+
#
|
|
9
|
+
# The operand is one of the seven scalar type names in {TYPE_NAMES}; any
|
|
10
|
+
# other operand is an unknown instruction. The cast pops one value and
|
|
11
|
+
# pushes what that value is as the named type. It never fails but on an
|
|
12
|
+
# empty stack: undefined converts to undefined, a value already of the
|
|
13
|
+
# type passes through, and a conversion that cannot produce a value of
|
|
14
|
+
# the type answers undefined (null, a list and a map included).
|
|
15
|
+
#
|
|
16
|
+
# The conversions:
|
|
17
|
+
#
|
|
18
|
+
# - integer: a float truncates toward zero; the text of an integer (an
|
|
19
|
+
# optional minus and decimal digits, nothing else) parses.
|
|
20
|
+
# - float: an integer widens; the text of a number (an optional minus,
|
|
21
|
+
# digits and an optional fraction, no exponent) parses.
|
|
22
|
+
# - string: an integer in decimal, a float as {FloatText} writes it, a
|
|
23
|
+
# boolean as `true` or `false`, a date as `YYYY-MM-DD`, a datetime in
|
|
24
|
+
# UTC as {Predicator::DateTime#to_s} writes it, a duration in the
|
|
25
|
+
# literal grammar {DurationText} writes.
|
|
26
|
+
# - boolean: exactly the text `true` or `false`.
|
|
27
|
+
# - date: a datetime's calendar date in UTC; the text of an ISO 8601
|
|
28
|
+
# calendar date that names a real date.
|
|
29
|
+
# - datetime: a date at midnight UTC; the text of an ISO 8601 datetime
|
|
30
|
+
# carrying a UTC offset, as the instant it names.
|
|
31
|
+
# - duration: the text of a duration literal, read as {DurationText}
|
|
32
|
+
# reads it.
|
|
33
|
+
#
|
|
34
|
+
# A map carrying the seven units the reference's duration always holds
|
|
35
|
+
# (years to seconds), each an Integer, casts as a duration: it passes
|
|
36
|
+
# through `::duration` and is written by `::string` in the literal
|
|
37
|
+
# grammar. The map is read by its String unit names, as `relative_date`
|
|
38
|
+
# reads one. `object_set` on a duration answers the map of its units
|
|
39
|
+
# with the key written; when that key is not a unit name, the cast
|
|
40
|
+
# writes the duration's own units, as the reference's cast does after
|
|
41
|
+
# its `object_set`. When the key is a unit name the two part: this reads
|
|
42
|
+
# the unit written (`days` set to 5 on `3d` writes `5d`, `milliseconds`
|
|
43
|
+
# set to 500 writes `3d500ms`, a unit set to a non-Integer is
|
|
44
|
+
# undefined), while the reference keeps its duration's own unit (`3d`),
|
|
45
|
+
# since its durations carry their units under keys a written String key
|
|
46
|
+
# does not replace. A String-keyed map from the context that carries
|
|
47
|
+
# the unit names is a duration here and undefined in the reference. A
|
|
48
|
+
# map whose units are not all Integers casts to undefined.
|
|
49
|
+
#
|
|
50
|
+
# The text forms `::date` and `::datetime` read are the reference's, as
|
|
51
|
+
# {IsoText} states them.
|
|
52
|
+
#
|
|
53
|
+
# Where the reference raises rather than answering, this answers
|
|
54
|
+
# undefined: an integer or a number's text too large for a finite Float
|
|
55
|
+
# (the domain holds no other) and an instant {IsoText} refuses for its
|
|
56
|
+
# year. A number's text too close to zero for any other Float reads as
|
|
57
|
+
# zero, signed as the text is, as the reference reads it.
|
|
58
|
+
module Casts
|
|
59
|
+
# The type names a cast converts to.
|
|
60
|
+
TYPE_NAMES = %w[integer float string boolean date datetime duration].freeze
|
|
61
|
+
|
|
62
|
+
# The units a map carries to be read as a duration.
|
|
63
|
+
DURATION_KEYS = %w[years months weeks days hours minutes seconds].freeze
|
|
64
|
+
|
|
65
|
+
INTEGER_TEXT = /\A-?[0-9]+\z/
|
|
66
|
+
FLOAT_TEXT = /\A-?[0-9]+(?:\.[0-9]+)?\z/
|
|
67
|
+
|
|
68
|
+
# The method that converts to each type.
|
|
69
|
+
CONVERTERS = TYPE_NAMES.to_h { |name| [name, :"cast_to_#{name}"] }.freeze
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def cast(at, type_name)
|
|
74
|
+
return insufficient("cast") if @stack.empty?
|
|
75
|
+
|
|
76
|
+
value = @stack.pop
|
|
77
|
+
push(at, UNDEFINED.equal?(value) ? UNDEFINED : send(CONVERTERS.fetch(type_name), value))
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def cast_to_integer(value)
|
|
81
|
+
case value
|
|
82
|
+
when Integer then value
|
|
83
|
+
when Float then value.truncate
|
|
84
|
+
when String then INTEGER_TEXT.match?(value) ? Integer(value, 10) : UNDEFINED
|
|
85
|
+
else UNDEFINED
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def cast_to_float(value)
|
|
90
|
+
case value
|
|
91
|
+
when Float then value
|
|
92
|
+
when Integer then value.abs < FloatText::CEILING ? value.to_f : UNDEFINED
|
|
93
|
+
when String then FLOAT_TEXT.match?(value) ? float_of_text(value) : UNDEFINED
|
|
94
|
+
else UNDEFINED
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# The Float nearest the number the text writes ({FloatText.read}), or
|
|
99
|
+
# undefined when it is too large for one.
|
|
100
|
+
def float_of_text(text) = FloatText.read(text) || UNDEFINED
|
|
101
|
+
|
|
102
|
+
def cast_to_string(value)
|
|
103
|
+
case value
|
|
104
|
+
when String then value
|
|
105
|
+
when Float then FloatText.spell(value)
|
|
106
|
+
when Integer, true, false, Predicator::Date, Predicator::DateTime then value.to_s
|
|
107
|
+
else cast_duration_text(value)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def cast_duration_text(value)
|
|
112
|
+
duration = cast_duration(value)
|
|
113
|
+
duration ? DurationText.write(duration) : UNDEFINED
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def cast_to_boolean(value)
|
|
117
|
+
case value
|
|
118
|
+
when true, false then value
|
|
119
|
+
when "true" then true
|
|
120
|
+
when "false" then false
|
|
121
|
+
else UNDEFINED
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def cast_to_date(value)
|
|
126
|
+
case value
|
|
127
|
+
when Predicator::Date then value
|
|
128
|
+
when Predicator::DateTime then Civil.date_of(value)
|
|
129
|
+
when String then IsoText.read_date(value) || UNDEFINED
|
|
130
|
+
else UNDEFINED
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def cast_to_datetime(value)
|
|
135
|
+
case value
|
|
136
|
+
when Predicator::DateTime then value
|
|
137
|
+
when Predicator::Date then Civil.instant_at(value)
|
|
138
|
+
when String then IsoText.read_datetime(value) || UNDEFINED
|
|
139
|
+
else UNDEFINED
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def cast_to_duration(value)
|
|
144
|
+
return DurationText.read(value) || UNDEFINED if value.is_a?(String)
|
|
145
|
+
|
|
146
|
+
cast_duration(value) ? value : UNDEFINED
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# A duration, or a map carrying the seven units read as one, or nil.
|
|
150
|
+
def cast_duration(value)
|
|
151
|
+
return value if value.is_a?(Duration)
|
|
152
|
+
|
|
153
|
+
as_duration(value) if value.is_a?(Hash) && DURATION_KEYS.all? { |key| value.key?(key) }
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
private_constant :Casts
|
|
157
|
+
end
|
|
158
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Predicator
|
|
4
|
+
module Evaluator
|
|
5
|
+
# The date arithmetic `add` and `subtract` reach for, as the reference
|
|
6
|
+
# states it at the tag the corpus names (its ISA, docs/isa.md, section 5,
|
|
7
|
+
# and its duration module where the ISA is silent).
|
|
8
|
+
#
|
|
9
|
+
# Two dates are apart by a duration in whole days and two datetimes by a
|
|
10
|
+
# duration in whole seconds, truncated toward zero; a date beside a
|
|
11
|
+
# datetime is read at midnight UTC. A date moved by a duration stays a
|
|
12
|
+
# date and a datetime stays a datetime. A month counts as 30 days and a
|
|
13
|
+
# year as 365, with no calendar rule and no month-end clamp: the
|
|
14
|
+
# reference's own conversion, which it calls approximate.
|
|
15
|
+
#
|
|
16
|
+
# The module holds no state.
|
|
17
|
+
module Chronology
|
|
18
|
+
MICROS_PER_SECOND = 1_000_000
|
|
19
|
+
|
|
20
|
+
# The day count of each date unit of a duration.
|
|
21
|
+
DAYS = { weeks: 7, months: 30, years: 365 }.freeze
|
|
22
|
+
|
|
23
|
+
# The second count of each time unit of a duration.
|
|
24
|
+
SECONDS = { minutes: 60, hours: 3600 }.freeze
|
|
25
|
+
|
|
26
|
+
module_function
|
|
27
|
+
|
|
28
|
+
# Whether value is a date or a datetime.
|
|
29
|
+
def chronological?(value) = value.is_a?(Predicator::Date) || value.is_a?(Predicator::DateTime)
|
|
30
|
+
|
|
31
|
+
# left minus right, both dates or datetimes, as a duration.
|
|
32
|
+
def difference(left, right)
|
|
33
|
+
if left.is_a?(Predicator::Date) && right.is_a?(Predicator::Date)
|
|
34
|
+
return Duration.new(days: Civil.days_of(left) - Civil.days_of(right))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
Duration.new(seconds: truncated_seconds(micros(left) - micros(right)))
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# anchor (a date or a datetime) moved by duration, forward when sign is
|
|
41
|
+
# 1 and back when it is -1.
|
|
42
|
+
def shift(anchor, duration, sign)
|
|
43
|
+
if anchor.is_a?(Predicator::Date)
|
|
44
|
+
return Civil.date_of_days(Civil.days_of(anchor) + (sign * whole_days(duration)))
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
moved = micros(anchor) + (sign * datetime_step(duration))
|
|
48
|
+
Predicator::DateTime.new(epoch_seconds: moved.div(MICROS_PER_SECOND), microsecond: moved % MICROS_PER_SECOND)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# The days a date moves by: the date units, then the whole days the
|
|
52
|
+
# time units add up to, truncated toward zero. Milliseconds do not move
|
|
53
|
+
# a date.
|
|
54
|
+
def whole_days(duration)
|
|
55
|
+
time = duration.seconds + SECONDS.sum { |unit, seconds| duration.public_send(unit) * seconds }
|
|
56
|
+
date_days(duration) + truncated(time, Civil::SECONDS_PER_DAY)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# The microseconds a datetime moves by. A positive milliseconds moves it
|
|
60
|
+
# by milliseconds; any other duration moves it by whole seconds, its
|
|
61
|
+
# milliseconds left out, as the reference splits the two.
|
|
62
|
+
def datetime_step(duration)
|
|
63
|
+
seconds = duration.seconds + SECONDS.sum { |unit, factor| duration.public_send(unit) * factor } +
|
|
64
|
+
(date_days(duration) * Civil::SECONDS_PER_DAY)
|
|
65
|
+
step = seconds * MICROS_PER_SECOND
|
|
66
|
+
duration.milliseconds.positive? ? step + (duration.milliseconds * 1000) : step
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def date_days(duration) = duration.days + DAYS.sum { |unit, days| duration.public_send(unit) * days }
|
|
70
|
+
|
|
71
|
+
# A date at midnight UTC, or a datetime, as microseconds since the epoch.
|
|
72
|
+
def micros(value)
|
|
73
|
+
return Civil.days_of(value) * Civil::SECONDS_PER_DAY * MICROS_PER_SECOND if value.is_a?(Predicator::Date)
|
|
74
|
+
|
|
75
|
+
(value.epoch_seconds * MICROS_PER_SECOND) + value.microsecond
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def truncated_seconds(micros) = truncated(micros, MICROS_PER_SECOND)
|
|
79
|
+
|
|
80
|
+
# amount divided by unit, truncated toward zero.
|
|
81
|
+
def truncated(amount, unit)
|
|
82
|
+
whole = amount.abs / unit
|
|
83
|
+
amount.negative? ? -whole : whole
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
private_constant :Chronology
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Predicator
|
|
4
|
+
module Evaluator
|
|
5
|
+
# The `compare` opcode's answer for one operator and one pair of values,
|
|
6
|
+
# as the reference's ISA (docs/isa.md, section 5, `compare`) states it.
|
|
7
|
+
#
|
|
8
|
+
# Strict equality is decided first and answers a boolean over every
|
|
9
|
+
# value, undefined and null included. Every other operator propagates:
|
|
10
|
+
# undefined on either side, or a pair whose types do not match, answers
|
|
11
|
+
# undefined, never an error. Null has no type peer, so every non-strict
|
|
12
|
+
# comparison involving it answers undefined too.
|
|
13
|
+
#
|
|
14
|
+
# The module holds no state.
|
|
15
|
+
module Compare
|
|
16
|
+
# The operators `compare` accepts; any other operand is an unknown
|
|
17
|
+
# instruction.
|
|
18
|
+
OPERATORS = %w[GT LT EQ GTE LTE NE STRICT_EQ STRICT_NE].freeze
|
|
19
|
+
|
|
20
|
+
# What each ordering operator makes of an order (-1, 0 or 1).
|
|
21
|
+
ORDERED = {
|
|
22
|
+
"GT" => lambda(&:positive?), "LT" => lambda(&:negative?),
|
|
23
|
+
"GTE" => ->(order) { order >= 0 }, "LTE" => ->(order) { order <= 0 }
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
# The type of each kind of value a comparison matches a pair on.
|
|
27
|
+
KINDS = {
|
|
28
|
+
Integer => :number, Float => :number, TrueClass => :boolean, FalseClass => :boolean,
|
|
29
|
+
String => :string, Array => :list, Predicator::Date => :chronological,
|
|
30
|
+
Predicator::DateTime => :chronological, Predicator::Duration => :duration, Hash => :map
|
|
31
|
+
}.freeze
|
|
32
|
+
|
|
33
|
+
module_function
|
|
34
|
+
|
|
35
|
+
# The comparison of left and right under operator: true, false or
|
|
36
|
+
# {Predicator::UNDEFINED}.
|
|
37
|
+
def call(operator, left, right)
|
|
38
|
+
case operator
|
|
39
|
+
when "STRICT_EQ" then strict?(left, right)
|
|
40
|
+
when "STRICT_NE" then !strict?(left, right)
|
|
41
|
+
else loose(operator, left, right)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def loose(operator, left, right)
|
|
46
|
+
return UNDEFINED unless peers?(left, right)
|
|
47
|
+
return loose_equal?(left, right, by_instant: false) == (operator == "EQ") if %w[EQ NE].include?(operator)
|
|
48
|
+
|
|
49
|
+
order = Order.order(left, right)
|
|
50
|
+
order.nil? ? UNDEFINED : ORDERED.fetch(operator).call(order)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# The type a comparison matches a pair on: integer and float are one
|
|
54
|
+
# type here, and a date and a datetime are one type. Null and undefined
|
|
55
|
+
# have none.
|
|
56
|
+
def kind(value) = KINDS.fetch(value.class) { KINDS.find { |type, _| value.is_a?(type) }&.last }
|
|
57
|
+
|
|
58
|
+
def peers?(left, right) = !kind(left).nil? && kind(left) == kind(right)
|
|
59
|
+
|
|
60
|
+
def undefined?(value) = UNDEFINED.equal?(value)
|
|
61
|
+
|
|
62
|
+
# Strict equality: the same member of the domain with the same value.
|
|
63
|
+
# It does not bridge integer and float, and a date is never strictly
|
|
64
|
+
# equal to a datetime. The walk is iterative, so a pair nested deeper
|
|
65
|
+
# than the stack is compared rather than exhausting it; two lists or
|
|
66
|
+
# maps that are one object are equal without a walk.
|
|
67
|
+
def strict?(left, right)
|
|
68
|
+
pairs = [[left, right]]
|
|
69
|
+
until pairs.empty?
|
|
70
|
+
a, b = pairs.pop
|
|
71
|
+
return false unless strict_pair?(a, b) { |member, other| pairs.push([member, other]) }
|
|
72
|
+
end
|
|
73
|
+
true
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# One pair of a strict walk: false when it differs, else true, having
|
|
77
|
+
# handed each member pair of two lists or two maps to the block.
|
|
78
|
+
def strict_pair?(left, right, &)
|
|
79
|
+
return right.instance_of?(left.class) && left == right unless container?(left)
|
|
80
|
+
return true if left.equal?(right)
|
|
81
|
+
|
|
82
|
+
right.is_a?(left.is_a?(Array) ? Array : Hash) && left.size == right.size && queued(left, right, &)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Hands each member pair of two lists of one size, or of two maps of
|
|
86
|
+
# one size, to the block; false when a key of the left map is missing
|
|
87
|
+
# from the right.
|
|
88
|
+
def queued(left, right)
|
|
89
|
+
if left.is_a?(Array)
|
|
90
|
+
left.each_index { |at| yield(left[at], right[at]) }
|
|
91
|
+
return true
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
left.all? { |key, member| right.key?(key) && (yield(member, right[key]) || true) }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def container?(value) = value.is_a?(Array) || value.is_a?(Hash)
|
|
98
|
+
|
|
99
|
+
# Loose equality: numbers by value across integer and float, a date
|
|
100
|
+
# and a datetime by the instant each names. Undefined equals nothing,
|
|
101
|
+
# itself included; null equals only null. Two members of a list or a
|
|
102
|
+
# map follow the member rule (member_pair?). The walk is iterative, as
|
|
103
|
+
# strict? is.
|
|
104
|
+
def loose_equal?(left, right, by_instant:)
|
|
105
|
+
pairs = [[left, right, false]]
|
|
106
|
+
until pairs.empty?
|
|
107
|
+
a, b, member = pairs.pop
|
|
108
|
+
return false unless loose_pair?(a, b, member, by_instant) { |x, y| pairs.push([x, y, true]) }
|
|
109
|
+
end
|
|
110
|
+
true
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# One pair of a loose walk: false when it differs, else true, having
|
|
114
|
+
# handed each member pair of two lists or two maps to the block.
|
|
115
|
+
def loose_pair?(left, right, member, by_instant, &)
|
|
116
|
+
verdict = member_rule(left, right, by_instant) if member
|
|
117
|
+
return verdict unless verdict.nil?
|
|
118
|
+
return left.nil? && right.nil? if absent?(left) || absent?(right)
|
|
119
|
+
|
|
120
|
+
peers?(left, right) && peers_equal?(left, right, &)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Two values of one type: equal, or their member pairs handed to the
|
|
124
|
+
# block.
|
|
125
|
+
def peers_equal?(left, right, &)
|
|
126
|
+
return instant(left) == instant(right) if kind(left) == :chronological
|
|
127
|
+
return left == right unless container?(left)
|
|
128
|
+
|
|
129
|
+
left.equal?(right) || (left.size == right.size && queued(left, right, &))
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# The member rule, or nil when it does not decide: two undefined
|
|
133
|
+
# members are the same member, and a date member and a datetime member
|
|
134
|
+
# differ whatever instants they name, unless by_instant asks for the
|
|
135
|
+
# instant alone.
|
|
136
|
+
def member_rule(left, right, by_instant)
|
|
137
|
+
return left.equal?(right) if undefined?(left) || undefined?(right)
|
|
138
|
+
|
|
139
|
+
false if !by_instant && date_against_datetime?(left, right)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def absent?(value) = value.nil? || undefined?(value)
|
|
143
|
+
|
|
144
|
+
def date_against_datetime?(left, right)
|
|
145
|
+
(left.is_a?(Predicator::Date) && right.is_a?(Predicator::DateTime)) ||
|
|
146
|
+
(left.is_a?(Predicator::DateTime) && right.is_a?(Predicator::Date))
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# A date or a datetime as the instant it names, a date at midnight UTC.
|
|
150
|
+
def instant(value)
|
|
151
|
+
return [value.epoch_seconds, value.microsecond] if value.is_a?(Predicator::DateTime)
|
|
152
|
+
|
|
153
|
+
[Civil.days_of(value) * Civil::SECONDS_PER_DAY, 0]
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
private_constant :Compare
|
|
157
|
+
end
|
|
158
|
+
end
|