openscript 0.4.0__py3-none-any.whl

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.
Files changed (97) hide show
  1. openscript/__init__.py +40 -0
  2. openscript/__main__.py +62 -0
  3. openscript/accounting/__init__.py +74 -0
  4. openscript/accounting/analysis.py +174 -0
  5. openscript/accounting/charges.py +397 -0
  6. openscript/accounting/equity.py +234 -0
  7. openscript/accounting/report.py +82 -0
  8. openscript/accounting/shapes.py +74 -0
  9. openscript/accounting/statistics.py +300 -0
  10. openscript/accounting/trades.py +294 -0
  11. openscript/adapter/__init__.py +32 -0
  12. openscript/adapter/answers.py +215 -0
  13. openscript/adapter/channels.py +137 -0
  14. openscript/adapter/expectations.py +67 -0
  15. openscript/adapter/facts.py +127 -0
  16. openscript/adapter/matching.py +257 -0
  17. openscript/adapter/ordering.py +187 -0
  18. openscript/adapter/page.py +130 -0
  19. openscript/adapter/reading.py +357 -0
  20. openscript/adapter/reporting.py +244 -0
  21. openscript/adapter/running.py +449 -0
  22. openscript/adapter/serving.py +229 -0
  23. openscript/adapter/sessions.py +168 -0
  24. openscript/adapter/spellings.py +184 -0
  25. openscript/bars.py +157 -0
  26. openscript/budget.py +342 -0
  27. openscript/canonical.py +192 -0
  28. openscript/civil.py +196 -0
  29. openscript/contracts.py +165 -0
  30. openscript/dates.py +302 -0
  31. openscript/diagnostics.py +104 -0
  32. openscript/hours.py +165 -0
  33. openscript/inputs.py +239 -0
  34. openscript/intervals.py +60 -0
  35. openscript/library/__init__.py +76 -0
  36. openscript/library/arithmetic.py +128 -0
  37. openscript/library/averages.py +133 -0
  38. openscript/library/bars.py +60 -0
  39. openscript/library/bookkeeping.py +166 -0
  40. openscript/library/code_points.py +85 -0
  41. openscript/library/colour.py +202 -0
  42. openscript/library/composites.py +208 -0
  43. openscript/library/counting.py +218 -0
  44. openscript/library/deviation.py +155 -0
  45. openscript/library/elementary.py +206 -0
  46. openscript/library/extremes.py +122 -0
  47. openscript/library/flows.py +220 -0
  48. openscript/library/momentum.py +203 -0
  49. openscript/library/number_text.py +223 -0
  50. openscript/library/prices.py +36 -0
  51. openscript/library/ranges.py +105 -0
  52. openscript/library/rounding.py +123 -0
  53. openscript/library/series.py +213 -0
  54. openscript/library/stateful.py +442 -0
  55. openscript/library/stateless.py +261 -0
  56. openscript/library/strength.py +180 -0
  57. openscript/library/strings.py +228 -0
  58. openscript/library/trend.py +260 -0
  59. openscript/library/values.py +91 -0
  60. openscript/logbook.py +119 -0
  61. openscript/machine.py +499 -0
  62. openscript/memory.py +204 -0
  63. openscript/opcodes.py +166 -0
  64. openscript/program.py +146 -0
  65. openscript/run.py +368 -0
  66. openscript/strategy/__init__.py +78 -0
  67. openscript/strategy/calls.py +201 -0
  68. openscript/strategy/closable.py +182 -0
  69. openscript/strategy/fills.py +131 -0
  70. openscript/strategy/holdings.py +277 -0
  71. openscript/strategy/intents.py +162 -0
  72. openscript/strategy/ledger.py +270 -0
  73. openscript/strategy/placing.py +206 -0
  74. openscript/strategy/positions.py +124 -0
  75. openscript/strategy/refusals.py +293 -0
  76. openscript/strategy/rows.py +219 -0
  77. openscript/strategy/sizing.py +229 -0
  78. openscript/strategy/statuses.py +65 -0
  79. openscript/surface/__init__.py +115 -0
  80. openscript/surface/bands.py +103 -0
  81. openscript/surface/levels.py +44 -0
  82. openscript/surface/marks.py +52 -0
  83. openscript/surface/paints.py +58 -0
  84. openscript/surface/plots.py +44 -0
  85. openscript/surface/published.py +119 -0
  86. openscript/values.py +210 -0
  87. openscript/verify.py +301 -0
  88. openscript/verify_code.py +290 -0
  89. openscript/verify_requests.py +271 -0
  90. openscript/verify_shape.py +162 -0
  91. openscript/verify_tables.py +256 -0
  92. openscript/version.py +39 -0
  93. openscript/zones.py +118 -0
  94. openscript-0.4.0.dist-info/METADATA +82 -0
  95. openscript-0.4.0.dist-info/RECORD +97 -0
  96. openscript-0.4.0.dist-info/WHEEL +5 -0
  97. openscript-0.4.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,128 @@
