sevgi-function 0.95.0 → 0.98.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5d64555b43d632235e9d1f1419294447fc5e63c0c2af0ba3b8c89fc1f82e7474
4
- data.tar.gz: 6a15d7e54278e3d0cee74369616f60b6e43d70f8e2a7784c33a6737099fd9e4b
3
+ metadata.gz: 4f11073d7043461caa01968570217c218040207ff9a6abdcd5e56501946e35e1
4
+ data.tar.gz: 9992d6ae73303b55ead462ba6b599e965446dd7b0455621e4ad6aa029aa65376
5
5
  SHA512:
6
- metadata.gz: 60083cb4298cad1644ec70bd490652b07bec34fe9498d8359653a82800fae41ac2d6128c527095af04f992a351bd260fb870761289537c3a5778dc71c214506a
7
- data.tar.gz: bfc1aeaa49fb6a08cb7b78f14dc5e8b077e3d09de7bc52f92bc460692f5ec5831c2a30cae5ddde96b49b9ed0c494546f884b12af026557c0d8e676171d272130
6
+ metadata.gz: 3cfb465db527caf1f1ba8e0305aebf3c3ba9164b6dc721122e4c5bb776f7843a05baf991fee72bff25a6c2d5be58c4625dfaac91c15d12e192fa1d12fc5ff2a3
7
+ data.tar.gz: cb6e493fa0ad5c0332b6424bf8e53d9d72a49fb0729f5f4b9dcd481e94fc492fd07e458d6405aeb44b676ec3927301dd72e33ca0da2bad256d3931955d5183dc
data/CHANGELOG.md CHANGED
@@ -5,6 +5,131 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
5
5
 
6
6
  ## Unreleased
7
7
 
