sevgi-function 0.95.0 → 1.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 +221 -2
- data/README.md +15 -8
- data/lib/sevgi/core.rb +1 -1
- data/lib/sevgi/function/color.rb +2 -1
- data/lib/sevgi/function/file.rb +9 -10
- data/lib/sevgi/function/locate.rb +33 -15
- data/lib/sevgi/function/math.rb +91 -24
- data/lib/sevgi/function/shell.rb +80 -26
- data/lib/sevgi/function/string.rb +19 -9
- data/lib/sevgi/function/ui.rb +2 -1
- data/lib/sevgi/function/version.rb +1 -1
- data/lib/sevgi/function.rb +14 -4
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7fd50bb171b09d5221092de41845690e2b1e8e05e62811b972306b0522f6cbea
|
|
4
|
+
data.tar.gz: d8cff5f7b9af0492f0d0824271b4a2dc95069d2d4e82bbab012b56e0b15dc631
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ca877b117f6e9e7d7a3da866b3ac73d6120c820038e0192e0e55bb927c9b28c527f68b16ecf02fddbd69f47435fdb940214e70fe78f2f3d96e4f3a006a9732cd
|
|
7
|
+
data.tar.gz: 5cced2a22f74bb0836f96d29de58d2d2dd1052d871bcc24bce6ffd1e59ee2760ab71e5bb2f83d36e7cf6bcdb30e3d3f0f6a4c9f7ca755bde02f2cb405948b16c
|
data/CHANGELOG.md
CHANGED
|
@@ -3,7 +3,226 @@
|
|
|
3
3
|
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) and follows
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
5
5
|
|
|
6
|
-
##
|
|
6
|
+
## 1.0.0 - 2026-09-16
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- Added Hatch and Orb Showcase examples and displayed both arc lengths in the Arc example.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Matched documentation page widths to the homepage Showcase.
|
|
15
|
+
- Aligned Derender user guidance with UTF-8 normalization and safe subtree selection rules.
|
|
16
|
+
- Clarified the packaged skill's script invocation, subtree copying, and Derender selection and encoding rules.
|
|
17
|
+
- Allowed Canvas and Paper values as the first `SVG` argument when using the default document profile.
|
|
18
|
+
- Made element `dup` and `clone` own independent subtrees while retaining IDs and Ruby copy semantics.
|
|
19
|
+
- Made Segment equality compare length and direction; ordering still compares length only.
|
|
20
|
+
- Limited Tile cell, row, and column queries to the requested geometry and accelerated XML character validation.
|
|
21
|
+
- Simplified the first-run guide and aligned library constructor examples.
|
|
22
|
+
- Updated the packaged skill for canvas origins, alignment, Paper lookup, curved geometry, and export constraints.
|
|
23
|
+
- Revised DSL catalog examples and packaged Sevgi skill guidance to use statement-style calls and show inspection results.
|
|
24
|
+
- Refined Showcase example tabs, made Ruby and XML views fill their panels, and hid executable preambles from Ruby views.
|
|
25
|
+
- Documented non-rendering element data and clarified the related DSL catalog examples.
|
|
26
|
+
- Refined Showcase heading spacing, page-title colors, and DSL catalog cards across light and dark themes.
|
|
27
|
+
- Removed an obsolete ruler example and unused site code.
|
|
28
|
+
- Kept the Squared and Copperplate guidesheet examples together at the end of the Showcase.
|
|
29
|
+
- Updated the packaged skill to avoid duplicate semantic shapes and unnecessary paint passes.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- Kept horizontal scrollbars visible in Showcase example code panels.
|
|
34
|
+
- Read Derender input in its declared XML encoding and normalized generated source, XML declarations, and markup to UTF-8.
|
|
35
|
+
- Preserved open-path endpoints and derived edges independently of display precision.
|
|
36
|
+
- Kept near-axis and near-parallel line equations and intersections independent of angle and slope rounding.
|
|
37
|
+
- Corrected the Showcase social image dimensions.
|
|
38
|
+
- Added SVG namespaces to standalone drawing examples and regenerated their outputs.
|
|
39
|
+
- Made failed Sevgi Rake tasks stop dependent tasks and captured required-library failures in executor results.
|
|
40
|
+
- Removed executor signal-handler replacement so the host and shell retain SIGINT ownership.
|
|
41
|
+
- Escaped late export CSS as XML text and inserted it only at a supported final root closing tag.
|
|
42
|
+
- Preserved Derender processing instructions and rejected custom entity references and unsupported trailing document nodes before inclusion.
|
|
43
|
+
- Included both box origins in `Align`, unified Tile index validation, and isolated custom numeric attribute snapshots.
|
|
44
|
+
- Prevented avoidable overflow and underflow in point distances.
|
|
45
|
+
- Corrected browser source-panel expectations and added executable documentation, YARD, and packaged-skill checks.
|
|
46
|
+
- Rendered inline code in DSL catalog summaries and kept linked task themes visible below the sticky header.
|
|
47
|
+
- Restored Showcase tab icons with embedded Font Awesome SVGs.
|
|
48
|
+
|
|
49
|
+
## 0.100.0 - 2026-09-09
|
|
50
|
+
|
|
51
|
+
### Added
|
|
52
|
+
|
|
53
|
+
- Added `ArcTo` and `ArcBy` for SVG-native elliptical arc paths.
|
|
54
|
+
- Added Geometry `Arc`, `Ellipse`, and `Circle` with bounds, length, predicates, affine transformations, and
|
|
55
|
+
linear/quadratic intersections. Closed ellipses and circles support sweeps and hatching.
|
|
56
|
+
- Added Geometry `Point.midpoint`, `Element#closed?`, lined `vertices`, open-path traversal, and `Rect#center`.
|
|
57
|
+
- Added Geometry aggregate bounding boxes through `Operation.box`, point collinearity through `Point.collinear?`, and
|
|
58
|
+
polygon `simple?`, `convex?`, and `concave?` predicates.
|
|
59
|
+
- Added Protractor and Arc examples and documented the drawing and calculation APIs.
|
|
60
|
+
|
|
61
|
+
### Changed
|
|
62
|
+
|
|
63
|
+
- Removed redundant parent scans while duplicating element subtrees.
|
|
64
|
+
- Reused source bounds across Tile cells to reduce repeated calculations and object allocations.
|
|
65
|
+
|
|
66
|
+
### Fixed
|
|
67
|
+
|
|
68
|
+
- Preserved Showcase card HTML through Markdown rendering so all examples and their tabs remain visible.
|
|
69
|
+
- Preserved XML declarations, pre-root comments, CDATA sections, inline comments, and namespace-only leaves in
|
|
70
|
+
Derender output and evaluation.
|
|
71
|
+
- Limited PDF stamp recoloring to replaced text and carried graphics state across a page's content streams.
|
|
72
|
+
- Corrected decimal interval fitting and made Grid point queries honor the active numeric precision.
|
|
73
|
+
- Prevented implicit shell interpretation of single executable arguments in `F.sh` and `F.sh!`, including paths with spaces or shell metacharacters.
|
|
74
|
+
- Preserved inherited namespaces on detached Derender nodes in generated source and direct evaluation, including imports through `EvaluateChildren` and `IncludeChildren`.
|
|
75
|
+
- Prevented sweep and Hatch lines from crossing the exterior between two tangent vertices of a concave polygon.
|
|
76
|
+
- Aligned Tile cells with their declared bounds for circles, rotated shapes, and polygons whose first vertex differs from their bounding-box origin.
|
|
77
|
+
- Limited `Load` chains to 128 active sources with `Executor::LoadDepthError`, preserving cycle detection and nested load diagnostics.
|
|
78
|
+
- Rejected nil and false aggregate-box inputs with the documented geometry error.
|
|
79
|
+
- Restored precision-aware rejection of zero-length Triangle sides.
|
|
80
|
+
- Made point-set collinearity independent of input order and removed eager polygon edge-pair storage.
|
|
81
|
+
- Applied the collinearity tolerance to every three-point subset, removing coordinate-axis-dependent baseline selection.
|
|
82
|
+
|
|
83
|
+
## 0.99.0 - 2026-08-09
|
|
84
|
+
|
|
85
|
+
### Added
|
|
86
|
+
|
|
87
|
+
- Added `SVG.Module` for building anonymous callable drawing modules with their contract installed before definition.
|
|
88
|
+
- Added responsive Mermaid diagrams for the Derender round trip and SVG validation lifecycle.
|
|
89
|
+
|
|
90
|
+
### Changed
|
|
91
|
+
|
|
92
|
+
- Reorganized the documentation around getting started, usage, composition, documents, layout, output, and examples.
|
|
93
|
+
- Documented how the `igves` and `igsev` command names reflect their conversion directions.
|
|
94
|
+
- Normalized DSL catalog provider labels to lowercase and simplified polyfill-backed editor entries to `inkscape`.
|
|
95
|
+
|
|
96
|
+
### Fixed
|
|
97
|
+
|
|
98
|
+
- Allowed A5-on-A4 pdfcpu conversion to replace an existing output file.
|
|
99
|
+
- Pre-rendered Mermaid diagrams as inline SVG instead of parsing them in site visitors' browsers.
|
|
100
|
+
- Removed duplicate provider and context labels from DSL catalog entries.
|
|
101
|
+
|
|
102
|
+
## 0.98.2 - 2026-07-25
|
|
103
|
+
|
|
104
|
+
### Fixed
|
|
105
|
+
|
|
106
|
+
- Restored the `sevgi` CLI's main-object scope so bare toolkit calls remain available inside script helper classes.
|
|
107
|
+
|
|
108
|
+
## 0.98.1 - 2026-07-20
|
|
109
|
+
|
|
110
|
+
### Changed
|
|
111
|
+
|
|
112
|
+
- Changed the `sevgi` CLI to use the executor's isolated scope by default.
|
|
113
|
+
|
|
114
|
+
### Removed
|
|
115
|
+
|
|
116
|
+
- Removed the redundant `-n` and `--nomain` CLI options; library consumers can still select `main: true` through
|
|
117
|
+
`Sevgi.execute` and `Sevgi.execute_file` when needed.
|
|
118
|
+
|
|
119
|
+
## 0.98.0 - 2026-07-20
|
|
120
|
+
|
|
121
|
+
### Added
|
|
122
|
+
|
|
123
|
+
- Added standard-input support to `sevgi`, `igves`, and `igsev`; `sevgi --as NAME` and
|
|
124
|
+
`Sevgi.execute_file(..., as: NAME)` supply the logical source basename used by implicit SVG, PDF, and PNG
|
|
125
|
+
destinations without changing physical load identity.
|
|
126
|
+
|
|
127
|
+
## 0.97.0 - 2026-07-19
|
|
128
|
+
|
|
129
|
+
### Added
|
|
130
|
+
|
|
131
|
+
- Added `sevgi-appendix`, containing the Sevgi agent skill and a RuboCop plugin for preserving readable `.sevgi` DSL
|
|
132
|
+
source alongside rubyfmt-formatted Ruby; the umbrella `sevgi` gem installs the matching Appendix version.
|
|
133
|
+
- Added `sevgi --skill` to report the validated path of the matching packaged agent skill for product-neutral setup.
|
|
134
|
+
- Added `igsev` to the umbrella gem for normalizing an SVG file through a complete SVG-to-Sevgi-to-SVG round trip.
|
|
135
|
+
|
|
136
|
+
### Changed
|
|
137
|
+
|
|
138
|
+
- Made Homebrew the recommended complete CLI installation while retaining Bundler and focused component gems for Ruby
|
|
139
|
+
application dependencies.
|
|
140
|
+
|
|
141
|
+
## 0.96.0 - 2026-07-18
|
|
142
|
+
|
|
143
|
+
### Added
|
|
144
|
+
|
|
145
|
+
- Added exact, subtree-wide attribute omission to Derender content/file conversion, evaluation, and inclusion APIs;
|
|
146
|
+
`igves --omit` exposes the same behavior from the command line.
|
|
147
|
+
- Added `Sevgi.SVG` as the explicit namespaced form of the top-level SVG document entrypoint.
|
|
148
|
+
- Added the opt-in recursive `SVG::Modules` contract for callable drawing namespaces.
|
|
149
|
+
- Added callable drawing-module `base` blocks, document and paper registry introspection, Canvas structural equality,
|
|
150
|
+
renderer options on `RenderChildren`, axis translation helpers, and a CC BY RDF helper.
|
|
151
|
+
- Formalized non-rendering `-` metadata and `+` attribute updates as `Attributes::META_PREFIX` and
|
|
152
|
+
`Attributes::UPDATE_SUFFIX`; repeated Array updates now concatenate into a stable flat value.
|
|
153
|
+
|
|
154
|
+
### Changed
|
|
155
|
+
|
|
156
|
+
- Breaking: replaced the `SVG = Sevgi::Graphics` alias with an independent SVG facade. Facade operations now use
|
|
157
|
+
capitalized names such as `SVG.Canvas` and `SVG.Document`; lowercase component helpers remain on
|
|
158
|
+
`Sevgi::Graphics`, and the stuttering `SVG.SVG` form was removed.
|
|
159
|
+
- Breaking: made low-level `Executor.execute` and `execute_file` runners private; consumers use
|
|
160
|
+
`Sevgi.execute` and `Sevgi.execute_file` while retaining the public result and error types.
|
|
161
|
+
- Breaking: removed the abstract `Document::Base` layer from the selectable document-profile registry; `Minimal` and
|
|
162
|
+
`Default` are now sibling concrete profiles, and custom profile hierarchies should derive from `Base`.
|
|
163
|
+
- Breaking: renamed Parallelogram segment and constraint arguments by geometric role: `base`, `side`, and
|
|
164
|
+
`constraint` replace axis-implying constructor names.
|
|
165
|
+
- Made degree-based sine and cosine exact at integer quarter turns, eliminating cardinal Geometry residue.
|
|
166
|
+
- Documented complete file-system failure families for file comparison, output, touch, and upward location helpers.
|
|
167
|
+
- Formalized generated Tile ids, positional CSS classes, template placement, and per-use callback signatures.
|
|
168
|
+
- Formalized String and Symbol ids across Derender selection APIs and documented file-read failures consistently.
|
|
169
|
+
- Breaking: made top-level `Decompile`, `Derender`, `Evaluate`, and `EvaluateChildren` consume inline SVG/XML;
|
|
170
|
+
file inputs now use the corresponding `File`-suffixed entrypoints.
|
|
171
|
+
- Breaking: replaced callable drawing module `call` block registration with argument-independent `base` blocks;
|
|
172
|
+
inherited bases run parent-first, and modules with one drawing method conventionally name it `call`.
|
|
173
|
+
- Breaking: made bracket and call notation the canonical Geometry constructors. Among Data value types, bracket notation
|
|
174
|
+
is public only for Point, Segment, LengthAngle, Margin, and Paper; use `.new` for Location and result carriers, and
|
|
175
|
+
`Stay` for traversal stop tokens.
|
|
176
|
+
- Breaking: aligned Point and Segment comparison with Ruby `Comparable`; malformed or unrelated comparison operands now
|
|
177
|
+
return nil from `<=>`.
|
|
178
|
+
- Breaking: executor entrypoints now return immutable `Executor::Result` values and expose only `execute` and
|
|
179
|
+
`execute_file`; inspect `result.error`, `result.stack`, and `result.value` instead of executor scopes or lifecycle state.
|
|
180
|
+
- Breaking: wrapper attributes and callable arguments use distinct channels, `With` rejects parentless receivers, and
|
|
181
|
+
unsupported direct constructors are private; use the documented factories for Content, concrete Geometry lined
|
|
182
|
+
elements, and Grid query values.
|
|
183
|
+
- Breaking: Canvas uses `.new` for explicit fields, `.from_paper(paper, **overrides)` for paper conversion, and
|
|
184
|
+
`.call`/`Graphics.canvas` for dispatch; the misleading keyword-only `.from_paper(width:, height:)` form was removed.
|
|
185
|
+
- Breaking: `Save`, `Write`, `PNG`, `PDF`, and Sundries native export normalize successful destinations to expanded
|
|
186
|
+
String paths and create missing parent directories; change-aware SVG writes still return nil when unchanged.
|
|
187
|
+
- Breaking: renamed `Derender.evaluate_file_children` to `evaluate_children_file`; decompiled nodes now own immutable
|
|
188
|
+
public state while parser, strategy, and construction plumbing remain private.
|
|
189
|
+
- Expanded runtime/YARD parity checks and exact contracts for inherited, extended, generated, and dynamic API surfaces,
|
|
190
|
+
forwarded options, error channels, path ownership, whitespace, namespaces, nil behavior, and return values.
|
|
191
|
+
|
|
192
|
+
### Fixed
|
|
193
|
+
|
|
194
|
+
- Rejected directories from default file discovery while preserving custom locator matchers.
|
|
195
|
+
- Kept `Ancestral` context in non-rendering `-context` metadata instead of leaking it into SVG attributes.
|
|
196
|
+
- Preserved source Canvas units and names when deriving fitted Grid canvases.
|
|
197
|
+
- Prevented stale documentation assets from mixing old layouts with new HTML, and moved sidebar navigation into the
|
|
198
|
+
tablet menu at 1024px and below.
|
|
199
|
+
- Normalized numeric slots owned by path, shape, transform, tile, and Inkscape page helpers to finite SVG number spelling;
|
|
200
|
+
arbitrary user-supplied attributes remain untouched.
|
|
201
|
+
- Made named callable wrapper and symbol ids stable while omitting defaults for anonymous modules, and indexed every
|
|
202
|
+
rendered id value, including false and numeric values, through its serialized string.
|
|
203
|
+
- Preserved signed Geometry constraint directions, rejected invalid sweep/export channels through Sevgi error families,
|
|
204
|
+
and validated raw output paths before expansion or rendering.
|
|
205
|
+
- Made callable module configuration copy-owned and freeze-aware, document subclasses inherit their nearest profile, and
|
|
206
|
+
false executor boot receivers remain explicit rather than defaulting to an internal scope.
|
|
207
|
+
- Made element trees, attributes, identifiers, locator results, document profiles, Derender nodes, shell results, and
|
|
208
|
+
executor results retain owned immutable snapshots where their public contracts promise value semantics.
|
|
209
|
+
- Corrected Shell combined-output separators, nil export density errors, document render-option routing, executor source
|
|
210
|
+
snapshots, selected-node namespace/whitespace documentation, and Standard character-data validation.
|
|
211
|
+
|
|
212
|
+
### Removed
|
|
213
|
+
|
|
214
|
+
- Removed public access to pluralization tables and the internal SVG save extension; pluralization rules are now deeply
|
|
215
|
+
immutable.
|
|
216
|
+
- Removed public documentation and constant access for command-line implementation modules; the `sevgi` and `igves`
|
|
217
|
+
executables remain unchanged.
|
|
218
|
+
- Removed eager loading and public documentation of private Showcase build/test support; explicit support entrypoints
|
|
219
|
+
now keep the harness under the Showcase namespace.
|
|
220
|
+
- Removed public access to document profile name normalizers; registry operations retain them as private plumbing.
|
|
221
|
+
- Removed the redundant `Margin.margin` constructor; use canonical bracket notation.
|
|
222
|
+
- Removed public executor orchestration, obsolete callable-module hooks, the old public attribute syntax constants, and
|
|
223
|
+
direct construction of abstract or internally wired Content, Element, and Grid query types.
|
|
224
|
+
- Removed accidental public access to abstract Lined factories, internal element-name/export maps, result/location
|
|
225
|
+
bracket constructors, and direct Stop construction.
|
|
7
226
|
|
|
8
227
|
## 0.95.0 - 2026-07-11
|
|
9
228
|
|
|
@@ -87,7 +306,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
|
|
87
306
|
|
|
88
307
|
### Changed
|
|
89
308
|
|
|
90
|
-
- Added the
|
|
309
|
+
- Added the checkerboard example to the documentation showcase flow.
|
|
91
310
|
- Consolidated Victor Book attribution for adapted showcase examples into a single documentation note.
|
|
92
311
|
|
|
93
312
|
### Fixed
|
data/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# Sevgi Function
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Sevgi Function contains the supported `Sevgi::F` toolbox shared by Sevgi components and advanced extensions. It is not
|
|
4
|
+
a general-purpose utility library. Nested helper modules organize the facade implementation and are not consumer
|
|
5
|
+
mixins.
|
|
4
6
|
|
|
5
7
|
## Install
|
|
6
8
|
|
|
@@ -17,20 +19,25 @@ require "sevgi/function"
|
|
|
17
19
|
## Example
|
|
18
20
|
|
|
19
21
|
```ruby
|
|
20
|
-
Sevgi::F.
|
|
22
|
+
Sevgi::F.with_precision(12) do
|
|
23
|
+
Sevgi::F.eq?(0.1 + 0.2, 0.3) # => true
|
|
24
|
+
end
|
|
21
25
|
```
|
|
22
26
|
|
|
27
|
+
The facade also provides `.sevgi` file discovery, generated-file output, argv-safe child processes, naming helpers,
|
|
28
|
+
and terminal status output. Use the linked API documentation for exact results and failure contracts.
|
|
29
|
+
|
|
23
30
|
## Ruby compatibility
|
|
24
31
|
|
|
25
|
-
Requires Ruby 3.4.0 or newer. CI verifies Ruby 3.4
|
|
32
|
+
Requires Ruby 3.4.0 or newer. CI verifies the current Ruby 3.4 release and the development Ruby from `.ruby-version`.
|
|
26
33
|
|
|
27
34
|
## Native prerequisites
|
|
28
35
|
|
|
29
|
-
|
|
36
|
+
This gem needs only Ruby and its Ruby dependencies.
|
|
30
37
|
|
|
31
38
|
## Links
|
|
32
39
|
|
|
33
|
-
- Documentation: https://sevgi.roktas.dev
|
|
34
|
-
- API documentation: https://www.rubydoc.info/gems/sevgi-function
|
|
35
|
-
- Source: https://github.com/roktas/sevgi/tree/main/function
|
|
36
|
-
- Changelog: https://github.com/roktas/sevgi/blob/main/CHANGELOG.md
|
|
40
|
+
- Documentation: <https://sevgi.roktas.dev>
|
|
41
|
+
- API documentation: <https://www.rubydoc.info/gems/sevgi-function>
|
|
42
|
+
- Source: <https://github.com/roktas/sevgi/tree/main/function>
|
|
43
|
+
- Changelog: <https://github.com/roktas/sevgi/blob/main/CHANGELOG.md>
|
data/lib/sevgi/core.rb
CHANGED
|
@@ -39,7 +39,7 @@ module Sevgi
|
|
|
39
39
|
end
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
-
# Error raised for internal invariants and implementation paths
|
|
42
|
+
# Error raised for broken internal invariants and unreachable implementation paths.
|
|
43
43
|
PanicError = Class.new(Error) unless defined?(self::PanicError)
|
|
44
44
|
|
|
45
45
|
# Error raised for invalid public API usage.
|
data/lib/sevgi/function/color.rb
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module Sevgi
|
|
4
4
|
module Function
|
|
5
|
-
# ANSI color and style
|
|
5
|
+
# ANSI color and style methods promoted to {Sevgi::F}. This module organizes the facade implementation. It is not a
|
|
6
|
+
# consumer mixin contract.
|
|
6
7
|
module Color
|
|
7
8
|
# Wraps a string in the blue terminal style.
|
|
8
9
|
# @param string [Object] content to style
|
data/lib/sevgi/function/file.rb
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "digest"
|
|
4
3
|
require "fileutils"
|
|
5
4
|
|
|
6
5
|
module Sevgi
|
|
7
6
|
module Function
|
|
8
|
-
# File-system
|
|
7
|
+
# File-system methods promoted to {Sevgi::F}. This module organizes the facade implementation. It is not a consumer
|
|
8
|
+
# mixin contract.
|
|
9
9
|
module File
|
|
10
|
-
# Checks whether a file
|
|
10
|
+
# Checks whether a file differs from the supplied content.
|
|
11
11
|
# @param file [String] file path to compare
|
|
12
12
|
# @param content [String] proposed file content
|
|
13
13
|
# @yield optional normalization filter applied to both old and new content
|
|
14
14
|
# @yieldparam content [String] content to normalize
|
|
15
15
|
# @yieldreturn [String]
|
|
16
16
|
# @return [Boolean] true when the file is missing or content differs
|
|
17
|
-
# @raise [
|
|
17
|
+
# @raise [SystemCallError] when the file cannot be inspected or read
|
|
18
18
|
def changed?(file, content, &filter)
|
|
19
19
|
return true unless ::File.exist?(file)
|
|
20
20
|
|
|
21
21
|
old_content = ::File.read(file)
|
|
22
22
|
old_content, content = [old_content, content].map(&filter) if filter
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
old_content != content
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
# Finds an existing file by exact path or by trying default extensions.
|
|
@@ -29,11 +29,11 @@ module Sevgi
|
|
|
29
29
|
# @param extensions [Array<String>] extensions to try when file has no extension
|
|
30
30
|
# @return [String, nil] matching file path, or nil when no file is found
|
|
31
31
|
def existing(file, extensions)
|
|
32
|
-
return file if ::File.
|
|
32
|
+
return file if ::File.file?(file)
|
|
33
33
|
return nil unless ::File.extname(file).empty?
|
|
34
34
|
return nil if extensions.empty?
|
|
35
35
|
|
|
36
|
-
extensions.map { |ext| "#{file}.#{ext}" }.detect { |file| ::File.
|
|
36
|
+
extensions.map { |ext| "#{file}.#{ext}" }.detect { |file| ::File.file?(file) }
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
# Finds an existing file or raises.
|
|
@@ -79,8 +79,7 @@ module Sevgi
|
|
|
79
79
|
# @yieldparam content [String] old or new content
|
|
80
80
|
# @yieldreturn [String]
|
|
81
81
|
# @return [String, nil] expanded file path when written, otherwise nil
|
|
82
|
-
# @raise [
|
|
83
|
-
# @raise [Errno::ENOENT] when the parent directory does not exist
|
|
82
|
+
# @raise [SystemCallError] when the destination cannot be inspected, read, or written
|
|
84
83
|
def out(content, *paths, &filter)
|
|
85
84
|
if paths.empty?
|
|
86
85
|
::Kernel.puts(content)
|
|
@@ -122,7 +121,7 @@ module Sevgi
|
|
|
122
121
|
# Creates a file and any missing parent directories.
|
|
123
122
|
# @param paths [Array<String>] path components for the file
|
|
124
123
|
# @return [String] touched file path
|
|
125
|
-
# @raise [
|
|
124
|
+
# @raise [SystemCallError] when the file or parent directory cannot be created
|
|
126
125
|
def touch(*paths)
|
|
127
126
|
::File.join(*paths).tap do |path|
|
|
128
127
|
::FileUtils.mkdir_p(::File.dirname(path))
|
|
@@ -10,31 +10,45 @@ module Sevgi
|
|
|
10
10
|
# @return [String] candidate path that matched
|
|
11
11
|
# @!attribute [r] dir
|
|
12
12
|
# @return [String] directory where the match was found
|
|
13
|
-
Location = Data.define(:file, :slug, :dir)
|
|
13
|
+
Location = Data.define(:file, :slug, :dir) do
|
|
14
|
+
# Creates an owned immutable location snapshot.
|
|
15
|
+
# @param file [String] absolute matching file path
|
|
16
|
+
# @param slug [String] candidate path that matched
|
|
17
|
+
# @param dir [String] directory where the match was found
|
|
18
|
+
# @return [void]
|
|
19
|
+
def initialize(file:, slug:, dir:)
|
|
20
|
+
super(file: file.dup.freeze, slug: slug.dup.freeze, dir: dir.dup.freeze)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private_class_method :[]
|
|
24
|
+
end
|
|
14
25
|
|
|
15
|
-
# Locates one of several candidate files by walking upward from
|
|
26
|
+
# Locates one of several candidate files by walking upward from an immutable configuration snapshot. Each call
|
|
27
|
+
# observes the filesystem again and returns an owned immutable {Location} snapshot.
|
|
16
28
|
class Locate
|
|
17
29
|
# @overload call(paths, start = Dir.pwd, exclude: nil, &block)
|
|
18
30
|
# Builds a locator and runs it.
|
|
19
31
|
# @param paths [Array<String>, String] candidate file paths
|
|
20
32
|
# @param start [String] directory where lookup starts
|
|
21
33
|
# @param exclude [Array<String>, String, nil] paths ignored during lookup
|
|
22
|
-
# @yield optional matcher used instead of file
|
|
34
|
+
# @yield optional matcher used instead of built-in file checks
|
|
23
35
|
# @yieldparam path [String] candidate path
|
|
24
36
|
# @yieldreturn [Boolean]
|
|
25
37
|
# @return [Sevgi::Function::Location, nil] found location, or nil
|
|
38
|
+
# @raise [Errno::ENOENT] when the start directory does not exist
|
|
39
|
+
# @raise [Errno::ENOTDIR] when the start path is not a directory
|
|
26
40
|
def self.call(*, **, &block) = new(*, **).call(&block)
|
|
27
41
|
|
|
28
|
-
# Returns candidate paths.
|
|
29
|
-
# @return [Array<String>]
|
|
42
|
+
# Returns the frozen owned candidate paths.
|
|
43
|
+
# @return [Array<String>] frozen candidate path strings
|
|
30
44
|
attr_reader :paths
|
|
31
45
|
|
|
32
|
-
# Returns the absolute start directory.
|
|
33
|
-
# @return [String]
|
|
46
|
+
# Returns the frozen absolute start directory.
|
|
47
|
+
# @return [String] frozen absolute path
|
|
34
48
|
attr_reader :start
|
|
35
49
|
|
|
36
|
-
# Returns
|
|
37
|
-
# @return [Array<String>, nil]
|
|
50
|
+
# Returns the frozen owned paths ignored during lookup.
|
|
51
|
+
# @return [Array<String>, nil] frozen absolute path strings, or nil
|
|
38
52
|
attr_reader :exclude
|
|
39
53
|
|
|
40
54
|
# Builds an upward file locator.
|
|
@@ -43,13 +57,14 @@ module Sevgi
|
|
|
43
57
|
# @param exclude [Array<String>, String, nil] paths ignored during lookup after absolute expansion
|
|
44
58
|
# @return [void]
|
|
45
59
|
def initialize(paths, start = ::Dir.pwd, exclude: nil)
|
|
46
|
-
@paths = Array(paths)
|
|
47
|
-
@start = ::File.expand_path(start)
|
|
48
|
-
@exclude = [*exclude].map { ::File.expand_path(it) } unless exclude.nil?
|
|
60
|
+
@paths = Array(paths).map { it.dup.freeze }.freeze
|
|
61
|
+
@start = ::File.expand_path(start).freeze
|
|
62
|
+
@exclude = [*exclude].map { ::File.expand_path(it).freeze }.freeze unless exclude.nil?
|
|
63
|
+
freeze
|
|
49
64
|
end
|
|
50
65
|
|
|
51
66
|
# Runs the upward lookup.
|
|
52
|
-
# @yield optional matcher used instead of file
|
|
67
|
+
# @yield optional matcher used instead of built-in file checks
|
|
53
68
|
# @yieldparam path [String] absolute candidate path
|
|
54
69
|
# @yieldreturn [Boolean]
|
|
55
70
|
# @note Absolute exclusions are applied before the default or custom matcher.
|
|
@@ -64,7 +79,7 @@ module Sevgi
|
|
|
64
79
|
|
|
65
80
|
slug, file = found
|
|
66
81
|
|
|
67
|
-
return Location
|
|
82
|
+
return Location.new(file:, slug:, dir: here)
|
|
68
83
|
end
|
|
69
84
|
end
|
|
70
85
|
|
|
@@ -90,7 +105,8 @@ module Sevgi
|
|
|
90
105
|
candidate = ::File.expand_path(path, here)
|
|
91
106
|
next if excluded?(candidate)
|
|
92
107
|
|
|
93
|
-
|
|
108
|
+
matched = block ? block.call(candidate) : ::File.file?(candidate)
|
|
109
|
+
return [path, candidate] if matched
|
|
94
110
|
end
|
|
95
111
|
|
|
96
112
|
nil
|
|
@@ -109,6 +125,8 @@ module Sevgi
|
|
|
109
125
|
# @param extension [String] default extension added before lookup
|
|
110
126
|
# @return [Sevgi::Function::Location] found location
|
|
111
127
|
# @raise [Sevgi::Error] when no matching file exists
|
|
128
|
+
# @raise [Errno::ENOENT] when the start directory does not exist
|
|
129
|
+
# @raise [Errno::ENOTDIR] when the start path is not a directory
|
|
112
130
|
def self.locate(filename, start, exclude: nil, extension: EXTENSION)
|
|
113
131
|
Locate.(F.qualify(filename, extension), start, exclude:).tap do |path|
|
|
114
132
|
Error.("Cannot load a file matching: #{filename}") unless path
|
data/lib/sevgi/function/math.rb
CHANGED
|
@@ -2,14 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
module Sevgi
|
|
4
4
|
module Function
|
|
5
|
-
# Numeric and trigonometric
|
|
5
|
+
# Numeric and trigonometric methods promoted to {Sevgi::F}. This module owns the public thread-local
|
|
6
|
+
# {Math.precision} configuration but is not otherwise a consumer mixin contract.
|
|
6
7
|
module Math
|
|
7
8
|
# Default decimal precision used by approximate comparisons.
|
|
8
9
|
PRECISION = 6
|
|
9
10
|
|
|
10
11
|
PRECISION_KEY = :sevgi_function_math_precision
|
|
12
|
+
QUADRANT_COSINES = [1.0, 0.0, -1.0, 0.0].freeze
|
|
13
|
+
QUADRANT_SINES = [0.0, 1.0, 0.0, -1.0].freeze
|
|
11
14
|
|
|
12
|
-
private_constant :PRECISION_KEY
|
|
15
|
+
private_constant :PRECISION_KEY, :QUADRANT_COSINES, :QUADRANT_SINES
|
|
13
16
|
|
|
14
17
|
# Returns the current thread's default numeric precision.
|
|
15
18
|
# @return [Integer] current thread precision, or {PRECISION} when no override is set
|
|
@@ -22,60 +25,78 @@ module Sevgi
|
|
|
22
25
|
# Sets or clears the current thread's default numeric precision.
|
|
23
26
|
# @param precision [Integer, nil] precision override, or nil to return to {PRECISION}
|
|
24
27
|
# @return [Integer, nil] assigned precision
|
|
28
|
+
# @raise [Sevgi::ArgumentError] when precision is not an Integer or nil
|
|
25
29
|
def self.precision=(precision)
|
|
30
|
+
unless precision.nil? || precision.is_a?(::Integer)
|
|
31
|
+
ArgumentError.("Precision must be an Integer or nil: #{precision.inspect}")
|
|
32
|
+
end
|
|
33
|
+
|
|
26
34
|
Thread.current.thread_variable_set(PRECISION_KEY, precision)
|
|
27
35
|
end
|
|
28
36
|
|
|
29
37
|
# Returns the inverse cosine in degrees.
|
|
30
38
|
# @param value [Numeric] cosine value
|
|
31
39
|
# @return [Float]
|
|
32
|
-
# @raise [
|
|
33
|
-
|
|
40
|
+
# @raise [Sevgi::ArgumentError] when value is not a finite real number
|
|
41
|
+
# @raise [::Math::DomainError] when value is outside -1..1
|
|
42
|
+
def acos(value) = to_degrees(::Math.acos(finite_real(:value, value)))
|
|
34
43
|
|
|
35
44
|
# Returns the inverse cotangent in degrees.
|
|
36
45
|
# @param value [Numeric] cotangent value
|
|
37
46
|
# @return [Float]
|
|
38
|
-
|
|
47
|
+
# @raise [Sevgi::ArgumentError] when value is not a finite real number
|
|
48
|
+
def acot(value) = 90.0 - to_degrees(::Math.atan(finite_real(:value, value)))
|
|
39
49
|
|
|
40
50
|
# Rounds a float with an explicit or thread-local precision.
|
|
41
51
|
# @param float [Numeric] value to round
|
|
42
52
|
# @param precision [Integer, nil] explicit precision, or nil to use {Math.precision}
|
|
43
53
|
# @return [Numeric] rounded value
|
|
44
|
-
# @raise [
|
|
54
|
+
# @raise [Sevgi::ArgumentError] when value is not finite real or precision is not an Integer or nil
|
|
45
55
|
def approx(float, precision = nil)
|
|
46
|
-
|
|
56
|
+
precision = precision.nil? ? Function::Math.precision : valid_precision(precision)
|
|
57
|
+
valid_real(:value, float).round(precision)
|
|
47
58
|
end
|
|
48
59
|
|
|
49
60
|
# Returns the inverse sine in degrees.
|
|
50
61
|
# @param value [Numeric] sine value
|
|
51
62
|
# @return [Float]
|
|
52
|
-
# @raise [
|
|
53
|
-
|
|
63
|
+
# @raise [Sevgi::ArgumentError] when value is not a finite real number
|
|
64
|
+
# @raise [::Math::DomainError] when value is outside -1..1
|
|
65
|
+
def asin(value) = to_degrees(::Math.asin(finite_real(:value, value)))
|
|
54
66
|
|
|
55
67
|
# Returns the inverse tangent in degrees.
|
|
56
68
|
# @param value [Numeric] tangent value
|
|
57
69
|
# @return [Float]
|
|
58
|
-
|
|
70
|
+
# @raise [Sevgi::ArgumentError] when value is not a finite real number
|
|
71
|
+
def atan(value) = to_degrees(::Math.atan(finite_real(:value, value)))
|
|
59
72
|
|
|
60
73
|
# Returns the quadrant-aware inverse tangent in degrees.
|
|
61
74
|
# @param y [Numeric] y component
|
|
62
75
|
# @param x [Numeric] x component
|
|
63
76
|
# @return [Float]
|
|
64
|
-
|
|
77
|
+
# @raise [Sevgi::ArgumentError] when an operand is not a finite real number
|
|
78
|
+
def atan2(y, x) = to_degrees(::Math.atan2(finite_real(:y, y), finite_real(:x, x)))
|
|
65
79
|
|
|
66
|
-
# Returns the cosine of an angle expressed in degrees.
|
|
80
|
+
# Returns the cosine of an angle expressed in degrees. Integer quarter turns return exact `-1.0`, `0.0`, or
|
|
81
|
+
# `1.0`. Other angles use Ruby's floating-point Math implementation.
|
|
67
82
|
# @param degrees [Numeric] angle in degrees
|
|
68
83
|
# @return [Float]
|
|
69
|
-
|
|
84
|
+
# @raise [Sevgi::ArgumentError] when degrees is not a finite real number
|
|
85
|
+
def cos(degrees)
|
|
86
|
+
degrees = finite_real(:degrees, degrees)
|
|
87
|
+
quadrant_value(degrees, QUADRANT_COSINES) { ::Math.cos(radians(degrees)) }
|
|
88
|
+
end
|
|
70
89
|
|
|
71
90
|
# Returns the cotangent of an angle expressed in degrees.
|
|
72
91
|
# @param degrees [Numeric] angle in degrees
|
|
73
92
|
# @return [Float]
|
|
93
|
+
# @raise [Sevgi::ArgumentError] when degrees is not a finite real number
|
|
74
94
|
def cot(degrees) = 1.0 / ::Math.tan(to_radians(degrees))
|
|
75
95
|
|
|
76
96
|
# Counts complete divisions in a length.
|
|
77
|
-
#
|
|
78
|
-
# @param
|
|
97
|
+
# A positive quotient one floating-point step below the next integer counts as that integer.
|
|
98
|
+
# @param length [Numeric] finite real total length
|
|
99
|
+
# @param division [Numeric] finite real, non-zero division size
|
|
79
100
|
# @return [Integer]
|
|
80
101
|
# @raise [Sevgi::ArgumentError] when an operand is not a finite Numeric or division is zero
|
|
81
102
|
def count(length, division)
|
|
@@ -83,7 +104,11 @@ module Sevgi
|
|
|
83
104
|
divisor = finite_real(:division, division)
|
|
84
105
|
ArgumentError.("Division must not be zero") if divisor.zero?
|
|
85
106
|
|
|
86
|
-
|
|
107
|
+
quotient = length / divisor
|
|
108
|
+
whole = quotient.to_i
|
|
109
|
+
candidate = whole + 1
|
|
110
|
+
|
|
111
|
+
quotient > whole && quotient.next_float == candidate ? candidate : whole
|
|
87
112
|
end
|
|
88
113
|
|
|
89
114
|
# Compares two numeric values after approximate rounding.
|
|
@@ -91,6 +116,7 @@ module Sevgi
|
|
|
91
116
|
# @param right [Numeric] right operand
|
|
92
117
|
# @param precision [Integer, nil] explicit precision, or nil to use {Math.precision}
|
|
93
118
|
# @return [Boolean]
|
|
119
|
+
# @raise [Sevgi::ArgumentError] when an operand is not finite real or precision is invalid
|
|
94
120
|
def eq?(left, right, precision: nil) = approx(left, precision) == approx(right, precision)
|
|
95
121
|
|
|
96
122
|
# Checks whether the rounded left operand is greater than or equal to the rounded right operand.
|
|
@@ -98,6 +124,7 @@ module Sevgi
|
|
|
98
124
|
# @param right [Numeric] right operand
|
|
99
125
|
# @param precision [Integer, nil] explicit precision, or nil to use {Math.precision}
|
|
100
126
|
# @return [Boolean]
|
|
127
|
+
# @raise [Sevgi::ArgumentError] when an operand is not finite real or precision is invalid
|
|
101
128
|
def ge?(left, right, precision: nil) = approx(left, precision) >= approx(right, precision)
|
|
102
129
|
|
|
103
130
|
# Checks whether the rounded left operand is greater than the rounded right operand.
|
|
@@ -105,6 +132,7 @@ module Sevgi
|
|
|
105
132
|
# @param right [Numeric] right operand
|
|
106
133
|
# @param precision [Integer, nil] explicit precision, or nil to use {Math.precision}
|
|
107
134
|
# @return [Boolean]
|
|
135
|
+
# @raise [Sevgi::ArgumentError] when an operand is not finite real or precision is invalid
|
|
108
136
|
def gt?(left, right, precision: nil) = approx(left, precision) > approx(right, precision)
|
|
109
137
|
|
|
110
138
|
# Checks whether the rounded left operand is less than or equal to the rounded right operand.
|
|
@@ -112,6 +140,7 @@ module Sevgi
|
|
|
112
140
|
# @param right [Numeric] right operand
|
|
113
141
|
# @param precision [Integer, nil] explicit precision, or nil to use {Math.precision}
|
|
114
142
|
# @return [Boolean]
|
|
143
|
+
# @raise [Sevgi::ArgumentError] when an operand is not finite real or precision is invalid
|
|
115
144
|
def le?(left, right, precision: nil) = approx(left, precision) <= approx(right, precision)
|
|
116
145
|
|
|
117
146
|
# Checks whether the rounded left operand is less than the rounded right operand.
|
|
@@ -119,33 +148,46 @@ module Sevgi
|
|
|
119
148
|
# @param right [Numeric] right operand
|
|
120
149
|
# @param precision [Integer, nil] explicit precision, or nil to use {Math.precision}
|
|
121
150
|
# @return [Boolean]
|
|
151
|
+
# @raise [Sevgi::ArgumentError] when an operand is not finite real or precision is invalid
|
|
122
152
|
def lt?(left, right, precision: nil) = approx(left, precision) < approx(right, precision)
|
|
123
153
|
|
|
124
154
|
# Rounds a value only when precision is present.
|
|
125
155
|
# @param float [Numeric] value to round
|
|
126
156
|
# @param precision [Integer, nil] explicit precision, or nil to return float unchanged
|
|
127
157
|
# @return [Numeric]
|
|
128
|
-
|
|
158
|
+
# @raise [Sevgi::ArgumentError] when value is not finite real or precision is not an Integer or nil
|
|
159
|
+
def round(float, precision)
|
|
160
|
+
number = valid_real(:value, float)
|
|
161
|
+
precision.nil? ? number : number.round(valid_precision(precision))
|
|
162
|
+
end
|
|
129
163
|
|
|
130
|
-
# Returns the sine of an angle expressed in degrees.
|
|
164
|
+
# Returns the sine of an angle expressed in degrees. Integer quarter turns return exact `-1.0`, `0.0`, or `1.0`.
|
|
165
|
+
# other angles use Ruby's floating-point Math implementation.
|
|
131
166
|
# @param degrees [Numeric] angle in degrees
|
|
132
167
|
# @return [Float]
|
|
133
|
-
|
|
168
|
+
# @raise [Sevgi::ArgumentError] when degrees is not a finite real number
|
|
169
|
+
def sin(degrees)
|
|
170
|
+
degrees = finite_real(:degrees, degrees)
|
|
171
|
+
quadrant_value(degrees, QUADRANT_SINES) { ::Math.sin(radians(degrees)) }
|
|
172
|
+
end
|
|
134
173
|
|
|
135
174
|
# Returns the tangent of an angle expressed in degrees.
|
|
136
175
|
# @param degrees [Numeric] angle in degrees
|
|
137
176
|
# @return [Float]
|
|
177
|
+
# @raise [Sevgi::ArgumentError] when degrees is not a finite real number
|
|
138
178
|
def tan(degrees) = ::Math.tan(to_radians(degrees))
|
|
139
179
|
|
|
140
180
|
# Converts radians to degrees.
|
|
141
181
|
# @param radians [Numeric] angle in radians
|
|
142
182
|
# @return [Float]
|
|
143
|
-
|
|
183
|
+
# @raise [Sevgi::ArgumentError] when radians is not a finite real number
|
|
184
|
+
def to_degrees(radians) = finite_real(:radians, radians) * 180 / ::Math::PI
|
|
144
185
|
|
|
145
186
|
# Converts degrees to radians.
|
|
146
187
|
# @param degrees [Numeric] angle in degrees
|
|
147
188
|
# @return [Float]
|
|
148
|
-
|
|
189
|
+
# @raise [Sevgi::ArgumentError] when degrees is not a finite real number
|
|
190
|
+
def to_radians(degrees) = radians(finite_real(:degrees, degrees))
|
|
149
191
|
|
|
150
192
|
# Runs a block with a current-thread precision override.
|
|
151
193
|
# @param precision [Integer, nil] scoped precision, or nil to use {PRECISION}
|
|
@@ -153,6 +195,10 @@ module Sevgi
|
|
|
153
195
|
# @yieldreturn [Object]
|
|
154
196
|
# @return [Object] block return value
|
|
155
197
|
# @raise [Sevgi::ArgumentError] when no block is given
|
|
198
|
+
# @raise [Sevgi::ArgumentError] when precision is not an Integer or nil
|
|
199
|
+
# @example Compare values under a temporary precision
|
|
200
|
+
# Sevgi::F.with_precision(2) { Sevgi::F.eq?(1.001, 1.0) } # => true
|
|
201
|
+
# Sevgi::Function::Math.precision # => 6
|
|
156
202
|
def with_precision(precision, &block)
|
|
157
203
|
ArgumentError.("Block required") unless block
|
|
158
204
|
|
|
@@ -167,19 +213,20 @@ module Sevgi
|
|
|
167
213
|
# @param value [Numeric] value to check
|
|
168
214
|
# @param precision [Integer, nil] explicit precision, or nil to use {Math.precision}
|
|
169
215
|
# @return [Boolean]
|
|
216
|
+
# @raise [Sevgi::ArgumentError] when value is not finite real or precision is invalid
|
|
170
217
|
def zero?(value, precision: nil) = eq?(value, 0.0, precision:)
|
|
171
218
|
|
|
172
219
|
private
|
|
173
220
|
|
|
174
221
|
def finite_real(field, value)
|
|
175
|
-
unless value.is_a?(::Numeric)
|
|
176
|
-
ArgumentError.("#{field} must be a finite Numeric: #{value.inspect}")
|
|
222
|
+
unless value.is_a?(::Numeric) && !value.is_a?(::Complex)
|
|
223
|
+
ArgumentError.("#{field} must be a finite real Numeric: #{value.inspect}")
|
|
177
224
|
end
|
|
178
225
|
|
|
179
226
|
number = begin
|
|
180
227
|
value.to_f
|
|
181
228
|
rescue ::StandardError => e
|
|
182
|
-
ArgumentError.("#{field} must be a finite Numeric: #{value.inspect} (#{e.message})")
|
|
229
|
+
ArgumentError.("#{field} must be a finite real Numeric: #{value.inspect} (#{e.message})")
|
|
183
230
|
end
|
|
184
231
|
|
|
185
232
|
ArgumentError.("#{field} must be finite: #{value.inspect}") unless number.is_a?(::Float) && number.finite?
|
|
@@ -187,6 +234,26 @@ module Sevgi
|
|
|
187
234
|
number
|
|
188
235
|
end
|
|
189
236
|
|
|
237
|
+
def quadrant_value(degrees, values)
|
|
238
|
+
turn = degrees % 360
|
|
239
|
+
return yield unless (turn % 90).zero?
|
|
240
|
+
|
|
241
|
+
values[(turn / 90).to_i]
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def radians(degrees) = degrees / 180 * ::Math::PI
|
|
245
|
+
|
|
246
|
+
def valid_precision(precision)
|
|
247
|
+
return precision if precision.is_a?(::Integer)
|
|
248
|
+
|
|
249
|
+
ArgumentError.("Precision must be an Integer or nil: #{precision.inspect}")
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def valid_real(field, value)
|
|
253
|
+
finite_real(field, value)
|
|
254
|
+
value
|
|
255
|
+
end
|
|
256
|
+
|
|
190
257
|
end
|
|
191
258
|
|
|
192
259
|
extend Math
|
data/lib/sevgi/function/shell.rb
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "open3"
|
|
4
|
+
require "shellwords"
|
|
4
5
|
|
|
5
6
|
module Sevgi
|
|
6
7
|
module Function
|
|
7
|
-
#
|
|
8
|
+
# Command methods promoted to {Sevgi::F}. This module owns the public immutable {Shell::Result} value but is not
|
|
9
|
+
# otherwise a consumer mixin contract.
|
|
8
10
|
module Shell
|
|
9
11
|
# Checks whether a program exists and is executable.
|
|
10
12
|
# @param program [Object] program name, absolute path, or relative slash-containing path
|
|
11
13
|
# @return [Boolean] true when an executable regular file is found
|
|
12
|
-
# @note PATH is evaluated on every call
|
|
14
|
+
# @note PATH is evaluated on every call. Empty PATH segments mean the current directory.
|
|
13
15
|
def executable?(program)
|
|
14
16
|
program = program.to_s
|
|
15
17
|
return false if program.empty?
|
|
@@ -24,30 +26,68 @@ module Sevgi
|
|
|
24
26
|
# @param args [Array<Object>] command arguments
|
|
25
27
|
# @return [nil]
|
|
26
28
|
# @raise [Sevgi::Error] when the program cannot be found in PATH
|
|
27
|
-
# @note The first argument is checked as one exact argv entry
|
|
29
|
+
# @note The first argument is checked as one exact argv entry. It is never shell-split.
|
|
28
30
|
def executable!(*args)
|
|
29
31
|
program = args.first.to_s
|
|
30
32
|
Error.("Missing executable: #{program}") unless executable?(program)
|
|
31
33
|
end
|
|
32
34
|
|
|
33
|
-
#
|
|
34
|
-
|
|
35
|
+
# Immutable result returned by shell commands.
|
|
36
|
+
#
|
|
37
|
+
# @example Inspect a command result
|
|
38
|
+
# require "rbconfig"
|
|
39
|
+
# result = Sevgi::F.sh(RbConfig.ruby, "-e", "puts 42")
|
|
40
|
+
# result.ok? # => true
|
|
41
|
+
# result.outline # => "42"
|
|
42
|
+
Result = Data.define(:args, :outs, :errs, :exit_code, :signal) do
|
|
35
43
|
# @!attribute [r] args
|
|
36
|
-
# @return [Array<String>] command arguments
|
|
44
|
+
# @return [Array<String>] frozen command arguments
|
|
37
45
|
# @!attribute [r] outs
|
|
38
|
-
# @return [Array<String>] captured stdout lines
|
|
46
|
+
# @return [Array<String>] frozen captured stdout lines
|
|
39
47
|
# @!attribute [r] errs
|
|
40
|
-
# @return [Array<String>] captured stderr lines
|
|
48
|
+
# @return [Array<String>] frozen captured stderr lines
|
|
41
49
|
# @!attribute [r] exit_code
|
|
42
50
|
# @return [Integer, nil] process exit code
|
|
51
|
+
# @!attribute [r] signal
|
|
52
|
+
# @return [Integer, nil] terminating signal number
|
|
53
|
+
|
|
54
|
+
# Creates an owned result snapshot.
|
|
55
|
+
# @param args [Array<Object>] command arguments
|
|
56
|
+
# @param outs [Array<String>] captured stdout lines
|
|
57
|
+
# @param errs [Array<String>] captured stderr lines
|
|
58
|
+
# @param exit_code [Integer, nil] process exit code
|
|
59
|
+
# @param signal [Integer, nil] terminating signal number
|
|
60
|
+
# @return [void]
|
|
61
|
+
def initialize(args:, outs:, errs:, exit_code:, signal:)
|
|
62
|
+
super(
|
|
63
|
+
args: args.map { it.to_s.dup.freeze }.freeze,
|
|
64
|
+
outs: outs.map { it.dup.freeze }.freeze,
|
|
65
|
+
errs: errs.map { it.dup.freeze }.freeze,
|
|
66
|
+
exit_code:,
|
|
67
|
+
signal:
|
|
68
|
+
)
|
|
69
|
+
end
|
|
43
70
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
71
|
+
private_class_method :[]
|
|
72
|
+
|
|
73
|
+
# Returns captured output with one blank line between non-empty streams.
|
|
74
|
+
# Captured lines are joined with one newline and are not otherwise trimmed.
|
|
75
|
+
# @example Combine standard output and standard error
|
|
76
|
+
# result = Sevgi::Function::Shell::Result.new(
|
|
77
|
+
# args: ["tool"], outs: ["output"], errs: ["warning"], exit_code: 0, signal: nil
|
|
78
|
+
# )
|
|
79
|
+
# result.all # => "output\n\nwarning"
|
|
80
|
+
# @return [String] combined output, or an empty string when neither stream contains lines
|
|
81
|
+
def all
|
|
82
|
+
return err if outs.empty?
|
|
83
|
+
return out if errs.empty?
|
|
84
|
+
|
|
85
|
+
"#{out}\n\n#{err}"
|
|
86
|
+
end
|
|
47
87
|
|
|
48
88
|
# Returns the command as a shell-like display string.
|
|
49
89
|
# @return [String]
|
|
50
|
-
def cmd =
|
|
90
|
+
def cmd = ::Shellwords.join(args)
|
|
51
91
|
|
|
52
92
|
# Returns captured stderr as a string.
|
|
53
93
|
# @return [String]
|
|
@@ -59,7 +99,7 @@ module Sevgi
|
|
|
59
99
|
|
|
60
100
|
# Reports whether the command exited successfully.
|
|
61
101
|
# @return [Boolean]
|
|
62
|
-
def ok? = exit_code
|
|
102
|
+
def ok? = !exit_code.nil? && exit_code.zero?
|
|
63
103
|
|
|
64
104
|
# Returns captured stdout as a string.
|
|
65
105
|
# @return [String]
|
|
@@ -69,15 +109,18 @@ module Sevgi
|
|
|
69
109
|
# @return [String, nil]
|
|
70
110
|
def outline = outs.first
|
|
71
111
|
|
|
72
|
-
#
|
|
73
|
-
# @return [
|
|
74
|
-
def
|
|
112
|
+
# Reports whether the command was terminated by a signal.
|
|
113
|
+
# @return [Boolean]
|
|
114
|
+
def signaled? = !signal.nil?
|
|
75
115
|
end
|
|
76
116
|
|
|
77
117
|
# Shared process-global SIGINT state for overlapping shell runners.
|
|
78
118
|
# @api private
|
|
79
119
|
module Signals
|
|
80
|
-
Entry = Data.define(:runner, :pid)
|
|
120
|
+
Entry = Data.define(:runner, :pid) do
|
|
121
|
+
private_class_method :[]
|
|
122
|
+
end
|
|
123
|
+
|
|
81
124
|
WAKE = "."
|
|
82
125
|
|
|
83
126
|
private_constant :WAKE
|
|
@@ -183,21 +226,29 @@ module Sevgi
|
|
|
183
226
|
# @yield optional content writer for stdin
|
|
184
227
|
# @yieldreturn [String, nil]
|
|
185
228
|
# @return [Sevgi::Function::Shell::Result]
|
|
229
|
+
# @raise [Sevgi::ArgumentError] when no command is given
|
|
186
230
|
# @raise [Errno::ENOENT] when the executable cannot be spawned
|
|
187
231
|
def call(*args, &input)
|
|
188
|
-
|
|
232
|
+
ArgumentError.("Shell command required") if args.empty?
|
|
189
233
|
|
|
190
234
|
@coathooks = 0
|
|
191
|
-
outs, errs, status = Open3.popen3(*args) do |stdin, stdout, stderr, wait_thread|
|
|
235
|
+
outs, errs, status = Open3.popen3(*command(args)) do |stdin, stdout, stderr, wait_thread|
|
|
192
236
|
capture(stdin, stdout, stderr, wait_thread, &input)
|
|
193
237
|
end
|
|
194
238
|
|
|
195
|
-
Result.new(args
|
|
239
|
+
Result.new(args:, outs:, errs:, exit_code: status.exitstatus, signal: status.termsig)
|
|
196
240
|
end
|
|
197
241
|
|
|
198
242
|
private
|
|
199
243
|
|
|
200
|
-
|
|
244
|
+
def command(args)
|
|
245
|
+
args = args.dup
|
|
246
|
+
index = args.first.is_a?(::Hash) ? 1 : 0
|
|
247
|
+
args[index] = [args[index], args[index]] unless args[index].is_a?(::Array)
|
|
248
|
+
args
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# rubocop:disable-next Lint/RescueException
|
|
201
252
|
def capture(stdin, stdout, stderr, wait_thread, &input)
|
|
202
253
|
registered = false
|
|
203
254
|
Signals.register(self, wait_thread.pid)
|
|
@@ -212,7 +263,6 @@ module Sevgi
|
|
|
212
263
|
close_input(stdin)
|
|
213
264
|
Signals.unregister(self) if registered
|
|
214
265
|
end
|
|
215
|
-
# rubocop:enable Lint/RescueException
|
|
216
266
|
|
|
217
267
|
def start_readers(stdout, stderr)
|
|
218
268
|
[
|
|
@@ -286,14 +336,17 @@ module Sevgi
|
|
|
286
336
|
end
|
|
287
337
|
end
|
|
288
338
|
|
|
339
|
+
private_constant :Runner
|
|
340
|
+
|
|
289
341
|
# @overload sh(*args, &block)
|
|
290
342
|
# Runs a command and captures stdout, stderr, and exit status.
|
|
291
343
|
# @param args [Array<String>] command and arguments
|
|
292
344
|
# @yield optional stdin producer, evaluated once after output readers start
|
|
293
|
-
# @yieldreturn [String, nil] content to write to stdin
|
|
345
|
+
# @yieldreturn [String, nil] content to write to stdin. Nil writes nothing
|
|
294
346
|
# @return [Sevgi::Function::Shell::Result]
|
|
347
|
+
# @raise [Sevgi::ArgumentError] when no command is given
|
|
295
348
|
# @raise [SystemCallError] when the executable cannot be spawned or process pipes cannot be opened
|
|
296
|
-
# @raise [StandardError] when the input block raises
|
|
349
|
+
# @raise [StandardError] when the input block raises. The child is terminated and reaped before propagation
|
|
297
350
|
# @note The child's stdin is closed after the input block. During execution, the first SIGINT sends TERM to the
|
|
298
351
|
# child process and the second SIGINT as KILL to each active child outside trap context, then restores the
|
|
299
352
|
# previous handler.
|
|
@@ -302,11 +355,12 @@ module Sevgi
|
|
|
302
355
|
# Runs a command, requiring both executable lookup and successful exit status.
|
|
303
356
|
# @param args [Array<String>] command and arguments
|
|
304
357
|
# @yield optional stdin producer, evaluated once after output readers start
|
|
305
|
-
# @yieldreturn [String, nil] content to write to stdin
|
|
358
|
+
# @yieldreturn [String, nil] content to write to stdin. Nil writes nothing
|
|
306
359
|
# @return [Sevgi::Function::Shell::Result]
|
|
360
|
+
# @raise [Sevgi::ArgumentError] when no command is given
|
|
307
361
|
# @raise [Sevgi::Error] when the executable is missing or the command fails
|
|
308
362
|
# @raise [SystemCallError] when the executable cannot be spawned or process pipes cannot be opened
|
|
309
|
-
# @raise [StandardError] when the input block raises
|
|
363
|
+
# @raise [StandardError] when the input block raises. The child is terminated and reaped before propagation
|
|
310
364
|
# @note The child's stdin is closed after the input block. During execution, the first SIGINT sends TERM to the
|
|
311
365
|
# child process and the second SIGINT as KILL to each active child outside trap context, then restores the previous
|
|
312
366
|
# handler.
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module Sevgi
|
|
4
4
|
module Function
|
|
5
|
-
# String
|
|
5
|
+
# String methods promoted to {Sevgi::F}. This module organizes the facade implementation. It is not a consumer mixin
|
|
6
|
+
# contract.
|
|
6
7
|
module String
|
|
7
8
|
# Returns the final constant name segment from a module path.
|
|
8
9
|
# @param path [Object] module, class, or string-like path
|
|
@@ -19,9 +20,11 @@ module Sevgi
|
|
|
19
20
|
|
|
20
21
|
extend String
|
|
21
22
|
|
|
22
|
-
#
|
|
23
|
+
# English pluralization promoted to {Sevgi::F}. This module organizes the facade implementation. It is not a
|
|
24
|
+
# consumer mixin contract.
|
|
23
25
|
module Pluralize
|
|
24
|
-
# Words
|
|
26
|
+
# Words excluded from pluralization.
|
|
27
|
+
# @api private
|
|
25
28
|
UNCOUNTABLES = %w[
|
|
26
29
|
equipment
|
|
27
30
|
fish
|
|
@@ -36,6 +39,7 @@ module Sevgi
|
|
|
36
39
|
.freeze
|
|
37
40
|
|
|
38
41
|
# Singular-to-plural forms that do not follow suffix rules.
|
|
42
|
+
# @api private
|
|
39
43
|
IRREGULARS = Hash[
|
|
40
44
|
*%w[
|
|
41
45
|
child
|
|
@@ -59,9 +63,11 @@ module Sevgi
|
|
|
59
63
|
.freeze
|
|
60
64
|
|
|
61
65
|
# Plural forms already accepted as plural.
|
|
66
|
+
# @api private
|
|
62
67
|
PLURALS = IRREGULARS.invert.freeze
|
|
63
68
|
|
|
64
69
|
# Ordered suffix replacement rules.
|
|
70
|
+
# @api private
|
|
65
71
|
RULES = [
|
|
66
72
|
[/(quiz)$/i, "\\1zes"],
|
|
67
73
|
[/^(oxen)$/i, "\\1"],
|
|
@@ -84,16 +90,20 @@ module Sevgi
|
|
|
84
90
|
[/^(ax|test)is$/i, "\\1es"],
|
|
85
91
|
[/s$/i, "s"],
|
|
86
92
|
[/$/, "s"]
|
|
87
|
-
]
|
|
93
|
+
]
|
|
94
|
+
.each(&:freeze)
|
|
95
|
+
.freeze
|
|
96
|
+
|
|
97
|
+
private_constant :IRREGULARS, :PLURALS, :RULES, :UNCOUNTABLES
|
|
88
98
|
|
|
89
99
|
# Pluralizes an English word using a small built-in rule set.
|
|
90
100
|
# @param word [Object] word to pluralize
|
|
91
101
|
# @return [String]
|
|
92
|
-
# @example
|
|
93
|
-
# F.pluralize("post") # => "posts"
|
|
94
|
-
# F.pluralize("octopus") # => "octopi"
|
|
95
|
-
# F.pluralize("sheep") # => "sheep"
|
|
96
|
-
# F.pluralize("CamelOctopus") # => "CamelOctopi"
|
|
102
|
+
# @example Pluralize through the library facade
|
|
103
|
+
# Sevgi::F.pluralize("post") # => "posts"
|
|
104
|
+
# Sevgi::F.pluralize("octopus") # => "octopi"
|
|
105
|
+
# Sevgi::F.pluralize("sheep") # => "sheep"
|
|
106
|
+
# Sevgi::F.pluralize("CamelOctopus") # => "CamelOctopi"
|
|
97
107
|
def pluralize(word)
|
|
98
108
|
result = word.to_s.dup
|
|
99
109
|
|
data/lib/sevgi/function/ui.rb
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module Sevgi
|
|
4
4
|
module Function
|
|
5
|
-
#
|
|
5
|
+
# Terminal status methods promoted to {Sevgi::F}. This module organizes the facade implementation. It is not a
|
|
6
|
+
# consumer mixin contract.
|
|
6
7
|
module UI
|
|
7
8
|
# Reports an in-progress status message.
|
|
8
9
|
# @param message [Object] status message
|
data/lib/sevgi/function.rb
CHANGED
|
@@ -13,13 +13,23 @@ require_relative "function/ui"
|
|
|
13
13
|
require_relative "function/version"
|
|
14
14
|
|
|
15
15
|
module Sevgi
|
|
16
|
-
#
|
|
16
|
+
# Support toolbox for Sevgi components and advanced extensions. The supported helper facade is {Sevgi::F}. It
|
|
17
|
+
# provides degree-based trigonometry and precision, file discovery and output, argv-safe commands, naming helpers,
|
|
18
|
+
# and small terminal status tools. It is not intended as a general-purpose utility library.
|
|
17
19
|
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
+
# {Function::Location}, {Function::Locate}, and {Function::Shell::Result} are public supporting values. The
|
|
21
|
+
# thread-local precision accessors remain on {Function::Math.precision}. Other nested helper modules organize the
|
|
22
|
+
# facade implementation and its method documentation. Consumers do not include or extend them.
|
|
23
|
+
#
|
|
24
|
+
# @example Use the supported facade in library code
|
|
25
|
+
# Sevgi::F.with_precision(3) do
|
|
26
|
+
# Sevgi::F.cos(60)
|
|
27
|
+
# Sevgi::F.approx(1.0 / 3)
|
|
28
|
+
# end
|
|
29
|
+
# @see https://sevgi.roktas.dev/functions/ Function toolbox guide
|
|
20
30
|
module Function
|
|
21
31
|
end
|
|
22
32
|
|
|
23
|
-
#
|
|
33
|
+
# Supported helper facade for Sevgi extensions and advanced consumers.
|
|
24
34
|
F = Function unless defined?(F)
|
|
25
35
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sevgi-function
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 1.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Recai Oktaş
|
|
@@ -9,7 +9,7 @@ bindir: bin
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies: []
|
|
12
|
-
description:
|
|
12
|
+
description: Collects numeric, string, file, shell, and terminal helpers.
|
|
13
13
|
email: roktas@gmail.com
|
|
14
14
|
executables: []
|
|
15
15
|
extensions: []
|
|
@@ -50,7 +50,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
50
50
|
- !ruby/object:Gem::Version
|
|
51
51
|
version: '0'
|
|
52
52
|
requirements: []
|
|
53
|
-
rubygems_version: 4.0.
|
|
53
|
+
rubygems_version: 4.0.20
|
|
54
54
|
specification_version: 4
|
|
55
|
-
summary:
|
|
55
|
+
summary: Shared utility functions for Sevgi components.
|
|
56
56
|
test_files: []
|