1
+ """The bare arithmetic of `stdlib.md` section 8.1.
2
+
3
+ None of these has a warmup: each reads this bar's values only, so it produces a
4
+ value on bar 0 whenever its arguments do. Every one is absent when any argument
5
+ is absent, and every numeric result goes through ``result``, so a value with no
6
+ finite real answer is absence rather than an infinity.
7
+
8
+ `stdlib.md` section 20.10 names most of this file as arithmetic with no
9
+ accumulation order to fix: ``abs`` and ``sign`` are one operation each, and
10
+ ``min``, ``max`` and ``clamp`` select a value and compute nothing. ``mod`` is the
11
+ exception and its order is written out in section 8.1 and confirmed in 20.7: the
12
+ division, then the floor, then the multiplication, then the subtraction.
13
+ """
14
+
15
+ from .values import ABSENT, Value, floor_of, number, result
16
+
17
+
18
+ def abs_of(x: Value) -> Value:
19
+ """``abs(x)``: magnitude without sign."""
20
+ value = number(x)
21
+ return ABSENT if value is None else result(abs(value))
22
+
23
+
24
+ def sign(x: Value) -> Value:
25
+ """``sign(x)``: -1, 0 or 1.
26
+
27
+ Three comparisons rather than a division: the sign of a zero is not
28
+ observable in this language (`compiled-program.md` section 3.1), so a zero
29
+ of either sign answers 0 here.
30
+ """
31
+ value = number(x)
32
+ if value is None:
33
+ return ABSENT
34
+ if value > 0:
35
+ return 1.0
36
+ if value < 0:
37
+ return -1.0
38
+ return 0.0
39
+
40
+
41
+ def minimum(a: Value, b: Value) -> Value:
42
+ """``min(a, b)``: the smaller of two."""
43
+ left = number(a)
44
+ right = number(b)
45
+ if left is None or right is None:
46
+ return ABSENT
47
+ return result(left if left < right else right)
48
+
49
+
50
+ def maximum(a: Value, b: Value) -> Value:
51
+ """``max(a, b)``: the larger of two."""
52
+ left = number(a)
53
+ right = number(b)
54
+ if left is None or right is None:
55
+ return ABSENT
56
+ return result(left if left > right else right)
57
+
58
+
59
+ def clamp(x: Value, low: Value, high: Value) -> Value:
60
+ """``clamp(x, lo, hi)``: ``x`` held inside a range.
61
+
62
+ Written as two comparisons in this order rather than as ``min(max(...))``,
63
+ because the two disagree when the bounds are crossed and this one answers
64
+ with the lower bound there, which is what a reader of the name expects.
65
+ """
66
+ value = number(x)
67
+ lower = number(low)
68
+ upper = number(high)
69
+ if value is None or lower is None or upper is None:
70
+ return ABSENT
71
+ if value < lower:
72
+ return result(lower)
73
+ if value > upper:
74
+ return result(upper)
75
+ return result(value)
76
+
77
+
78
+ def mod(a: Value, b: Value) -> Value:
79
+ """``mod(a, b)``: ``a - b * floor(a / b)``, the floored remainder.
80
+
81
+ Its sign follows ``b``. The formula is written out rather than handed to the
82
+ interpreter's own remainder because "the modulo" names two different
83
+ functions in common use: this is the floored one and ``%`` is the truncated
84
+ one, so ``mod(-7, 3)`` is 2 where ``-7 % 3`` is -1 in this language. The two
85
+ agree for every positive ``b``, which is every use that wraps an index, a bar
86
+ count or a session offset.
87
+
88
+ **The host language's own floored remainder is not this function either.**
89
+ It agrees at every argument a script is likely to write and parts from it
90
+ where the division overflows, because it is exact rather than a division
91
+ followed by a floor: at ``mod(0.1, 1e-310)`` the quotient is an infinity and
92
+ this function is absent there, where the host's remainder answers
93
+ 3.135287412051e-311.
94
+
95
+ ``mod(a, 0)`` is absent, on the rule of `stdlib.md` section 2.4 that a result
96
+ with no finite real value is absent.
97
+ """
98
+ left = number(a)
99
+ right = number(b)
100
+ if left is None or right is None or right == 0:
101
+ return ABSENT
102
+ return result(left - right * floor_of(left / right))
103
+
104
+
105
+ def is_none(x: Value) -> bool:
106
+ """``isNone(x)``: true when the value is absent."""
107
+ return x is None
108
+
109
+
110
+ def or_else(x: Value, fallback: Value) -> Value:
111
+ """``orElse(x, fallback)``: ``x`` when present, ``fallback`` when absent.
112
+
113
+ The fallback is returned as it stands, absence included, so
114
+ ``orElse(x, none)`` is ``x``.
115
+ """
116
+ return fallback if x is None else x
117
+
118
+
119
+ def to_bool(x: Value) -> bool:
120
+ """``toBool(x)``: absence to false, a bool to itself.
121
+
122
+ A number is a type error the checker refuses, so an engine that reaches one
123
+ is looking at a program its verifier should not have accepted, and false is
124
+ the safe reading. Identity against ``True`` rather than truthiness, because
125
+ every non-zero number is truthy in this interpreter and one reaching here
126
+ would come back as a bool the language never made.
127
+ """
128
+ return x is True
@@ -0,0 +1,133 @@
1
+ """The six means of `stdlib.md` section 20.3 that are not built from another mean.
2
+
3
+ Each is one of the two shapes of section 20.2 and nothing more: a fresh window
4
+ sum, or a window mean that seeds a recurrence. What makes them six functions
5
+ rather than one is the step and the weighting, and what makes each of them one
6
+ function rather than a family is the arrangement, which is why every entry here
7
+ names the arrangement it is not.
8
+
9
+ A value arrives already read off this bar's arguments, and every one of these
10
+ pushes it into a buffer of its own before it looks at the window, because a call
11
+ that skipped a bar's contribution would have a window a bar out of step with the
12
+ bars it is supposed to cover.
13
+ """
14
+
15
+ from .series import Region, contributed, mean, seeded, total, window
16
+ from .values import ABSENT, Value, number, result
17
+
18
+
19
+ def simple(state: Region, value: Value, length: int | None) -> Value:
20
+ """``sma(src, len)``: the window sum of 20.2.1 divided by ``len``."""
21
+ values = contributed(state, "src", number(value), length)
22
+ held = window(values, length)
23
+ if held is None or length is None:
24
+ return ABSENT
25
+ return result(mean(held, length))
26
+
27
+
28
+ def exponential(state: Region, value: Value, length: int | None) -> Value:
29
+ """``ema(src, len)``: the seeded recurrence with the weight of section 4.
30
+
31
+ **The step is the two products added**, and it is not
32
+ ``running + (value - running) * weight``. That third arrangement of the same
33
+ algebra is the one an engine reaches for, because it is one multiplication
34
+ rather than two, and over the fixture the release gate compares bit for bit
35
+ it differs on most of the values at every length.
36
+ """
37
+ values = contributed(state, "src", number(value), length)
38
+ if length is None:
39
+ return ABSENT
40
+ weight = 2 / (length + 1)
41
+ rest = 1 - weight
42
+
43
+ def step(running: float, value: float) -> float:
44
+ return value * weight + running * rest
45
+
46
+ return seeded(state, "run", values, length, step)
47
+
48
+
49
+ def smoothed(state: Region, value: Value, length: int | None) -> Value:
50
+ """``rma(src, len)``: the recurrence the classic oscillators are built on.
51
+
52
+ **This is not the shape of ``ema`` with a weight of ``1 / len``.** The two
53
+ are one function in exact arithmetic and two numbers in binary64, and the
54
+ difference propagates into the strength reading, the average true range, the
55
+ trailing band and the directional index, which are the readings a chart is
56
+ most often asked to reproduce. Neither is
57
+ ``running + (value - running) / len``, which is a third arrangement again.
58
+ """
59
+ values = contributed(state, "src", number(value), length)
60
+ if length is None:
61
+ return ABSENT
62
+ span = float(length)
63
+
64
+ def step(running: float, value: float) -> float:
65
+ return (running * (span - 1) + value) / span
66
+
67
+ return seeded(state, "run", values, length, step)
68
+
69
+
70
+ def linear(state: Region, value: Value, length: int | None) -> Value:
71
+ """``wma(src, len)``: weight 1 added first, weight ``len`` last, one division.
72
+
73
+ The weight on ``w[k]`` is ``len - k``, formed as a multiplication of the
74
+ value by the whole number weight and not by a precomputed fraction. Dividing
75
+ each term by the divisor as it is added is a different number.
76
+
77
+ Nothing is fixed about how the divisor itself is formed: its factors are a
78
+ whole number product scaled by a power of two, which is exact however it is
79
+ grouped.
80
+ """
81
+ values = contributed(state, "src", number(value), length)
82
+ held = window(values, length)
83
+ if held is None or length is None:
84
+ return ABSENT
85
+ span = float(length)
86
+ divisor = (span * (span + 1)) / 2
87
+ running = 0.0
88
+ for position in range(length):
89
+ running = running + held[length - 1 - position] * float(position + 1)
90
+ return result(running / divisor)
91
+
92
+
93
+ def symmetric(state: Region, value: Value) -> Value:
94
+ """``swma(src)``: the fixed four bar mean, added left to right, divided once.
95
+
96
+ Over twenty thousand four bar windows of ordinary prices, regrouping the four
97
+ terms in the middle differs on 5281 of them, adding them right to left on
98
+ 7230, and dividing each term by 6 as it is added on 9564. How the two middle
99
+ terms are doubled is not fixed, because an exact scaling by two is one value
100
+ however it is written.
101
+ """
102
+ values = contributed(state, "src", number(value), 4)
103
+ held = window(values, 4)
104
+ if held is None:
105
+ return ABSENT
106
+ running = held[3]
107
+ running = running + 2 * held[2]
108
+ running = running + 2 * held[1]
109
+ running = running + held[0]
110
+ return result(running / 6)
111
+
112
+
113
+ def volume_weighted(state: Region, value: Value, size: Value, length: int | None) -> Value:
114
+ """``vwma(src, len)``: this bar's product formed first, then two window sums.
115
+
116
+ Dividing each sum by ``len`` first and then dividing one by the other is the
117
+ same quantity with two extra roundings in it, and it is not this
118
+ arrangement. The result is absent where the denominator is zero, which is a
119
+ window nothing traded in rather than an error.
120
+ """
121
+ price = number(value)
122
+ traded = number(size)
123
+ product = ABSENT if price is None or traded is None else price * traded
124
+ products = contributed(state, "flow", product, length)
125
+ volumes = contributed(state, "size", traded, length)
126
+ top = window(products, length)
127
+ bottom = window(volumes, length)
128
+ if top is None or bottom is None:
129
+ return ABSENT
130
+ divisor = total(bottom)
131
+ if divisor == 0:
132
+ return ABSENT
133
+ return result(total(top) / divisor)
@@ -0,0 +1,60 @@
1
+ """The readings taken from one bar and nothing before it.
2
+
3
+ `stdlib.md` section 20.5 fixes the one that is not a single operation:
4
+
5
+ ```text
6
+ result = max(high - low, abs(high - previousClose), abs(low - previousClose))
7
+ ```
8
+
9
+ **The oldest bar of the dataset is the library's one deliberate exception to
10
+ absence propagation**, and `stdlib.md` section 6 puts it there rather than
11
+ leaving it to an engine: ``trueRange()`` is ``high - low`` on that bar. The other
12
+ two terms need a close that does not exist there, and propagating absence would
13
+ start every average of this reading one bar later than the declared warmup while
14
+ adding nothing, because the bar's own range is a true statement about that bar.
15
+
16
+ The exception is granted to ``trueRange`` and therefore to the averages of it. It
17
+ is not granted to every function that happens to use a range: where a function's
18
+ declared warmup shows it counts changes rather than levels, section 20.5's gap
19
+ aware form is what it takes, and that one is absent on the oldest bar like any
20
+ other change. Two functions use it and both declare a warmup one bar later than
21
+ the plain reading would give, which is how the page says which of the two they
22
+ take. That form is not a call a script can make, and an engine reaches it through
23
+ the stateful half of this library rather than through here.
24
+
25
+ **Where the previous close comes from is a host fact, not an arithmetic one.** An
26
+ engine is handed it with the bar and that is the one it must use, because a call
27
+ inside a branch does not see every bar and the close of the bar it last ran on is
28
+ a different number. So it is an argument here, and this function holds nothing
29
+ across bars.
30
+ """
31
+
32
+ from .values import ABSENT, Value, number, result
33
+
34
+
35
+ def true_range(high: Value, low: Value, previous_close: Value, first_bar: bool) -> Value:
36
+ """``trueRange()``: the bar's range, including any gap from the last close.
37
+
38
+ ``first_bar`` is the caller's answer to "is this the oldest bar of the
39
+ dataset", which is the question section 6's exception turns on and not one
40
+ the three numbers can answer. An absent previous close in the middle of a run
41
+ is a hole, and a hole propagates.
42
+ """
43
+ top = number(high)
44
+ bottom = number(low)
45
+ if first_bar:
46
+ if top is None or bottom is None:
47
+ return ABSENT
48
+ return result(top - bottom)
49
+ close_before = number(previous_close)
50
+ if top is None or bottom is None or close_before is None:
51
+ return ABSENT
52
+ within = top - bottom
53
+ up_gap = abs(top - close_before)
54
+ down_gap = abs(bottom - close_before)
55
+ widest = within
56
+ if up_gap > widest:
57
+ widest = up_gap
58
+ if down_gap > widest:
59
+ widest = down_gap
60
+ return result(widest)
@@ -0,0 +1,166 @@
1
+ """The comparisons and counters of `stdlib.md` section 9, named in 20.10.
2
+
3
+ None of these accumulates. They compare this bar's value with an older one, or
4
+ count bars, so there is one arrangement and no choice to record. What there is
5
+ instead is a set of edge rules, and each of them is a decision somebody has to
6
+ make the same way twice for two engines to agree:
7
+
8
+ - **A crossing keeps the two series apart rather than subtracting them.** The
9
+ test turns on whether one was at or below the other, and a rounded zero would
10
+ change the answer on exactly the bars a crossing matters.
11
+ - **A crossing is "at or below, then above"**, not "strictly below, then above",
12
+ so two series that touch and separate report one cross rather than none. The
13
+ alternative loses the case where the values are briefly equal, which is common
14
+ on instruments with a coarse tick.
15
+ - **A run test needs its own changes to be present.** ``rising(src, len)`` reads
16
+ ``len`` changes and therefore ``len + 1`` values, which is where its warmup of
17
+ bar ``len`` comes from and why it is a bar later than a window of ``len``.
18
+ - **The two that wait for a condition are absent, not zero, before it has ever
19
+ been true.** Zero would read as "it happened on this bar".
20
+ """
21
+
22
+ from typing import Optional
23
+
24
+ from .series import Region, back, contributed, raw_window, region
25
+ from .values import ABSENT, Value, number, result
26
+
27
+
28
+ def change(state: Region, value: Value, length: Optional[int]) -> Value:
29
+ """``change(src)`` and ``change(src, len)``: ``src - src[len]``, one subtraction."""
30
+ keep = None if length is None else length + 1
31
+ values = contributed(state, "src", number(value), keep)
32
+ older = back(values, length)
33
+ newer = values[len(values) - 1]
34
+ if not isinstance(newer, float) or not isinstance(older, float):
35
+ return ABSENT
36
+ return result(newer - older)
37
+
38
+
39
+ def rising(state: Region, value: Value, length: Optional[int]) -> Value:
40
+ """``rising(src, len)``: true when each of the last ``len`` changes was positive."""
41
+ return _run(state, value, length, True)
42
+
43
+
44
+ def falling(state: Region, value: Value, length: Optional[int]) -> Value:
45
+ """``falling(src, len)``: true when each of the last ``len`` changes was negative."""
46
+ return _run(state, value, length, False)
47
+
48
+
49
+ def _run(state: Region, value: Value, length: Optional[int], up: bool) -> Value:
50
+ """The run test over the ``len + 1`` values the ``len`` changes are taken from.
51
+
52
+ **The changes are read oldest first and the answer is the first one that
53
+ settles it.** A change that is not in the direction asked for makes the run
54
+ false there and then, and a change that cannot be taken because one of its
55
+ two bars is absent makes the answer absent there and then. So a window with a
56
+ hole in it is false rather than absent whenever an older change had already
57
+ failed, which is not the blanket propagation of section 2.4 and is the order
58
+ this reading is taken in.
59
+ """
60
+ keep = None if length is None else length + 1
61
+ held = raw_window(contributed(state, "src", number(value), keep), keep)
62
+ if held is None or length is None:
63
+ return ABSENT
64
+ for age in range(length - 1, -1, -1):
65
+ newer = held[age]
66
+ older = held[age + 1]
67
+ if not isinstance(newer, float) or not isinstance(older, float):
68
+ return ABSENT
69
+ if up and not newer > older:
70
+ return False
71
+ if not up and not newer < older:
72
+ return False
73
+ return True
74
+
75
+
76
+ def crossed_up(state: Region, first: Value, second: Value) -> Value:
77
+ """``crossUp(a, b)``: ``a`` was at or below ``b`` and is now above."""
78
+ pair = _pair(state, first, second)
79
+ if pair is None:
80
+ return ABSENT
81
+ (now_first, before_first), (now_second, before_second) = pair
82
+ return before_first <= before_second and now_first > now_second
83
+
84
+
85
+ def crossed_down(state: Region, first: Value, second: Value) -> Value:
86
+ """``crossDown(a, b)``: ``a`` was at or above ``b`` and is now below."""
87
+ pair = _pair(state, first, second)
88
+ if pair is None:
89
+ return ABSENT
90
+ (now_first, before_first), (now_second, before_second) = pair
91
+ return before_first >= before_second and now_first < now_second
92
+
93
+
94
+ def crossed(state: Region, first: Value, second: Value) -> Value:
95
+ """``cross(a, b)``: either direction, read from the one pair of readings."""
96
+ pair = _pair(state, first, second)
97
+ if pair is None:
98
+ return ABSENT
99
+ (now_first, before_first), (now_second, before_second) = pair
100
+ if before_first <= before_second and now_first > now_second:
101
+ return True
102
+ return before_first >= before_second and now_first < now_second
103
+
104
+
105
+ def _pair(state: Region, first: Value, second: Value):
106
+ """This bar's and the previous bar's readings of both series, or absence.
107
+
108
+ Both are contributed on every bar whatever the other one did, so a hole in
109
+ one does not shift the other's idea of which bar was previous.
110
+ """
111
+ left = contributed(state, "a", number(first), 2)
112
+ right = contributed(state, "b", number(second), 2)
113
+ now_left, before_left = left[len(left) - 1], back(left, 1)
114
+ now_right, before_right = right[len(right) - 1], back(right, 1)
115
+ for reading in (now_left, before_left, now_right, before_right):
116
+ if not isinstance(reading, float):
117
+ return None
118
+ return (now_left, before_left), (now_right, before_right)
119
+
120
+
121
+ def bars_since(state: Region, condition: Value) -> Value:
122
+ """``barsSince(cond)``: bars since the condition last held, 0 on the bar itself.
123
+
124
+ Absent until the condition has been true once. A bar the condition is absent
125
+ on is a bar it did not hold: the count goes on rather than stopping, because
126
+ the question is how long ago the last true bar was and an unknown bar in
127
+ between does not change the answer.
128
+ """
129
+ held = region(state, "since")
130
+ if condition is True:
131
+ held["bars"] = 0.0
132
+ return 0.0
133
+ since = held.get("bars")
134
+ if since is None:
135
+ return ABSENT
136
+ since = since + 1
137
+ held["bars"] = since
138
+ return since
139
+
140
+
141
+ def value_when(
142
+ state: Region, condition: Value, value: Value, occurrence: Optional[int]
143
+ ) -> Value:
144
+ """``valueWhen(cond, src, occurrence)``: ``src`` as it stood on a true bar.
145
+
146
+ Occurrence 0 is the most recent true bar, 1 the one before it, and the answer
147
+ is absent until that many true bars have happened. The value recorded is
148
+ whatever ``src`` was on that bar, absence included: the condition decides
149
+ which bar is read and the source decides what was there.
150
+ """
151
+ wanted = 0 if occurrence is None else occurrence
152
+ held = region(state, "seen")
153
+ values = held.get("values")
154
+ if values is None:
155
+ values = []
156
+ held["values"] = values
157
+ if condition is True:
158
+ values.append(value)
159
+ depth = max(wanted + 1, held.get("depth", 1))
160
+ held["depth"] = depth
161
+ extra = len(values) - depth
162
+ if extra > 0:
163
+ del values[:extra]
164
+ if wanted < 0 or len(values) <= wanted:
165
+ return ABSENT
166
+ return values[len(values) - 1 - wanted]
@@ -0,0 +1,85 @@
1
+ """The two string rules two engines have to share, written from the page.
2
+
3
+ **A string is a sequence of code points** (`compiled-program.md` section 3.1),
4
+ so length, indexing and comparison count code points and not the storage unit of
5
+ whatever language an engine is written in. This host stores a string as code
6
+ points already, which makes the rule free here and does not make it optional: an
7
+ engine whose strings are sixteen bit units has to count and index past a
8
+ surrogate pair as one element, or the two disagree about the length of a string
9
+ holding a symbol outside the basic plane and about every substring taken after
10
+ one.
11
+
12
+ **Ordering.** Two strings are ordered by comparing code points from the front,
13
+ the first difference deciding, and a string that runs out first ordering first
14
+ (`language.md` section 9.3, `stdlib.md` section 10). It is the one order in the
15
+ language: the four comparison operators and a sort over an array of strings
16
+ agree. This host's own comparison is already by code point, so the function below
17
+ is the rule written down rather than a correction of the host, and a test holds
18
+ it to a pair the unit based order would get wrong.
19
+
20
+ **Whitespace.** ``str.trim`` removes, and ``toNumber`` ignores at either end,
21
+ exactly the code points `stdlib.md` section 10 lists, which are the ones with the
22
+ Unicode White_Space property, and no other. The host's own strip removes a
23
+ different set: it takes the four information separators U+001C to U+001F as well,
24
+ and another host takes the byte order mark U+FEFF and leaves those, so neither
25
+ host's default is the set and the set is written out here. ``tests/test_strings``
26
+ walks every code point of the basic plane against the table read out of the page,
27
+ which is what makes the page the input rather than this list.
28
+ """
29
+
30
+ # The code points `stdlib.md` section 10 lists, in the order it lists them.
31
+ _WHITESPACE: frozenset[int] = frozenset(
32
+ {
33
+ 0x0009, # character tabulation
34
+ 0x000A, # line feed
35
+ 0x000B, # line tabulation
36
+ 0x000C, # form feed
37
+ 0x000D, # carriage return
38
+ 0x0020, # space
39
+ 0x0085, # next line
40
+ 0x00A0, # no-break space
41
+ 0x1680, # ogham space mark
42
+ 0x2000, # en quad, through the hair space
43
+ 0x2001,
44
+ 0x2002,
45
+ 0x2003,
46
+ 0x2004,
47
+ 0x2005,
48
+ 0x2006,
49
+ 0x2007,
50
+ 0x2008,
51
+ 0x2009,
52
+ 0x200A,
53
+ 0x2028, # line separator
54
+ 0x2029, # paragraph separator
55
+ 0x202F, # narrow no-break space
56
+ 0x205F, # medium mathematical space
57
+ 0x3000, # ideographic space
58
+ }
59
+ )
60
+
61
+
62
+ def is_whitespace(point: int) -> bool:
63
+ """Whether one code point is in the trimmed set."""
64
+ return point in _WHITESPACE
65
+
66
+
67
+ def trimmed(text: str) -> str:
68
+ """``text`` with the whitespace of the written set taken off both ends."""
69
+ start = 0
70
+ end = len(text)
71
+ while start < end and is_whitespace(ord(text[start])):
72
+ start += 1
73
+ while end > start and is_whitespace(ord(text[end - 1])):
74
+ end -= 1
75
+ return text[start:end]
76
+
77
+
78
+ def compare(a: str, b: str) -> int:
79
+ """The order of two strings: negative when ``a`` comes first, by code point."""
80
+ for one, other in zip(a, b):
81
+ if one != other:
82
+ return -1 if ord(one) < ord(other) else 1
83
+ if len(a) == len(b):
84
+ return 0
85
+ return -1 if len(a) < len(b) else 1