-
Notifications
You must be signed in to change notification settings - Fork 1
Add programmatic composite unit builder #72
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
suleman-uzair
wants to merge
18
commits into
main
Choose a base branch
from
feature/composite_units_builder
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 17 commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
550bd55
Add programmatic composite unit builder
a6a6c0c
feat(compose): add dimensions keyword and harden the builder
653339f
feat(compose): add fluent chaining builder (#unit/#dimension)
735cd5d
fix(compose): validate the fluent #dimension reference
8fbb0d4
fix(compose): validate fluent-chain ref/power/prefix/multiplier
574b8f3
fix(compose): validate the name metadata like multiplier
9358a51
docs: note fluent-chain + metadata validation in compose section
ae3d2f0
refactor(compose): resolve units by symbol id only (drop short names)
e680e72
docs(compose): document the default * separator and multiplier: control
bf10d02
fix(compose): address Copilot review findings
b6ec6d2
feat(compose): validate power_numerator type in Unit and Dimension
f2e0b9d
fix(compose): storage-specific message for unsupported power types
3d5f80b
feat(compose): division keeps "/", explicit extenders, input hardening
309352b
fix(compose): store power_numerator as a Number; address Copilot review
d1251b9
docs(compose): note Unit.new/Dimension.new validate on construction
ce2c108
fix(compose): harden power/prefix/quantity/extender edges (Codex + Co…
c6970de
fix(compose): harden Prefix-object name via safe_string (Codex re-rev…
cd038bc
docs(compose): clarify validation comments
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| module Unitsml | ||
| # Programmatic composite-unit construction: the operator DSL / fluent chain | ||
| # (Composable), the Unitsml.compose keyword form (Composite), and the shared | ||
| # assembly (Builder). The raw-input validators below are shared by BOTH the | ||
| # keyword form and the fluent chain so a bad reference/power/prefix fails fast | ||
| # with the same Errors::* class no matter which surface is used. | ||
| module Compose | ||
| autoload :Builder, "unitsml/compose/builder" | ||
| autoload :Composable, "unitsml/compose/composable" | ||
| autoload :Composite, "unitsml/compose/composite" | ||
| autoload :TermTree, "unitsml/compose/term_tree" | ||
|
|
||
| # The parser's grammar knows exactly these separators; anything else is a | ||
| # display concern served by the render-time multiplier: option. | ||
| EXTENDERS = ["*", "/", "//"].freeze | ||
|
|
||
| module_function | ||
|
|
||
| # A render name is embedded directly into <UnitName>, so it must be a plain | ||
| # String/Symbol (or nil); a Hash/other serializes as garbage. Shared by the | ||
| # compose build path and the render-option path (to_*(name:)). | ||
| def validate_name!(name) | ||
| return if type_any?([NilClass, String, Symbol], name) | ||
|
|
||
| raise Errors::InvalidUnitEntryError.new(value: name, field: :name) | ||
| end | ||
|
|
||
| # A multiplier is a render separator: nil, a String, or :space/:nospace. | ||
| # Reject anything else where it enters (compose metadata, or a | ||
| # to_*(multiplier:) option) so a bad value fails as Errors::*, not later. | ||
| def validate_multiplier!(multiplier) | ||
| return if type_any?([NilClass, String], multiplier) | ||
| return if %i[space nospace].include?(multiplier) | ||
|
|
||
| raise Errors::InvalidUnitEntryError.new(value: multiplier, | ||
| field: :multiplier) | ||
| end | ||
|
|
||
| # An explicit extender: an Extender object (reduced to its symbol), or a | ||
| # glyph the parser could have produced ("*", "/" or "//"). Custom separators | ||
| # belong to the multiplier: render option, not the Formula. | ||
| def extender_sym(value) | ||
| symbol = type?(Extender, value) ? value.symbol : value | ||
| string = safe_string(symbol) | ||
| return string if EXTENDERS.include?(string) | ||
|
|
||
| raise Errors::InvalidUnitEntryError.new(value: symbol, field: :extender) | ||
| end | ||
|
|
||
| # A unit reference just needs to be non-blank; Unit.new then fail-fasts on | ||
| # an unresolvable (or dim_*) name. Rejects nil/"" (which Unit.new would let | ||
| # through as its internal empty sentinel and crash at render). | ||
| def unit_ref(reference) | ||
| string = safe_string(reference) | ||
| return string if string && !string.strip.empty? | ||
|
|
||
| raise Errors::UnknownUnitError.new(value: reference) | ||
| end | ||
|
|
||
| # Validate a dimension reference (Dimension.new does not), so the keyword | ||
| # form and the fluent #dimension chain both fail fast with the same | ||
| # Errors::UnknownDimensionError instead of crashing at render. | ||
| def dimension_ref(reference) | ||
| string = safe_string(reference) | ||
| return string if string && Unitsdb.dimensions.parsables.key?(string) | ||
|
|
||
| raise Errors::UnknownDimensionError.new(value: reference) | ||
| end | ||
|
|
||
| # A prefix supplied as a Prefix object is reduced to its name so Unit.new | ||
| # validates it; other values (String/Symbol/nil) pass through. | ||
| def prefix_ref(prefix) | ||
| type?(Prefix, prefix) ? prefix.prefix_name : prefix | ||
| end | ||
|
|
||
| # A Number power must still be a parser-valid exponent (integer or n/m, | ||
| # optionally signed / double-slashed); a decimal ("0.5") or garbage ("abc") | ||
| # has no parsed equivalent and is rejected. Other types (Integer/Rational/ | ||
| # Float/nil) are validated by Builder during assembly. | ||
| def power(value) | ||
| return value unless type?(Number, value) | ||
| return value if value.raw_value.match?(%r{\A-?\d+(//?-?\d+)?\z}) | ||
|
|
||
| raise Errors::InvalidPowerError.new(value: value, | ||
| reason: :invalid_number) | ||
| end | ||
|
|
||
| # #to_s for a raw compose input without letting a pathological override (one | ||
| # that raises, has no #to_s, or returns a non-String) escape as a | ||
| # non-Errors::* exception; the caller treats nil as "not usable". | ||
| def safe_string(value) | ||
| string = value.to_s | ||
| string if string.is_a?(String) | ||
| rescue StandardError | ||
| nil | ||
| end | ||
|
|
||
| # A raw Numeric exponent, rendered in the parser's string form (a whole | ||
| # integer, or an n/m fraction from a Rational). A decimal (non-integer | ||
| # Float) has no parser representation and is rejected — nothing beyond what | ||
| # the parser accepts is introduced. | ||
| def numeric_exponent_string(numeric) | ||
| case numeric | ||
| when Rational then rational_exponent_string(numeric) | ||
| when Float then integer_float_string(numeric) | ||
| else numeric.to_s # Integer (and any other whole Numeric) | ||
| end | ||
| end | ||
|
|
||
| def rational_exponent_string(rational) | ||
| rational.denominator == 1 ? rational.numerator.to_s : rational.to_s | ||
| end | ||
|
|
||
| def integer_float_string(float) | ||
| unless float.finite? && float == float.to_i | ||
| raise Errors::InvalidPowerError.new(value: float, | ||
| reason: :non_integer_float) | ||
| end | ||
|
|
||
| float.to_i.to_s | ||
| end | ||
|
|
||
| # Class-membership test via Module#=== rather than #is_a?, so a compose | ||
| # input that is a BasicObject (which has no #is_a?/#nil?) is rejected as a | ||
| # typed Errors::* instead of the check itself raising a raw NoMethodError. | ||
| def type?(klass, value) | ||
| klass === value # rubocop:disable Style/CaseEquality | ||
| end | ||
|
|
||
| def type_any?(klasses, value) | ||
| klasses.any? { |klass| type?(klass, value) } | ||
| end | ||
|
suleman-uzair marked this conversation as resolved.
|
||
| end | ||
| end | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.