Skip to content

Commit

Permalink
Merge c6b2a7c into 97d1490
Browse files Browse the repository at this point in the history
  • Loading branch information
joshwlewis committed Apr 22, 2017
2 parents 97d1490 + c6b2a7c commit 7b76447
Show file tree
Hide file tree
Showing 9 changed files with 104 additions and 22 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Unitwise uses semantic versioning.

### Added

- `Unitwise.register` is a new method that allows user defined units
- Support for MRI 2.3 and 2.4

## 2.0.0 - 2015-09-13
Expand Down
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,37 @@ Unitwise(1, "meter/s") # Does not work, mixed designations (name and primar
Unitwise(1, "meter") / Unitwise(1, "s") # Also works
```


### Adding custom units

While UCUM's list of units is rather exhaustive, there may still be occasions
where you need custom or uncommon measurements. You can add them yourself
with `Unitwise.register`, which will allow you to convert to or from the new
unit.

For example, if your app needed to pour "3 fingers" of bourbon, you could
register an atom for that:

```ruby
Unitwise.register(
names: ["finger", "fingers"],
symbol: "🥃",
primary_code: "fng",
secondary_code: "fng",
scale: {
value: 1.0,
unit_code: '[foz_us]'
},
property: 'fluid volume'
)

Unitwise(1, "gallon").to_fingers
# => #<Unitwise::Measurement value=0.153721590464621430998E3 unit=fingers>

Unitwise(1, "🥃").to_cup
# => #<Unitwise::Measurement value=0.125 unit=cup>
```

## Supported Ruby Versions

This library aims to support and is tested against the following Ruby
Expand Down
9 changes: 8 additions & 1 deletion lib/unitwise.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,14 @@ def self.valid?(expression)
false
end
end


# Add additional atoms. Useful for registering uncommon or custom units.
# @param properties [Hash] Properties of the atom
def self.register(atom_hash)
Unitwise::Atom.register(atom_hash)
Unitwise::Expression::Decomposer.send(:reset)
end

# The system path for the installed gem
# @api private
def self.path
Expand Down
4 changes: 2 additions & 2 deletions lib/unitwise/atom.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ class Atom < Base
include Compatible

class << self
# Array of hashes representing atom properties.
# Array of hashes representing default atom properties.
# @api private
def data
@data ||= data_files.map { |file| YAML.load(File.open file) }.flatten
end

# Data files containing atom data
# Data files containing default atom data
# @api private
def data_files
%w(base_unit derived_unit).map { |type| Unitwise.data_file type }
Expand Down
7 changes: 7 additions & 0 deletions lib/unitwise/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ def self.all
@all ||= data.map { |d| new d }
end

# Add another instance to the collection.
# @param properties [Hash] properties for the new instance
# @return [Array] The updated collection
def self.register(properties)
all.push(new properties)
end

# Find a matching instance by a specified attribute.
# @param string [String] The search term
# @param method [Symbol] The attribute to search by
Expand Down
26 changes: 20 additions & 6 deletions lib/unitwise/expression/decomposer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,6 @@ class Decomposer

MODES = [:primary_code, :secondary_code, :names, :slugs, :symbol]

PARSERS = MODES.reduce({}) do |hash, mode|
hash[mode] = Parser.new(mode); hash
end

TRANSFORMER = Transformer.new

class << self
Expand All @@ -25,13 +21,31 @@ def parse(expression)
end
end

def parsers
@parsers ||= MODES.reduce({}) do |hash, mode|
hash[mode] = Parser.new(mode); hash
end
end

def transformer
@transformer = Transformer.new
end

private

# A simple cache to prevent re-decomposing the same units
# api private
def cache
@cache ||= {}
end

# Reset memoized data. Allows rebuilding of parsers, transformers, and
# the cache after list of atoms has been modified.
def reset
@parsers = nil
@transformer = nil
@cache = nil
end
end

attr_reader :expression, :mode
Expand All @@ -44,15 +58,15 @@ def initialize(expression)
end

def parse
PARSERS.reduce(nil) do |_, (mode, parser)|
self.class.parsers.reduce(nil) do |_, (mode, parser)|
parsed = parser.parse(expression) rescue next
@mode = mode
break parsed
end
end

def transform
@transform ||= TRANSFORMER.apply(parse, :mode => mode)
@transform ||= self.class.transformer.apply(parse, :mode => mode)
end

def terms
Expand Down
11 changes: 3 additions & 8 deletions lib/unitwise/expression/matcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,15 @@ module Expression
class Matcher
class << self
def atom(mode)
@atom ||= {}
@atom[mode] ||= new(Atom.all, mode).alternative
new(Atom.all, mode).alternative
end

def metric_atom(mode)
@metric_atom ||= {}
@metric_atom[mode] ||=
new(Atom.all.select(&:metric?), mode).alternative
new(Atom.all.select(&:metric?), mode).alternative
end

def prefix(mode)
@prefix ||= {}
@prefix[mode] ||= new(Prefix.all, mode).alternative
new(Prefix.all, mode).alternative
end
end

Expand All @@ -41,7 +37,6 @@ def matchers
def alternative
Parslet::Atoms::Alternative.new(*matchers)
end

end
end
end
15 changes: 11 additions & 4 deletions lib/unitwise/expression/parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,21 @@ module Expression
class Parser < Parslet::Parser
attr_reader :key
def initialize(key = :primary_code)
@key = key
@key = key
@atom_matcher = Matcher.atom(key)
@metric_atom_matcher = Matcher.metric_atom(key)
@prefix_matcher = Matcher.prefix(key)
end

private

attr_reader :atom_matcher, :metric_atom_matcher, :prefix_matcher

root :expression

rule (:atom) { Matcher.atom(key).as(:atom_code) }
rule (:metric_atom) { Matcher.metric_atom(key).as(:atom_code) }
rule (:prefix) { Matcher.prefix(key).as(:prefix_code) }
rule (:atom) { atom_matcher.as(:atom_code) }
rule (:metric_atom) { metric_atom_matcher.as(:atom_code) }
rule (:prefix) { prefix_matcher.as(:prefix_code) }

rule (:simpleton) do
(prefix.as(:prefix) >> metric_atom.as(:atom) | atom.as(:atom))
Expand Down
22 changes: 21 additions & 1 deletion test/unitwise_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,28 @@
end
end

describe 'register' do
it 'should allow custom units to be registered' do
josh = {
names: ["Josh W Lewis", "joshwlewis"],
symbol: "JWL",
primary_code: "jwl",
secondary_code: "jwl",
scale: {
value: 71.875,
unit_code: "[in_i]"
}
}

Unitwise.register(josh)

joshes = Unitwise(1, 'mile').to_jwl

joshes.to_i.must_equal(881)
end
end

it "should have a path" do
Unitwise.path.must_match(/unitwise$/)
end

end

0 comments on commit 7b76447

Please sign in to comment.