8
+ ## 0.98.2 - 2026-07-25
9
+
10
+ ### Fixed
11
+
12
+ - Restored the `sevgi` CLI's main-object scope so bare toolkit calls remain available inside script helper classes.
13
+
14
+ ## 0.98.1 - 2026-07-20
15
+
16
+ ### Changed
17
+
18
+ - Changed the `sevgi` CLI to use the executor's isolated scope by default.
19
+
20
+ ### Removed
21
+
22
+ - Removed the redundant `-n` and `--nomain` CLI options; library consumers can still select `main: true` through
23
+ `Sevgi.execute` and `Sevgi.execute_file` when needed.
24
+
25
+ ## 0.98.0 - 2026-07-20
26
+
27
+ ### Added
28
+
29
+ - Added standard-input support to `sevgi`, `igves`, and `igsev`; `sevgi --as NAME` and
30
+ `Sevgi.execute_file(..., as: NAME)` supply the logical source basename used by implicit SVG, PDF, and PNG
31
+ destinations without changing physical load identity.
32
+
33
+ ## 0.97.0 - 2026-07-19
34
+
35
+ ### Added
36
+
37
+ - Added `sevgi-appendix`, containing the Sevgi agent skill and a RuboCop plugin for preserving readable `.sevgi` DSL
38
+ source alongside rubyfmt-formatted Ruby; the umbrella `sevgi` gem installs the matching Appendix version.
39
+ - Added `sevgi --skill` to report the validated path of the matching packaged agent skill for product-neutral setup.
40
+ - Added `igsev` to the umbrella gem for normalizing an SVG file through a complete SVG-to-Sevgi-to-SVG round trip.
41
+
42
+ ### Changed
43
+
44
+ - Made Homebrew the recommended complete CLI installation while retaining Bundler and focused component gems for Ruby
45
+ application dependencies.
46
+
47
+ ## 0.96.0 - 2026-07-18
48
+
49
+ ### Added
50
+
51
+ - Added exact, subtree-wide attribute omission to Derender content/file conversion, evaluation, and inclusion APIs;
52
+ `igves --omit` exposes the same behavior from the command line.
53
+ - Added `Sevgi.SVG` as the explicit namespaced form of the top-level SVG document entrypoint.
54
+ - Added the opt-in recursive `SVG::Modules` contract for callable drawing namespaces.
55
+ - Added callable drawing-module `base` blocks, document and paper registry introspection, Canvas structural equality,
56
+ renderer options on `RenderChildren`, axis translation helpers, and a CC BY RDF helper.
57
+ - Formalized non-rendering `-` metadata and `+` attribute updates as `Attributes::META_PREFIX` and
58
+ `Attributes::UPDATE_SUFFIX`; repeated Array updates now concatenate into a stable flat value.
59
+
60
+ ### Changed
61
+
62
+ - Breaking: replaced the `SVG = Sevgi::Graphics` alias with an independent SVG facade. Facade operations now use
63
+ capitalized names such as `SVG.Canvas` and `SVG.Document`; lowercase component helpers remain on
64
+ `Sevgi::Graphics`, and the stuttering `SVG.SVG` form was removed.
65
+ - Breaking: made low-level `Executor.execute` and `execute_file` runners private; consumers use
66
+ `Sevgi.execute` and `Sevgi.execute_file` while retaining the public result and error types.
67
+ - Breaking: removed the abstract `Document::Base` layer from the selectable document-profile registry; `Minimal` and
68
+ `Default` are now sibling concrete profiles, and custom profile hierarchies should derive from `Base`.
69
+ - Breaking: renamed Parallelogram segment and constraint arguments by geometric role: `base`, `side`, and
70
+ `constraint` replace axis-implying constructor names.
71
+ - Made degree-based sine and cosine exact at integer quarter turns, eliminating cardinal Geometry residue.
72
+ - Documented complete file-system failure families for file comparison, output, touch, and upward location helpers.
73
+ - Formalized generated Tile ids, positional CSS classes, template placement, and per-use callback signatures.
74
+ - Formalized String and Symbol ids across Derender selection APIs and documented file-read failures consistently.
75
+ - Breaking: made top-level `Decompile`, `Derender`, `Evaluate`, and `EvaluateChildren` consume inline SVG/XML;
76
+ file inputs now use the corresponding `File`-suffixed entrypoints.
77
+ - Breaking: replaced callable drawing module `call` block registration with argument-independent `base` blocks;
78
+ inherited bases run parent-first, and modules with one drawing method conventionally name it `call`.
79
+ - Breaking: made bracket and call notation the canonical Geometry constructors. Among Data value types, bracket notation
80
+ is public only for Point, Segment, LengthAngle, Margin, and Paper; use `.new` for Location and result carriers, and
81
+ `Stay` for traversal stop tokens.
82
+ - Breaking: aligned Point and Segment comparison with Ruby `Comparable`; malformed or unrelated comparison operands now
83
+ return nil from `<=>`.
84
+ - Breaking: executor entrypoints now return immutable `Executor::Result` values and expose only `execute` and
85
+ `execute_file`; inspect `result.error`, `result.stack`, and `result.value` instead of executor scopes or lifecycle state.
86
+ - Breaking: wrapper attributes and callable arguments use distinct channels, `With` rejects parentless receivers, and
87
+ unsupported direct constructors are private; use the documented factories for Content, concrete Geometry lined
88
+ elements, and Grid query values.
89
+ - Breaking: Canvas uses `.new` for explicit fields, `.from_paper(paper, **overrides)` for paper conversion, and
90
+ `.call`/`Graphics.canvas` for dispatch; the misleading keyword-only `.from_paper(width:, height:)` form was removed.
91
+ - Breaking: `Save`, `Write`, `PNG`, `PDF`, and Sundries native export normalize successful destinations to expanded
92
+ String paths and create missing parent directories; change-aware SVG writes still return nil when unchanged.
93
+ - Breaking: renamed `Derender.evaluate_file_children` to `evaluate_children_file`; decompiled nodes now own immutable
94
+ public state while parser, strategy, and construction plumbing remain private.
95
+ - Expanded runtime/YARD parity checks and exact contracts for inherited, extended, generated, and dynamic API surfaces,
96
+ forwarded options, error channels, path ownership, whitespace, namespaces, nil behavior, and return values.
97
+
98
+ ### Fixed
99
+
100
+ - Rejected directories from default file discovery while preserving custom locator matchers.
101
+ - Kept `Ancestral` context in non-rendering `-context` metadata instead of leaking it into SVG attributes.
102
+ - Preserved source Canvas units and names when deriving fitted Grid canvases.
103
+ - Prevented stale documentation assets from mixing old layouts with new HTML, and moved sidebar navigation into the
104
+ tablet menu at 1024px and below.
105
+ - Normalized numeric slots owned by path, shape, transform, tile, and Inkscape page helpers to finite SVG number spelling;
106
+ arbitrary user-supplied attributes remain untouched.
107
+ - Made named callable wrapper and symbol ids stable while omitting defaults for anonymous modules, and indexed every
108
+ rendered id value, including false and numeric values, through its serialized string.
109
+ - Preserved signed Geometry constraint directions, rejected invalid sweep/export channels through Sevgi error families,
110
+ and validated raw output paths before expansion or rendering.
111
+ - Made callable module configuration copy-owned and freeze-aware, document subclasses inherit their nearest profile, and
112
+ false executor boot receivers remain explicit rather than defaulting to an internal scope.
113
+ - Made element trees, attributes, identifiers, locator results, document profiles, Derender nodes, shell results, and
114
+ executor results retain owned immutable snapshots where their public contracts promise value semantics.
115
+ - Corrected Shell combined-output separators, nil export density errors, document render-option routing, executor source
116
+ snapshots, selected-node namespace/whitespace documentation, and Standard character-data validation.
117
+
118
+ ### Removed
119
+
120
+ - Removed public access to pluralization tables and the internal SVG save extension; pluralization rules are now deeply
121
+ immutable.
122
+ - Removed public documentation and constant access for command-line implementation modules; the `sevgi` and `igves`
123
+ executables remain unchanged.
124
+ - Removed eager loading and public documentation of private Showcase build/test support; explicit support entrypoints
125
+ now keep the harness under the Showcase namespace.
126
+ - Removed public access to document profile name normalizers; registry operations retain them as private plumbing.
127
+ - Removed the redundant `Margin.margin` constructor; use canonical bracket notation.
128
+ - Removed public executor orchestration, obsolete callable-module hooks, the old public attribute syntax constants, and
129
+ direct construction of abstract or internally wired Content, Element, and Grid query types.
130
+ - Removed accidental public access to abstract Lined factories, internal element-name/export maps, result/location
131
+ bracket constructors, and direct Stop construction.
132
+
8
133
  ## 0.95.0 - 2026-07-11
