eyeling 1.12.6 → 1.12.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/HANDBOOK.md +18 -13
- package/examples/bayes-therapy.n3 +264 -0
- package/examples/output/bayes-therapy.n3 +26 -0
- package/eyeling-builtins.ttl +3 -3
- package/eyeling.js +61 -6
- package/lib/builtins.js +61 -6
- package/package.json +1 -1
- package/test/examples.test.js +4 -1
package/HANDBOOK.md
CHANGED
|
@@ -893,6 +893,12 @@ These are “function-like” relations where the subject is usually a list and
|
|
|
893
893
|
- Computes the numeric sum.
|
|
894
894
|
- Chooses an output datatype based on the “widest” numeric datatype seen among inputs and (optionally) the object position; integers stay integers unless the result is non-integer.
|
|
895
895
|
|
|
896
|
+
Eyeling also supports a small, EYE-style convenience for timestamp arithmetic:
|
|
897
|
+
|
|
898
|
+
- **DateTime plus duration/seconds**: `(dateTime durationOrSeconds) math:sum dateTime`
|
|
899
|
+
- `xsd:duration` is interpreted as seconds (same model as `math:difference`).
|
|
900
|
+
- Output is a normalized `xsd:dateTime` in UTC lexical form (`...Z`).
|
|
901
|
+
|
|
896
902
|
#### `math:product`
|
|
897
903
|
|
|
898
904
|
**Shape:** `( $x1 $x2 ... ) math:product $total`
|
|
@@ -911,9 +917,10 @@ Eyeling supports:
|
|
|
911
917
|
|
|
912
918
|
1. **Numeric subtraction**: `c = a - b`.
|
|
913
919
|
2. **DateTime difference**: `(dateTime1 dateTime2) math:difference duration`
|
|
914
|
-
- Produces an
|
|
920
|
+
- Produces an **`xsd:duration`** in a seconds-only lexical form such as `"PT900S"^^xsd:duration`.
|
|
921
|
+
- This avoids ambiguity around month/year day-length and still plays well with `math:lessThan`, `math:greaterThan`, etc. because Eyeling's numeric comparison builtins treat `xsd:duration` as seconds.
|
|
915
922
|
|
|
916
|
-
3. **DateTime minus duration**: `(dateTime
|
|
923
|
+
3. **DateTime minus duration**: `(dateTime durationOrSeconds) math:difference dateTime`
|
|
917
924
|
- Subtracts a duration from a dateTime and yields a new dateTime.
|
|
918
925
|
|
|
919
926
|
If the types don’t fit any supported case, the builtin fails.
|
|
@@ -1963,21 +1970,19 @@ In that sense, N3 is less a bid to make the web “smarter” than a bid to make
|
|
|
1963
1970
|
|
|
1964
1971
|
<a id="app-c"></a>
|
|
1965
1972
|
|
|
1966
|
-
## Appendix C — N3 beyond Prolog: logic
|
|
1967
|
-
|
|
1968
|
-
At first glance, an N3 rule set can feel familiar if you’ve used Prolog: variables, unification, and rules that read like “if this pattern holds, then that pattern follows.” But N3 is not just “logic programming with a different syntax.” It is logic shaped for a different environment: not a single program with a single database, but a world of distributed graphs that can be published, merged, and cited across boundaries.
|
|
1973
|
+
## Appendix C — N3 beyond Prolog: logic for RDF-style graphs
|
|
1969
1974
|
|
|
1970
|
-
|
|
1975
|
+
Notation3 (N3) rule sets often look similar to Prolog at the surface: they use variables, unification, and implication-style rules (“if these patterns match, then these patterns follow”). N3 is typically used in a different setting, though: instead of a single program operating over a single local database, N3 rules and data are commonly written as documents that can be published, shared, merged, and referenced across systems.
|
|
1971
1976
|
|
|
1972
|
-
|
|
1977
|
+
In practice, that setting is reflected in several common features of N3-style rule writing:
|
|
1973
1978
|
|
|
1974
|
-
- **
|
|
1975
|
-
- **
|
|
1976
|
-
- **
|
|
1977
|
-
- **Rules can be
|
|
1978
|
-
- **
|
|
1979
|
+
- **Identifiers are IRIs.** Terms are usually global identifiers rather than local symbols, which supports linking across datasets.
|
|
1980
|
+
- **Input and output are graphs.** Rules consume graph patterns and produce additional triples, so the result of inference can be represented in the same form as the input data.
|
|
1981
|
+
- **Quoted graphs allow statements-as-data.** N3 can treat a graph (a set of triples) as a term, which makes it possible to represent and reason about assertions (e.g., “this source says …” or “this formula implies …”) as data.
|
|
1982
|
+
- **Rules can be distributed as text artifacts.** Rules can live alongside data, be versioned, and be reused without requiring an external host language to “carry” the meaning.
|
|
1983
|
+
- **Built-ins cover common computations.** Many N3 workflows rely on built-ins for operations such as string handling, list processing, comparisons, and related utilities; some workflows also use IRIs as pointers to retrievable content.
|
|
1979
1984
|
|
|
1980
|
-
|
|
1985
|
+
Engines can combine execution styles in different ways. One common pattern is to use a Prolog-like backward-chaining prover to satisfy rule bodies, while still using forward chaining to add the instantiated conclusions to the fact set until no new facts are produced.
|
|
1981
1986
|
|
|
1982
1987
|
---
|
|
1983
1988
|
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# ============================================================================
|
|
2
|
+
# Bayes Therapy Decision Support
|
|
3
|
+
#
|
|
4
|
+
# This example extends a tiny Bayesian diagnostic model (Disease -> Symptoms)
|
|
5
|
+
# with a decision layer that scores therapies by expected utility.
|
|
6
|
+
#
|
|
7
|
+
# NOTE: All probabilities and weights are illustrative and not medical advice.
|
|
8
|
+
# ============================================================================
|
|
9
|
+
|
|
10
|
+
@prefix : <https://example.org/diag#> .
|
|
11
|
+
@prefix math: <http://www.w3.org/2000/10/swap/math#> .
|
|
12
|
+
@prefix list: <http://www.w3.org/2000/10/swap/list#> .
|
|
13
|
+
|
|
14
|
+
# -------------------------------------------
|
|
15
|
+
# 1) MODEL: Disease -> Symptoms (Naive Bayes)
|
|
16
|
+
# -------------------------------------------
|
|
17
|
+
|
|
18
|
+
:COVID19 a :Disease; :prior 0.05 .
|
|
19
|
+
:Influenza a :Disease; :prior 0.03 .
|
|
20
|
+
:AllergicRhinitis a :Disease; :prior 0.10 .
|
|
21
|
+
:BacterialPneumonia a :Disease; :prior 0.01 .
|
|
22
|
+
|
|
23
|
+
# Conditional probabilities P(symptom | disease)
|
|
24
|
+
:COVID19 :pGiven [ :symptom :Fever; :p 0.70 ] .
|
|
25
|
+
:COVID19 :pGiven [ :symptom :DryCough; :p 0.65 ] .
|
|
26
|
+
:COVID19 :pGiven [ :symptom :LossOfSmell; :p 0.40 ] .
|
|
27
|
+
:COVID19 :pGiven [ :symptom :Sneezing; :p 0.15 ] .
|
|
28
|
+
:COVID19 :pGiven [ :symptom :ShortBreath; :p 0.20 ] .
|
|
29
|
+
|
|
30
|
+
:Influenza :pGiven [ :symptom :Fever; :p 0.80 ] .
|
|
31
|
+
:Influenza :pGiven [ :symptom :DryCough; :p 0.50 ] .
|
|
32
|
+
:Influenza :pGiven [ :symptom :LossOfSmell; :p 0.05 ] .
|
|
33
|
+
:Influenza :pGiven [ :symptom :Sneezing; :p 0.20 ] .
|
|
34
|
+
:Influenza :pGiven [ :symptom :ShortBreath; :p 0.10 ] .
|
|
35
|
+
|
|
36
|
+
:AllergicRhinitis :pGiven [ :symptom :Fever; :p 0.05 ] .
|
|
37
|
+
:AllergicRhinitis :pGiven [ :symptom :DryCough; :p 0.15 ] .
|
|
38
|
+
:AllergicRhinitis :pGiven [ :symptom :LossOfSmell; :p 0.10 ] .
|
|
39
|
+
:AllergicRhinitis :pGiven [ :symptom :Sneezing; :p 0.80 ] .
|
|
40
|
+
:AllergicRhinitis :pGiven [ :symptom :ShortBreath; :p 0.05 ] .
|
|
41
|
+
|
|
42
|
+
:BacterialPneumonia :pGiven [ :symptom :Fever; :p 0.70 ] .
|
|
43
|
+
:BacterialPneumonia :pGiven [ :symptom :DryCough; :p 0.60 ] .
|
|
44
|
+
:BacterialPneumonia :pGiven [ :symptom :LossOfSmell; :p 0.02 ] .
|
|
45
|
+
:BacterialPneumonia :pGiven [ :symptom :Sneezing; :p 0.05 ] .
|
|
46
|
+
:BacterialPneumonia :pGiven [ :symptom :ShortBreath; :p 0.60 ] .
|
|
47
|
+
|
|
48
|
+
# ----------------------------------------------------------------------
|
|
49
|
+
# 2) THERAPY MODEL: P(improve | disease, therapy) + P(adverse | therapy)
|
|
50
|
+
# ----------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
:Paxlovid a :Therapy;
|
|
53
|
+
# aligned with :Case :diseases order
|
|
54
|
+
:successByDisease ( 0.75 0.05 0.02 0.05 );
|
|
55
|
+
:adverse 0.10 .
|
|
56
|
+
|
|
57
|
+
:Oseltamivir a :Therapy;
|
|
58
|
+
:successByDisease ( 0.05 0.60 0.02 0.05 );
|
|
59
|
+
:adverse 0.08 .
|
|
60
|
+
|
|
61
|
+
:Antihistamine a :Therapy;
|
|
62
|
+
:successByDisease ( 0.10 0.10 0.75 0.05 );
|
|
63
|
+
:adverse 0.03 .
|
|
64
|
+
|
|
65
|
+
:Antibiotic a :Therapy;
|
|
66
|
+
:successByDisease ( 0.05 0.05 0.02 0.80 );
|
|
67
|
+
:adverse 0.07 .
|
|
68
|
+
|
|
69
|
+
:SupportiveCare a :Therapy;
|
|
70
|
+
:successByDisease ( 0.30 0.30 0.25 0.20 );
|
|
71
|
+
:adverse 0.01 .
|
|
72
|
+
|
|
73
|
+
:Model :benefitWeight 10; :harmWeight 3 .
|
|
74
|
+
|
|
75
|
+
# -------------------------------------------
|
|
76
|
+
# 3) CASE: evidence (symptoms present/absent)
|
|
77
|
+
# -------------------------------------------
|
|
78
|
+
|
|
79
|
+
:Case a :PatientCase;
|
|
80
|
+
:diseases ( :COVID19 :Influenza :AllergicRhinitis :BacterialPneumonia );
|
|
81
|
+
:therapies ( :Paxlovid :Oseltamivir :SupportiveCare :Antibiotic :Antihistamine );
|
|
82
|
+
:evidence (
|
|
83
|
+
[ :symptom :Fever; :present true ]
|
|
84
|
+
[ :symptom :DryCough; :present true ]
|
|
85
|
+
[ :symptom :LossOfSmell; :present false ]
|
|
86
|
+
[ :symptom :Sneezing; :present false ]
|
|
87
|
+
[ :symptom :ShortBreath; :present false ]
|
|
88
|
+
).
|
|
89
|
+
|
|
90
|
+
# -----------------------------------------------------------
|
|
91
|
+
# 4) GUARDS (inference fuses): probabilities must be in [0,1]
|
|
92
|
+
# -----------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
{ ?d :prior ?p. ?p math:lessThan 0. } => false.
|
|
95
|
+
{ ?d :prior ?p. ?p math:greaterThan 1. } => false.
|
|
96
|
+
|
|
97
|
+
{ ?d :pGiven [ :p ?p ]. ?p math:lessThan 0. } => false.
|
|
98
|
+
{ ?d :pGiven [ :p ?p ]. ?p math:greaterThan 1. } => false.
|
|
99
|
+
|
|
100
|
+
{ ?t a :Therapy. ?t :adverse ?p. ?p math:lessThan 0. } => false.
|
|
101
|
+
{ ?t a :Therapy. ?t :adverse ?p. ?p math:greaterThan 1. } => false.
|
|
102
|
+
|
|
103
|
+
{ ?t a :Therapy. ?t :successByDisease ?ps.
|
|
104
|
+
?ps list:iterate ( ?i ?p ).
|
|
105
|
+
?p math:lessThan 0.
|
|
106
|
+
} => false.
|
|
107
|
+
|
|
108
|
+
{ ?t a :Therapy. ?t :successByDisease ?ps.
|
|
109
|
+
?ps list:iterate ( ?i ?p ).
|
|
110
|
+
?p math:greaterThan 1.
|
|
111
|
+
} => false.
|
|
112
|
+
|
|
113
|
+
# ------------------------------------------
|
|
114
|
+
# 5) HELPERS (all written as backward rules)
|
|
115
|
+
# ------------------------------------------
|
|
116
|
+
|
|
117
|
+
# pairList(d, (x1 x2 ...)) -> ((d x1) (d x2) ...)
|
|
118
|
+
{ ( ?d () ) :pairList () } <= true.
|
|
119
|
+
|
|
120
|
+
{ ( ?d ?xs ) :pairList ?pairs } <= {
|
|
121
|
+
?xs list:firstRest ( ?x ?rest ).
|
|
122
|
+
( ?d ?rest ) :pairList ?tailPairs.
|
|
123
|
+
?pairs list:firstRest ( ( ?d ?x ) ?tailPairs ).
|
|
124
|
+
}.
|
|
125
|
+
|
|
126
|
+
# factor(d, evidenceItem) -> probability factor for that symptom
|
|
127
|
+
{ ( ?d ?ev ) :factor ?p } <= {
|
|
128
|
+
?ev :symptom ?s.
|
|
129
|
+
?ev :present true.
|
|
130
|
+
?d :pGiven [ :symptom ?s; :p ?p ].
|
|
131
|
+
}.
|
|
132
|
+
|
|
133
|
+
{ ( ?d ?ev ) :factor ?q } <= {
|
|
134
|
+
?ev :symptom ?s.
|
|
135
|
+
?ev :present false.
|
|
136
|
+
?d :pGiven [ :symptom ?s; :p ?p ].
|
|
137
|
+
( 1 ?p ) math:difference ?q.
|
|
138
|
+
}.
|
|
139
|
+
|
|
140
|
+
# pairWithConst((x1 x2 ...), c) -> ((x1 c) (x2 c) ...)
|
|
141
|
+
{ ( () ?c ) :pairWithConst () } <= true.
|
|
142
|
+
|
|
143
|
+
{ ( ?xs ?c ) :pairWithConst ?pairs } <= {
|
|
144
|
+
?xs list:firstRest ( ?x ?rest ).
|
|
145
|
+
( ?rest ?c ) :pairWithConst ?tailPairs.
|
|
146
|
+
?pairs list:firstRest ( ( ?x ?c ) ?tailPairs ).
|
|
147
|
+
}.
|
|
148
|
+
|
|
149
|
+
# zip((a1 a2 ...), (b1 b2 ...)) -> ((a1 b1) (a2 b2) ...)
|
|
150
|
+
{ ( () () ) :zip () } <= true.
|
|
151
|
+
|
|
152
|
+
{ ( ?as ?bs ) :zip ?pairs } <= {
|
|
153
|
+
?as list:firstRest ( ?a ?arest ).
|
|
154
|
+
?bs list:firstRest ( ?b ?brest ).
|
|
155
|
+
( ?arest ?brest ) :zip ?tailPairs.
|
|
156
|
+
?pairs list:firstRest ( ( ?a ?b ) ?tailPairs ).
|
|
157
|
+
}.
|
|
158
|
+
|
|
159
|
+
# Helpers for list:map
|
|
160
|
+
{ ?pair :mul ?p } <= { ?pair math:product ?p }.
|
|
161
|
+
{ ?pair :quot2 ?q } <= { ?pair math:quotient ?q }.
|
|
162
|
+
|
|
163
|
+
# ---------------------------------------------------------
|
|
164
|
+
# 6) DIAGNOSIS: unnormalized scores + normalized posteriors
|
|
165
|
+
# ---------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
# score(d) = prior(d) * Π_e factor(d,e)
|
|
168
|
+
{ ?d :scoreFor ?score } <= {
|
|
169
|
+
?d :prior ?prior.
|
|
170
|
+
:Case :evidence ?evs.
|
|
171
|
+
( ?d ?evs ) :pairList ?pairs.
|
|
172
|
+
( ?pairs :factor ) list:map ?factors.
|
|
173
|
+
?factors math:product ?likelihood.
|
|
174
|
+
( ?prior ?likelihood ) math:product ?score.
|
|
175
|
+
}.
|
|
176
|
+
|
|
177
|
+
# Compute the score list, total evidence, and posterior list once.
|
|
178
|
+
{
|
|
179
|
+
:Case :diseases ?ds.
|
|
180
|
+
( ?ds :scoreFor ) list:map ?scores.
|
|
181
|
+
?scores math:sum ?total.
|
|
182
|
+
|
|
183
|
+
( ?scores ?total ) :pairWithConst ?scorePairs.
|
|
184
|
+
( ?scorePairs :quot2 ) list:map ?posteriors.
|
|
185
|
+
} => {
|
|
186
|
+
:Case :scores ?scores;
|
|
187
|
+
:evidenceTotal ?total;
|
|
188
|
+
:posteriors ?posteriors.
|
|
189
|
+
}.
|
|
190
|
+
|
|
191
|
+
# Attach each disease posterior to the disease term (no blank nodes in output)
|
|
192
|
+
{
|
|
193
|
+
:Case :diseases ?ds.
|
|
194
|
+
:Case :posteriors ?posts.
|
|
195
|
+
|
|
196
|
+
?ds list:iterate ( ?i ?d ).
|
|
197
|
+
( ?posts ?i ) list:memberAt ?p.
|
|
198
|
+
} => {
|
|
199
|
+
?d :posterior ?p.
|
|
200
|
+
}.
|
|
201
|
+
|
|
202
|
+
# -------------------------------------------------------
|
|
203
|
+
# 7) THERAPY SCORING: expected success + expected utility
|
|
204
|
+
# -------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
# expectedSuccess(t) = Σ_i post[i] * successByDisease[i]
|
|
207
|
+
{
|
|
208
|
+
:Case :posteriors ?posts.
|
|
209
|
+
?t a :Therapy.
|
|
210
|
+
?t :successByDisease ?succ.
|
|
211
|
+
|
|
212
|
+
( ?posts ?succ ) :zip ?pairs.
|
|
213
|
+
( ?pairs :mul ) list:map ?terms.
|
|
214
|
+
?terms math:sum ?expectedSuccess.
|
|
215
|
+
|
|
216
|
+
?t :adverse ?adverse.
|
|
217
|
+
:Model :benefitWeight ?bw.
|
|
218
|
+
:Model :harmWeight ?hw.
|
|
219
|
+
|
|
220
|
+
( ?bw ?expectedSuccess ) math:product ?benefit.
|
|
221
|
+
( ?hw ?adverse ) math:product ?harmCost.
|
|
222
|
+
( ?benefit ?harmCost ) math:difference ?utility.
|
|
223
|
+
} => {
|
|
224
|
+
?t :expectedSuccess ?expectedSuccess;
|
|
225
|
+
:expectedAdverse ?adverse;
|
|
226
|
+
:utility ?utility.
|
|
227
|
+
}.
|
|
228
|
+
|
|
229
|
+
# --------------------------------------------------------------
|
|
230
|
+
# 8) RECOMMENDATION: pick max-utility therapy from the case list
|
|
231
|
+
# --------------------------------------------------------------
|
|
232
|
+
|
|
233
|
+
# betterOf(t1,t2) chooses t1 if utility(t1) >= utility(t2), else t2.
|
|
234
|
+
{ ( ?t1 ?t2 ) :betterOf ?t1 } <= {
|
|
235
|
+
?t1 :utility ?u1.
|
|
236
|
+
?t2 :utility ?u2.
|
|
237
|
+
?u1 math:notLessThan ?u2.
|
|
238
|
+
}.
|
|
239
|
+
{ ( ?t1 ?t2 ) :betterOf ?t2 } <= {
|
|
240
|
+
?t1 :utility ?u1.
|
|
241
|
+
?t2 :utility ?u2.
|
|
242
|
+
?u1 math:lessThan ?u2.
|
|
243
|
+
}.
|
|
244
|
+
|
|
245
|
+
# bestTherapy((t)) = t
|
|
246
|
+
{ ( ?ts ) :bestTherapy ?t } <= {
|
|
247
|
+
?ts list:firstRest ( ?t () ).
|
|
248
|
+
}.
|
|
249
|
+
|
|
250
|
+
# bestTherapy((head rest...)) = betterOf(head, bestTherapy(rest))
|
|
251
|
+
{ ( ?ts ) :bestTherapy ?best } <= {
|
|
252
|
+
?ts list:firstRest ( ?head ?rest ).
|
|
253
|
+
?rest list:firstRest ( ?_ ?__ ). # ensure rest is non-empty
|
|
254
|
+
( ?rest ) :bestTherapy ?bestRest.
|
|
255
|
+
( ?head ?bestRest ) :betterOf ?best.
|
|
256
|
+
}.
|
|
257
|
+
|
|
258
|
+
# Emit a single recommendation for this case.
|
|
259
|
+
{
|
|
260
|
+
:Case :therapies ?ts.
|
|
261
|
+
( ?ts ) :bestTherapy ?best.
|
|
262
|
+
} => {
|
|
263
|
+
:Case :recommendedTherapy ?best.
|
|
264
|
+
}.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
@prefix : <https://example.org/diag#> .
|
|
2
|
+
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
|
|
3
|
+
|
|
4
|
+
:Case :scores ("0.009281999999999999"^^xsd:decimal "0.008208000000000002"^^xsd:decimal "0.00012824999999999997"^^xsd:decimal "0.00156408"^^xsd:decimal) .
|
|
5
|
+
:Case :evidenceTotal "0.019182329999999997"^^xsd:decimal .
|
|
6
|
+
:Case :posteriors ("0.48388282341092037"^^xsd:decimal "0.4278937960091398"^^xsd:decimal "0.006685840562642807"^^xsd:decimal "0.08153754001729718"^^xsd:decimal) .
|
|
7
|
+
:COVID19 :posterior "0.48388282341092037"^^xsd:decimal .
|
|
8
|
+
:Influenza :posterior "0.4278937960091398"^^xsd:decimal .
|
|
9
|
+
:AllergicRhinitis :posterior "0.006685840562642807"^^xsd:decimal .
|
|
10
|
+
:BacterialPneumonia :posterior "0.08153754001729718"^^xsd:decimal .
|
|
11
|
+
:Paxlovid :expectedSuccess "0.388517401170765"^^xsd:decimal .
|
|
12
|
+
:Paxlovid :expectedAdverse 0.10 .
|
|
13
|
+
:Paxlovid :utility "3.5851740117076503"^^xsd:decimal .
|
|
14
|
+
:Oseltamivir :expectedSuccess "0.28514101258814756"^^xsd:decimal .
|
|
15
|
+
:Oseltamivir :expectedAdverse 0.08 .
|
|
16
|
+
:Oseltamivir :utility "2.6114101258814753"^^xsd:decimal .
|
|
17
|
+
:Antihistamine :expectedSuccess "0.10026891936485298"^^xsd:decimal .
|
|
18
|
+
:Antihistamine :expectedAdverse 0.03 .
|
|
19
|
+
:Antihistamine :utility "0.9126891936485299"^^xsd:decimal .
|
|
20
|
+
:Antibiotic :expectedSuccess "0.1109525797960936"^^xsd:decimal .
|
|
21
|
+
:Antibiotic :expectedAdverse 0.07 .
|
|
22
|
+
:Antibiotic :utility "0.8995257979609361"^^xsd:decimal .
|
|
23
|
+
:SupportiveCare :expectedSuccess "0.2915119539701382"^^xsd:decimal .
|
|
24
|
+
:SupportiveCare :expectedAdverse 0.01 .
|
|
25
|
+
:SupportiveCare :utility "2.8851195397013822"^^xsd:decimal .
|
|
26
|
+
:Case :recommendedTherapy :Paxlovid .
|
package/eyeling-builtins.ttl
CHANGED
|
@@ -77,13 +77,13 @@ math:notGreaterThan a ex:Builtin ; ex:kind ex:Test;
|
|
|
77
77
|
# --- math: arithmetic / numeric functions ----------------------------
|
|
78
78
|
|
|
79
79
|
math:sum a ex:Builtin ; ex:kind ex:Function ;
|
|
80
|
-
rdfs:comment "Sum of a list of
|
|
80
|
+
rdfs:comment "Sum of a list of numeric terms. Binds/unifies object with the total (integer mode uses BigInt when possible). Also supports 2-element timestamp arithmetic: (xsd:dateTime xsd:duration) or (xsd:dateTime seconds) (and the commuted forms) -> xsd:dateTime." .
|
|
81
81
|
|
|
82
82
|
math:product a ex:Builtin ; ex:kind ex:Function ;
|
|
83
|
-
rdfs:comment "Product of a list of
|
|
83
|
+
rdfs:comment "Product of a list of numeric terms. Binds/unifies object with the product (integer mode uses BigInt when possible)." .
|
|
84
84
|
|
|
85
85
|
math:difference a ex:Builtin ; ex:kind ex:Function ;
|
|
86
|
-
rdfs:comment "Difference of a 2-element list (a b). Supports
|
|
86
|
+
rdfs:comment "Difference of a 2-element list (a b). Supports xsd:dateTime-xsd:dateTime -> xsd:duration (normalized to PT...S); xsd:dateTime-(xsd:duration|seconds) -> xsd:dateTime; integer BigInt; otherwise numeric a-b." .
|
|
87
87
|
|
|
88
88
|
math:quotient a ex:Builtin ; ex:kind ex:Function ;
|
|
89
89
|
rdfs:comment "Quotient of a 2-element list (a b). Binds/unifies object with a/b (guards division by zero and non-finite results)." .
|
package/eyeling.js
CHANGED
|
@@ -788,9 +788,14 @@ function parseNumOrDuration(t) {
|
|
|
788
788
|
}
|
|
789
789
|
|
|
790
790
|
function formatDurationLiteralFromSeconds(secs) {
|
|
791
|
+
// xsd:duration allows a leading '-' sign.
|
|
792
|
+
// We emit a conservative seconds-only lexical form so we don't lose precision
|
|
793
|
+
// for sub-day differences (e.g., PT900S).
|
|
791
794
|
const neg = secs < 0;
|
|
792
|
-
const
|
|
793
|
-
const
|
|
795
|
+
const abs = Math.abs(secs);
|
|
796
|
+
const sLex = Number.isFinite(abs) ? (Number.isInteger(abs) && abs < 1e21 ? abs.toFixed(0) : String(abs)) : 'NaN';
|
|
797
|
+
const core = `P${abs === 0 ? 'T0S' : `T${sLex}S`}`;
|
|
798
|
+
const literalLex = neg ? `"-${core}"` : `"${core}"`;
|
|
794
799
|
return internLiteral(`${literalLex}^^<${XSD_NS}duration>`);
|
|
795
800
|
}
|
|
796
801
|
function numEqualTerm(t, n, eps = 1e-9) {
|
|
@@ -1433,6 +1438,49 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
|
|
|
1433
1438
|
if (!(g.s instanceof ListTerm)) return [];
|
|
1434
1439
|
const xs = g.s.elems;
|
|
1435
1440
|
|
|
1441
|
+
// Special-case: (dateTime durationOrSeconds) math:sum dateTime
|
|
1442
|
+
// This mirrors EYE-style convenience for timestamp arithmetic.
|
|
1443
|
+
//
|
|
1444
|
+
// Notes:
|
|
1445
|
+
// - We treat xsd:duration as seconds via parseNumOrDuration (same model as math:difference).
|
|
1446
|
+
// - Output is normalized to UTC lexical form ("...Z"), consistent with other dateTime outputs.
|
|
1447
|
+
if (xs.length === 2) {
|
|
1448
|
+
const dt0 = parseDatetimeLike(xs[0]);
|
|
1449
|
+
const dt1 = parseDatetimeLike(xs[1]);
|
|
1450
|
+
|
|
1451
|
+
if (dt0 !== null && dt1 === null) {
|
|
1452
|
+
const secs = parseNumOrDuration(xs[1]);
|
|
1453
|
+
if (secs !== null) {
|
|
1454
|
+
const outSecs = dt0.getTime() / 1000.0 + secs;
|
|
1455
|
+
const lex = time.utcIsoDateTimeStringFromEpochSeconds(outSecs);
|
|
1456
|
+
const lit = internLiteral(`"${lex}"^^<${XSD_NS}dateTime>`);
|
|
1457
|
+
if (g.o instanceof Var) {
|
|
1458
|
+
const s2 = { ...subst };
|
|
1459
|
+
s2[g.o.name] = lit;
|
|
1460
|
+
return [s2];
|
|
1461
|
+
}
|
|
1462
|
+
const s2 = unifyTerm(g.o, lit, subst);
|
|
1463
|
+
return s2 !== null ? [s2] : [];
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
if (dt1 !== null && dt0 === null) {
|
|
1468
|
+
const secs = parseNumOrDuration(xs[0]);
|
|
1469
|
+
if (secs !== null) {
|
|
1470
|
+
const outSecs = dt1.getTime() / 1000.0 + secs;
|
|
1471
|
+
const lex = time.utcIsoDateTimeStringFromEpochSeconds(outSecs);
|
|
1472
|
+
const lit = internLiteral(`"${lex}"^^<${XSD_NS}dateTime>`);
|
|
1473
|
+
if (g.o instanceof Var) {
|
|
1474
|
+
const s2 = { ...subst };
|
|
1475
|
+
s2[g.o.name] = lit;
|
|
1476
|
+
return [s2];
|
|
1477
|
+
}
|
|
1478
|
+
const s2 = unifyTerm(g.o, lit, subst);
|
|
1479
|
+
return s2 !== null ? [s2] : [];
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1436
1484
|
const dtOut0 = commonNumericDatatype(xs, g.o);
|
|
1437
1485
|
|
|
1438
1486
|
// Exact integer mode
|
|
@@ -1540,18 +1588,25 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
|
|
|
1540
1588
|
if (!(g.s instanceof ListTerm) || g.s.elems.length !== 2) return [];
|
|
1541
1589
|
const [a0, b0] = g.s.elems;
|
|
1542
1590
|
|
|
1543
|
-
// 1) Date/datetime difference -> duration
|
|
1591
|
+
// 1) Date/datetime difference -> xsd:duration
|
|
1592
|
+
//
|
|
1593
|
+
// Emit a conservative seconds-only lexical form (e.g., "PT900S"^^xsd:duration).
|
|
1594
|
+
// This avoids day/month/year normalization ambiguity and still allows numeric
|
|
1595
|
+
// comparisons via parseNumOrDuration (used by math:lessThan, etc.).
|
|
1544
1596
|
const aDt = parseDatetimeLike(a0);
|
|
1545
1597
|
const bDt = parseDatetimeLike(b0);
|
|
1546
1598
|
if (aDt !== null && bDt !== null) {
|
|
1547
1599
|
const diffSecs = (aDt.getTime() - bDt.getTime()) / 1000.0;
|
|
1548
|
-
|
|
1600
|
+
if (!Number.isFinite(diffSecs)) return [];
|
|
1601
|
+
|
|
1602
|
+
const lit = formatDurationLiteralFromSeconds(diffSecs);
|
|
1603
|
+
|
|
1549
1604
|
if (g.o instanceof Var) {
|
|
1550
1605
|
const s2 = { ...subst };
|
|
1551
|
-
s2[g.o.name] =
|
|
1606
|
+
s2[g.o.name] = lit;
|
|
1552
1607
|
return [s2];
|
|
1553
1608
|
}
|
|
1554
|
-
const s2 = unifyTerm(g.o,
|
|
1609
|
+
const s2 = unifyTerm(g.o, lit, subst);
|
|
1555
1610
|
return s2 !== null ? [s2] : [];
|
|
1556
1611
|
}
|
|
1557
1612
|
|
package/lib/builtins.js
CHANGED
|
@@ -776,9 +776,14 @@ function parseNumOrDuration(t) {
|
|
|
776
776
|
}
|
|
777
777
|
|
|
778
778
|
function formatDurationLiteralFromSeconds(secs) {
|
|
779
|
+
// xsd:duration allows a leading '-' sign.
|
|
780
|
+
// We emit a conservative seconds-only lexical form so we don't lose precision
|
|
781
|
+
// for sub-day differences (e.g., PT900S).
|
|
779
782
|
const neg = secs < 0;
|
|
780
|
-
const
|
|
781
|
-
const
|
|
783
|
+
const abs = Math.abs(secs);
|
|
784
|
+
const sLex = Number.isFinite(abs) ? (Number.isInteger(abs) && abs < 1e21 ? abs.toFixed(0) : String(abs)) : 'NaN';
|
|
785
|
+
const core = `P${abs === 0 ? 'T0S' : `T${sLex}S`}`;
|
|
786
|
+
const literalLex = neg ? `"-${core}"` : `"${core}"`;
|
|
782
787
|
return internLiteral(`${literalLex}^^<${XSD_NS}duration>`);
|
|
783
788
|
}
|
|
784
789
|
function numEqualTerm(t, n, eps = 1e-9) {
|
|
@@ -1421,6 +1426,49 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
|
|
|
1421
1426
|
if (!(g.s instanceof ListTerm)) return [];
|
|
1422
1427
|
const xs = g.s.elems;
|
|
1423
1428
|
|
|
1429
|
+
// Special-case: (dateTime durationOrSeconds) math:sum dateTime
|
|
1430
|
+
// This mirrors EYE-style convenience for timestamp arithmetic.
|
|
1431
|
+
//
|
|
1432
|
+
// Notes:
|
|
1433
|
+
// - We treat xsd:duration as seconds via parseNumOrDuration (same model as math:difference).
|
|
1434
|
+
// - Output is normalized to UTC lexical form ("...Z"), consistent with other dateTime outputs.
|
|
1435
|
+
if (xs.length === 2) {
|
|
1436
|
+
const dt0 = parseDatetimeLike(xs[0]);
|
|
1437
|
+
const dt1 = parseDatetimeLike(xs[1]);
|
|
1438
|
+
|
|
1439
|
+
if (dt0 !== null && dt1 === null) {
|
|
1440
|
+
const secs = parseNumOrDuration(xs[1]);
|
|
1441
|
+
if (secs !== null) {
|
|
1442
|
+
const outSecs = dt0.getTime() / 1000.0 + secs;
|
|
1443
|
+
const lex = time.utcIsoDateTimeStringFromEpochSeconds(outSecs);
|
|
1444
|
+
const lit = internLiteral(`"${lex}"^^<${XSD_NS}dateTime>`);
|
|
1445
|
+
if (g.o instanceof Var) {
|
|
1446
|
+
const s2 = { ...subst };
|
|
1447
|
+
s2[g.o.name] = lit;
|
|
1448
|
+
return [s2];
|
|
1449
|
+
}
|
|
1450
|
+
const s2 = unifyTerm(g.o, lit, subst);
|
|
1451
|
+
return s2 !== null ? [s2] : [];
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
if (dt1 !== null && dt0 === null) {
|
|
1456
|
+
const secs = parseNumOrDuration(xs[0]);
|
|
1457
|
+
if (secs !== null) {
|
|
1458
|
+
const outSecs = dt1.getTime() / 1000.0 + secs;
|
|
1459
|
+
const lex = time.utcIsoDateTimeStringFromEpochSeconds(outSecs);
|
|
1460
|
+
const lit = internLiteral(`"${lex}"^^<${XSD_NS}dateTime>`);
|
|
1461
|
+
if (g.o instanceof Var) {
|
|
1462
|
+
const s2 = { ...subst };
|
|
1463
|
+
s2[g.o.name] = lit;
|
|
1464
|
+
return [s2];
|
|
1465
|
+
}
|
|
1466
|
+
const s2 = unifyTerm(g.o, lit, subst);
|
|
1467
|
+
return s2 !== null ? [s2] : [];
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1424
1472
|
const dtOut0 = commonNumericDatatype(xs, g.o);
|
|
1425
1473
|
|
|
1426
1474
|
// Exact integer mode
|
|
@@ -1528,18 +1576,25 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
|
|
|
1528
1576
|
if (!(g.s instanceof ListTerm) || g.s.elems.length !== 2) return [];
|
|
1529
1577
|
const [a0, b0] = g.s.elems;
|
|
1530
1578
|
|
|
1531
|
-
// 1) Date/datetime difference -> duration
|
|
1579
|
+
// 1) Date/datetime difference -> xsd:duration
|
|
1580
|
+
//
|
|
1581
|
+
// Emit a conservative seconds-only lexical form (e.g., "PT900S"^^xsd:duration).
|
|
1582
|
+
// This avoids day/month/year normalization ambiguity and still allows numeric
|
|
1583
|
+
// comparisons via parseNumOrDuration (used by math:lessThan, etc.).
|
|
1532
1584
|
const aDt = parseDatetimeLike(a0);
|
|
1533
1585
|
const bDt = parseDatetimeLike(b0);
|
|
1534
1586
|
if (aDt !== null && bDt !== null) {
|
|
1535
1587
|
const diffSecs = (aDt.getTime() - bDt.getTime()) / 1000.0;
|
|
1536
|
-
|
|
1588
|
+
if (!Number.isFinite(diffSecs)) return [];
|
|
1589
|
+
|
|
1590
|
+
const lit = formatDurationLiteralFromSeconds(diffSecs);
|
|
1591
|
+
|
|
1537
1592
|
if (g.o instanceof Var) {
|
|
1538
1593
|
const s2 = { ...subst };
|
|
1539
|
-
s2[g.o.name] =
|
|
1594
|
+
s2[g.o.name] = lit;
|
|
1540
1595
|
return [s2];
|
|
1541
1596
|
}
|
|
1542
|
-
const s2 = unifyTerm(g.o,
|
|
1597
|
+
const s2 = unifyTerm(g.o, lit, subst);
|
|
1543
1598
|
return s2 !== null ? [s2] : [];
|
|
1544
1599
|
}
|
|
1545
1600
|
|
package/package.json
CHANGED
package/test/examples.test.js
CHANGED
|
@@ -145,8 +145,11 @@ function main() {
|
|
|
145
145
|
let passed = 0;
|
|
146
146
|
let failed = 0;
|
|
147
147
|
|
|
148
|
+
// Pretty, stable numbering (e.g., 001..100 when running 100 tests)
|
|
149
|
+
const idxWidth = String(files.length).length;
|
|
150
|
+
|
|
148
151
|
for (let i = 0; i < files.length; i++) {
|
|
149
|
-
const idx = String(i + 1).padStart(
|
|
152
|
+
const idx = String(i + 1).padStart(idxWidth, '0');
|
|
150
153
|
const file = files[i];
|
|
151
154
|
|
|
152
155
|
const start = Date.now();
|