9
134
 
10
135
  ### Security
data/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # Sevgi Function
2
2
 
3
- Shared helper functions used by Sevgi components.
3
+ Sevgi Function contains the supported `Sevgi::F` toolbox shared by Sevgi components and advanced extensions. It is not
4
+ intended as a general-purpose utility library; nested helper modules organize the facade implementation and are not
5
+ consumer mixins.
4
6
 
5
7
  ## Install
6
8
 
@@ -22,15 +24,15 @@ Sevgi::F.eq?(0.1 + 0.2, 0.3, precision: 12)
22
24
 
23
25
  ## Ruby compatibility
24
26
 
25
- Requires Ruby 3.4.0 or newer. CI verifies Ruby 3.4.0 and the current development Ruby from `.ruby-version`.
27
+ Requires Ruby 3.4.0 or newer. CI verifies the current Ruby 3.4 release and the development Ruby from `.ruby-version`.
26
28
 
27
29
  ## Native prerequisites
28
30
 
29
- None beyond Ruby and this gem's Ruby dependencies.
31
+ This gem needs only Ruby and its Ruby dependencies.
30
32
 
31
33
  ## Links
32
34
 
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
35
+ - Documentation: <https://sevgi.roktas.dev>
36
+ - API documentation: <https://www.rubydoc.info/gems/sevgi-function>
37
+ - Source: <https://github.com/roktas/sevgi/tree/main/function>
38
+ - Changelog: <https://github.com/roktas/sevgi/blob/main/CHANGELOG.md>
@@ -2,7 +2,8 @@
2
2
 
3
3
  module Sevgi
4
4
  module Function
5
- # ANSI color and style helpers for terminal output.
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
@@ -1,11 +1,11 @@
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 helpers used by build scripts and DSL support code.
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
10
  # Checks whether a file would change if written with content.
11
11
  # @param file [String] file path to compare
@@ -14,14 +14,14 @@ module Sevgi
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 [Errno::EACCES] when the file cannot be read
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
- Digest::SHA1.digest(old_content) != Digest::SHA1.digest(content)
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.exist?(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.exist?(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 [Errno::EACCES] when the file cannot be read or written
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 [Errno::EACCES] when the file or parent directory cannot be created
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 a start directory.
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 existence checks
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 expanded paths ignored during lookup.
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 existence checks
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[file, slug, here]
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
- return [path, candidate] if (block || proc { ::File.exist?(it) }).call(candidate)
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
@@ -2,14 +2,17 @@
2
2
 
3
3
  module Sevgi
4
4
  module Function
5
- # Numeric and trigonometric helpers used by geometry and DSL code.
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,77 @@ 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 [Math::DomainError] when value is outside -1..1
33
- def acos(value) = to_degrees(::Math.acos(value))
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
- def acot(value) = 90.0 - to_degrees(::Math.atan(value))
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 [TypeError] when float cannot be rounded
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
- float.round(precision.nil? ? Function::Math.precision : precision)
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 [Math::DomainError] when value is outside -1..1
53
- def asin(value) = to_degrees(::Math.asin(value))
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
- def atan(value) = to_degrees(::Math.atan(value))
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
- def atan2(y, x) = to_degrees(::Math.atan2(y, x))
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
- def cos(degrees) = ::Math.cos(to_radians(degrees))
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
- # @param length [Numeric] finite total length
78
- # @param division [Numeric] finite, non-zero division size
97
+ # @param length [Numeric] finite real total length
98
+ # @param division [Numeric] finite real, non-zero division size
79
99
  # @return [Integer]
80
100
  # @raise [Sevgi::ArgumentError] when an operand is not a finite Numeric or division is zero
81
101
  def count(length, division)
@@ -91,6 +111,7 @@ module Sevgi
91
111
  # @param right [Numeric] right operand
92
112
  # @param precision [Integer, nil] explicit precision, or nil to use {Math.precision}
93
113
  # @return [Boolean]
114
+ # @raise [Sevgi::ArgumentError] when an operand is not finite real or precision is invalid
94
115
  def eq?(left, right, precision: nil) = approx(left, precision) == approx(right, precision)
95
116
 
96
117
  # Checks whether the rounded left operand is greater than or equal to the rounded right operand.
@@ -98,6 +119,7 @@ module Sevgi
98
119
  # @param right [Numeric] right operand
99
120
  # @param precision [Integer, nil] explicit precision, or nil to use {Math.precision}
100
121
  # @return [Boolean]
122
+ # @raise [Sevgi::ArgumentError] when an operand is not finite real or precision is invalid
101
123
  def ge?(left, right, precision: nil) = approx(left, precision) >= approx(right, precision)
102
124
 
103
125
  # Checks whether the rounded left operand is greater than the rounded right operand.
@@ -105,6 +127,7 @@ module Sevgi
105
127
  # @param right [Numeric] right operand
106
128
  # @param precision [Integer, nil] explicit precision, or nil to use {Math.precision}
107
129
  # @return [Boolean]
130
+ # @raise [Sevgi::ArgumentError] when an operand is not finite real or precision is invalid
108
131
  def gt?(left, right, precision: nil) = approx(left, precision) > approx(right, precision)
109
132
 
110
133
  # Checks whether the rounded left operand is less than or equal to the rounded right operand.
@@ -112,6 +135,7 @@ module Sevgi
112
135
  # @param right [Numeric] right operand
113
136
  # @param precision [Integer, nil] explicit precision, or nil to use {Math.precision}
114
137
  # @return [Boolean]
138
+ # @raise [Sevgi::ArgumentError] when an operand is not finite real or precision is invalid
115
139
  def le?(left, right, precision: nil) = approx(left, precision) <= approx(right, precision)
116
140
 
117
141
  # Checks whether the rounded left operand is less than the rounded right operand.
@@ -119,33 +143,46 @@ module Sevgi
119
143
  # @param right [Numeric] right operand
120
144
  # @param precision [Integer, nil] explicit precision, or nil to use {Math.precision}
121
145
  # @return [Boolean]
146
+ # @raise [Sevgi::ArgumentError] when an operand is not finite real or precision is invalid
122
147
  def lt?(left, right, precision: nil) = approx(left, precision) < approx(right, precision)
123
148
 
124
149
  # Rounds a value only when precision is present.
125
150
  # @param float [Numeric] value to round
126
151
  # @param precision [Integer, nil] explicit precision, or nil to return float unchanged
127
152
  # @return [Numeric]
128
- def round(float, precision) = precision ? float.round(precision) : float
153
+ # @raise [Sevgi::ArgumentError] when value is not finite real or precision is not an Integer or nil
154
+ def round(float, precision)
155
+ number = valid_real(:value, float)
156
+ precision.nil? ? number : number.round(valid_precision(precision))
157
+ end
129
158
 
130
- # Returns the sine of an angle expressed in degrees.
159
+ # Returns the sine of an angle expressed in degrees. Integer quarter turns return exact `-1.0`, `0.0`, or `1.0`;
160
+ # other angles use Ruby's floating-point Math implementation.
131
161
  # @param degrees [Numeric] angle in degrees
132
162
  # @return [Float]
133
- def sin(degrees) = ::Math.sin(to_radians(degrees))
163
+ # @raise [Sevgi::ArgumentError] when degrees is not a finite real number
164
+ def sin(degrees)
165
+ degrees = finite_real(:degrees, degrees)
166
+ quadrant_value(degrees, QUADRANT_SINES) { ::Math.sin(radians(degrees)) }
167
+ end
134
168
 
135
169
  # Returns the tangent of an angle expressed in degrees.
136
170
  # @param degrees [Numeric] angle in degrees
137
171
  # @return [Float]
172
+ # @raise [Sevgi::ArgumentError] when degrees is not a finite real number
138
173
  def tan(degrees) = ::Math.tan(to_radians(degrees))
139
174
 
140
175
  # Converts radians to degrees.
141
176
  # @param radians [Numeric] angle in radians
142
177
  # @return [Float]
143
- def to_degrees(radians) = radians.to_f * 180 / ::Math::PI
178
+ # @raise [Sevgi::ArgumentError] when radians is not a finite real number
179
+ def to_degrees(radians) = finite_real(:radians, radians) * 180 / ::Math::PI
144
180
 
145
181
  # Converts degrees to radians.
146
182
  # @param degrees [Numeric] angle in degrees
147
183
  # @return [Float]
148
- def to_radians(degrees) = degrees.to_f / 180 * ::Math::PI
184
+ # @raise [Sevgi::ArgumentError] when degrees is not a finite real number
185
+ def to_radians(degrees) = radians(finite_real(:degrees, degrees))
149
186
 
150
187
  # Runs a block with a current-thread precision override.
151
188
  # @param precision [Integer, nil] scoped precision, or nil to use {PRECISION}
@@ -153,6 +190,10 @@ module Sevgi
153
190
  # @yieldreturn [Object]
154
191
  # @return [Object] block return value
155
192
  # @raise [Sevgi::ArgumentError] when no block is given
193
+ # @raise [Sevgi::ArgumentError] when precision is not an Integer or nil
194
+ # @example Compare values under a temporary precision
195
+ # Sevgi::F.with_precision(2) { Sevgi::F.eq?(1.001, 1.0) } # => true
196
+ # Sevgi::Function::Math.precision # => 6
156
197
  def with_precision(precision, &block)
157
198
  ArgumentError.("Block required") unless block
158
199
 
@@ -167,19 +208,20 @@ module Sevgi
167
208
  # @param value [Numeric] value to check
168
209
  # @param precision [Integer, nil] explicit precision, or nil to use {Math.precision}
169
210
  # @return [Boolean]
211
+ # @raise [Sevgi::ArgumentError] when value is not finite real or precision is invalid
170
212
  def zero?(value, precision: nil) = eq?(value, 0.0, precision:)
171
213
 
172
214
  private
173
215
 
174
216
  def finite_real(field, value)
175
- unless value.is_a?(::Numeric)
176
- ArgumentError.("#{field} must be a finite Numeric: #{value.inspect}")
217
+ unless value.is_a?(::Numeric) && !value.is_a?(::Complex)
218
+ ArgumentError.("#{field} must be a finite real Numeric: #{value.inspect}")
177
219
  end
178
220
 
179
221
  number = begin
180
222
  value.to_f
181
223
  rescue ::StandardError => e
182
- ArgumentError.("#{field} must be a finite Numeric: #{value.inspect} (#{e.message})")
224
+ ArgumentError.("#{field} must be a finite real Numeric: #{value.inspect} (#{e.message})")
183
225
  end
184
226
 
185
227
  ArgumentError.("#{field} must be finite: #{value.inspect}") unless number.is_a?(::Float) && number.finite?
@@ -187,6 +229,26 @@ module Sevgi
187
229
  number
188
230
  end
189
231
 
232
+ def quadrant_value(degrees, values)
233
+ turn = degrees % 360
234
+ return yield unless (turn % 90).zero?
235
+
236
+ values[(turn / 90).to_i]
237
+ end
238
+
239
+ def radians(degrees) = degrees / 180 * ::Math::PI
240
+
241
+ def valid_precision(precision)
242
+ return precision if precision.is_a?(::Integer)
243
+
244
+ ArgumentError.("Precision must be an Integer or nil: #{precision.inspect}")
245
+ end
246
+
247
+ def valid_real(field, value)
248
+ finite_real(field, value)
249
+ value
250
+ end
251
+
190
252
  end
191
253
 
192
254
  extend Math
@@ -1,10 +1,12 @@
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
- # Shell execution helpers and executable lookup utilities.
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
@@ -30,24 +32,62 @@ module Sevgi
30
32
  Error.("Missing executable: #{program}") unless executable?(program)
31
33
  end
32
34
 
33
- # Result object returned by shell commands.
34
- Result = Struct.new(:args, :outs, :errs, :exit_code) do
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
- # Returns stdout, a separator, and stderr as one string.
45
- # @return [String]
46
- def all = [*outs, "\n\n", *errs].join("\n").strip
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 = args.join(" ")
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&.zero?
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
- # Builds a successful empty result.
73
- # @return [Sevgi::Function::Shell::Result]
74
- def self.dummy = new([], [], [], 0)
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,16 +226,17 @@ 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
- return Result.dummy if args.empty?
232
+ ArgumentError.("Shell command required") if args.empty?
189
233
 
190
234
  @coathooks = 0
191
235
  outs, errs, status = Open3.popen3(*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, outs, errs, status.exitstatus)
239
+ Result.new(args:, outs:, errs:, exit_code: status.exitstatus, signal: status.termsig)
196
240
  end
197
241
 
198
242
  private
@@ -286,12 +330,15 @@ module Sevgi
286
330
  end
287
331
  end
288
332
 
333
+ private_constant :Runner
334
+
289
335
  # @overload sh(*args, &block)
290
336
  # Runs a command and captures stdout, stderr, and exit status.
291
337
  # @param args [Array<String>] command and arguments
292
338
  # @yield optional stdin producer, evaluated once after output readers start
293
339
  # @yieldreturn [String, nil] content to write to stdin; nil writes nothing
294
340
  # @return [Sevgi::Function::Shell::Result]
341
+ # @raise [Sevgi::ArgumentError] when no command is given
295
342
  # @raise [SystemCallError] when the executable cannot be spawned or process pipes cannot be opened
296
343
  # @raise [StandardError] when the input block raises; the child is terminated and reaped before propagation
297
344
  # @note The child's stdin is closed after the input block. During execution, the first SIGINT sends TERM to the
@@ -304,6 +351,7 @@ module Sevgi
304
351
  # @yield optional stdin producer, evaluated once after output readers start
305
352
  # @yieldreturn [String, nil] content to write to stdin; nil writes nothing
306
353
  # @return [Sevgi::Function::Shell::Result]
354
+ # @raise [Sevgi::ArgumentError] when no command is given
307
355
  # @raise [Sevgi::Error] when the executable is missing or the command fails
308
356
  # @raise [SystemCallError] when the executable cannot be spawned or process pipes cannot be opened
309
357
  # @raise [StandardError] when the input block raises; the child is terminated and reaped before propagation
@@ -2,7 +2,8 @@
2
2
 
3
3
  module Sevgi
4
4
  module Function
5
- # String helpers used by generated names and user-facing text.
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
- # Lightweight English pluralization helper.
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
26
  # Words that should not be pluralized.
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
- ].freeze
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
 
@@ -2,7 +2,8 @@
2
2
 
3
3
  module Sevgi
4
4
  module Function
5
- # Small terminal status helpers used by build and release tasks.
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
@@ -3,6 +3,6 @@
3
3
  module Sevgi
4
4
  module Function
5
5
  # Current version of the Sevgi function gem.
6
- VERSION = "0.95.0"
6
+ VERSION = "0.98.2"
7
7
  end
8
8
  end
@@ -13,13 +13,23 @@ require_relative "function/ui"
13
13
  require_relative "function/version"
14
14
 
15
15
  module Sevgi
16
- # Shared helper namespace used directly as `Sevgi::Function` and through {Sevgi::F}.
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
- # @example Use helper methods through the public alias
19
- # F.pluralize("axis")
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 should 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
- # Public alias for the shared function helper namespace.
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.95.0
4
+ version: 0.98.2
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: Enhances the Sevgi toolkit with utility methods.
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.11
53
+ rubygems_version: 4.0.16
54
54
  specification_version: 4
55
- summary: Various utility functions for the Sevgi toolkit.
55
+ summary: Shared utility functions for Sevgi components.
56
56
  test_files: []