percent 0.0.1
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 +7 -0
- data/lib/percent.rb +9 -0
- data/lib/percent/active_model/percentage_validator.rb +11 -0
- data/lib/percent/active_record/migration_extensions/options.rb +21 -0
- data/lib/percent/active_record/migration_extensions/schema_statements.rb +18 -0
- data/lib/percent/active_record/migration_extensions/table.rb +18 -0
- data/lib/percent/active_record/percentizable.rb +71 -0
- data/lib/percent/hooks.rb +18 -0
- data/lib/percent/railtie.rb +7 -0
- data/lib/percentage.rb +127 -0
- data/percent.gemspec +25 -0
- data/spec/lib/percent/active_record/migration_extensions/schema_statements_spec.rb +73 -0
- data/spec/lib/percent/active_record/migration_extensions/table_spec.rb +74 -0
- data/spec/lib/percent/active_record/percentizable_spec.rb +350 -0
- data/spec/lib/percentage_spec.rb +719 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/support/models.rb +18 -0
- data/spec/support/schema.rb +17 -0
- metadata +152 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: 6ba54ea6f9cd4071338360bee6e073c87710f6f9
|
|
4
|
+
data.tar.gz: 14d795b2df55dfa1ed3eb4629061b68b533878cd
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 4924d7c67df23882936c2a1a5a2c3687128bf6b71c138d9ce6c6021a201916f000b5e0e930acf7b78f674e6ea914ce1e2822fdf79796699eaeff369e6c3f1b9d
|
|
7
|
+
data.tar.gz: 17bc724e07383b35fb7c6c8f8372ddfbad765a850762cf855b31b860cd27897f55428b5d77fd2f5425c6c7bf225065626ec686e7e784f4d8a925514dc5611c89
|
data/lib/percent.rb
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module Percent
|
|
2
|
+
module ActiveModel
|
|
3
|
+
class PercentageValidator < ::ActiveModel::Validations::NumericalityValidator
|
|
4
|
+
def validate_each(record, attr, value)
|
|
5
|
+
super record, attr, (value.nil? ? value : value.to_decimal)
|
|
6
|
+
end
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
ActiveModel::Validations::PercentageValidator = Percent::ActiveModel::PercentageValidator
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Percent
|
|
2
|
+
module ActiveRecord
|
|
3
|
+
module MigrationExtensions
|
|
4
|
+
module Options
|
|
5
|
+
def self.without_table(accessor, options = {})
|
|
6
|
+
column_name = accessor.to_s + '_fraction'
|
|
7
|
+
options[:null] ||= false
|
|
8
|
+
options[:default] ||= 0
|
|
9
|
+
type = :decimal
|
|
10
|
+
|
|
11
|
+
[column_name, type, options]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.with_table(table_name, accessor, options = {})
|
|
15
|
+
options = self.without_table accessor, options
|
|
16
|
+
options.unshift table_name
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module Percent
|
|
2
|
+
module ActiveRecord
|
|
3
|
+
module MigrationExtensions
|
|
4
|
+
module SchemaStatements
|
|
5
|
+
def add_percentage(table_name, accessor, options={})
|
|
6
|
+
*opts = Options.with_table table_name, accessor, options
|
|
7
|
+
add_column *opts
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def remove_percentage(table_name, accessor, options={})
|
|
11
|
+
*opts = Options.with_table table_name, accessor, options
|
|
12
|
+
remove_column *opts
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module Percent
|
|
2
|
+
module ActiveRecord
|
|
3
|
+
module MigrationExtensions
|
|
4
|
+
module Table
|
|
5
|
+
def percentage(accessor, options={})
|
|
6
|
+
*opts = Options.without_table accessor, options
|
|
7
|
+
column *opts
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def remove_percentage(accessor, options={})
|
|
11
|
+
*opts = Options.without_table accessor, options
|
|
12
|
+
remove *opts
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
module Percent
|
|
2
|
+
module ActiveRecord
|
|
3
|
+
module Percentizable
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
module ClassMethods
|
|
7
|
+
def percentize(*columns)
|
|
8
|
+
# initialize options
|
|
9
|
+
options = columns.extract_options!
|
|
10
|
+
|
|
11
|
+
columns.each do |column|
|
|
12
|
+
column_name = column.to_s
|
|
13
|
+
|
|
14
|
+
# determine attribute name
|
|
15
|
+
if options[:as]
|
|
16
|
+
attribute_name = options[:as]
|
|
17
|
+
elsif column_name =~ /_fraction$/
|
|
18
|
+
attribute_name = column_name.sub /_fraction$/, ""
|
|
19
|
+
else
|
|
20
|
+
attribute_name = column_name + '_percent'
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# validation
|
|
24
|
+
unless options[:disable_validation]
|
|
25
|
+
allow_nil = options.fetch :allow_nil, false
|
|
26
|
+
numericality = options[:numericality]
|
|
27
|
+
fraction = options[:fraction_numericality]
|
|
28
|
+
|
|
29
|
+
if numericality || !options.key?(:numericality)
|
|
30
|
+
numericality = numericality_range 0, 100 unless fraction.is_a? Hash
|
|
31
|
+
|
|
32
|
+
validates attribute_name, {
|
|
33
|
+
allow_nil: allow_nil,
|
|
34
|
+
'percent/active_model/percentage': numericality
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
if fraction || !options.key?(:fraction_numericality)
|
|
39
|
+
unless numericality.is_a?(Hash) && options.key?(:numericality)
|
|
40
|
+
fraction = numericality_range 0, 1
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
validates column_name, allow_nil: allow_nil, numericality: fraction
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# percent attribute getter
|
|
48
|
+
define_method attribute_name do
|
|
49
|
+
value = public_send column_name
|
|
50
|
+
value.nil? ? value : Percentage.from_fraction(value, options)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# percent attribute setter
|
|
54
|
+
define_method "#{attribute_name}=" do |value|
|
|
55
|
+
unless value.nil? || value.is_a?(Percentage)
|
|
56
|
+
value = Percentage.from_amount value, options
|
|
57
|
+
end
|
|
58
|
+
public_send "#{column_name}=", (value.nil? ? value : value.to_d)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def numericality_range(min, max)
|
|
66
|
+
{ greater_than_or_equal_to: min, less_than_or_equal_to: max }
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module Percent
|
|
2
|
+
class Hooks
|
|
3
|
+
def self.init
|
|
4
|
+
ActiveSupport.on_load(:active_record) do
|
|
5
|
+
require 'percent/active_model/percentage_validator'
|
|
6
|
+
require 'percent/active_record/percentizable'
|
|
7
|
+
::ActiveRecord::Base.send :include, Percent::ActiveRecord::Percentizable
|
|
8
|
+
|
|
9
|
+
require 'percent/active_record/migration_extensions/options'
|
|
10
|
+
require 'percent/active_record/migration_extensions/schema_statements'
|
|
11
|
+
require 'percent/active_record/migration_extensions/table'
|
|
12
|
+
::ActiveRecord::Migration.send :include, Percent::ActiveRecord::MigrationExtensions::SchemaStatements
|
|
13
|
+
::ActiveRecord::ConnectionAdapters::TableDefinition.send :include, Percent::ActiveRecord::MigrationExtensions::Table
|
|
14
|
+
::ActiveRecord::ConnectionAdapters::Table.send :include, Percent::ActiveRecord::MigrationExtensions::Table
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
data/lib/percentage.rb
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
require 'bigdecimal'
|
|
2
|
+
require 'bigdecimal/util'
|
|
3
|
+
|
|
4
|
+
class Percentage < Numeric
|
|
5
|
+
def initialize(val = 0, options = {})
|
|
6
|
+
val = 0.0 if !val
|
|
7
|
+
val = 1.0 if val == true
|
|
8
|
+
val = val.to_r if val.is_a?(String) && val['/']
|
|
9
|
+
val = val.to_f if val.is_a?(Complex) || val.is_a?(Rational)
|
|
10
|
+
val = val.to_i if val.is_a?(String) && !val['.']
|
|
11
|
+
val = val.to_d / 100 if val.is_a?(Integer) || (val.is_a?(String) && val['%'])
|
|
12
|
+
val = val.value if val.is_a? self.class
|
|
13
|
+
|
|
14
|
+
@value = val.to_d
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
###
|
|
18
|
+
# Attributes
|
|
19
|
+
###
|
|
20
|
+
def value
|
|
21
|
+
@value ||= 0
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
###
|
|
25
|
+
# Convert percentage to different number formats
|
|
26
|
+
###
|
|
27
|
+
def to_i; (self.value * 100).to_i; end
|
|
28
|
+
def to_c; self.value.to_c; end
|
|
29
|
+
def to_d; self.value.to_d; end
|
|
30
|
+
def to_f; self.value.to_f; end
|
|
31
|
+
def to_r; self.value.to_r; end
|
|
32
|
+
|
|
33
|
+
###
|
|
34
|
+
# Convert percentage fraction to percent amount
|
|
35
|
+
###
|
|
36
|
+
def to_complex; (self.value * 100).to_c; end
|
|
37
|
+
def to_decimal; (self.value * 100).to_d; end
|
|
38
|
+
def to_float; (self.value * 100).to_f; end
|
|
39
|
+
def to_rational; (self.value * 100).to_r; end
|
|
40
|
+
|
|
41
|
+
###
|
|
42
|
+
# String conversion methods
|
|
43
|
+
###
|
|
44
|
+
def to_s; self.to_amount.to_s; end
|
|
45
|
+
def to_str; self.to_f.to_s; end
|
|
46
|
+
def to_string; self.to_s + '%'; end
|
|
47
|
+
|
|
48
|
+
def format(options = {})
|
|
49
|
+
# set defaults; all other options default to false
|
|
50
|
+
options[:percent_sign] = options.fetch :percent_sign, true
|
|
51
|
+
|
|
52
|
+
if options[:as_decimal]
|
|
53
|
+
return self.to_str
|
|
54
|
+
elsif options[:rounded]
|
|
55
|
+
string = self.to_float.round.to_s
|
|
56
|
+
elsif options[:no_decimal]
|
|
57
|
+
string = self.to_i.to_s
|
|
58
|
+
elsif options[:no_decimal_if_whole]
|
|
59
|
+
string = self.to_s
|
|
60
|
+
else
|
|
61
|
+
string = self.to_float.to_s
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
string += ' ' if options[:space_before_sign]
|
|
65
|
+
string += '%' if options[:percent_sign]
|
|
66
|
+
return string
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
###
|
|
70
|
+
# Additional conversion methods
|
|
71
|
+
###
|
|
72
|
+
def to_amount
|
|
73
|
+
(int = self.to_i) == (float = self.to_float) ? int : float
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def inspect
|
|
77
|
+
"#<#{self.class.name}:#{self.object_id}, #{self.value.inspect}>"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
###
|
|
81
|
+
# Comparisons
|
|
82
|
+
###
|
|
83
|
+
def == other
|
|
84
|
+
self.eql?(other) || self.to_f == other
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def eql? other
|
|
88
|
+
self.class == other.class && self.value == other.value
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def <=> other
|
|
92
|
+
self.to_f <=> other.to_f
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
###
|
|
96
|
+
# Mathematical operations
|
|
97
|
+
###
|
|
98
|
+
[:+, :-, :/, :*].each do |operator|
|
|
99
|
+
define_method operator do |other|
|
|
100
|
+
case other
|
|
101
|
+
when Percentage
|
|
102
|
+
new_value = self.value.public_send(operator, other.value)
|
|
103
|
+
self.class.new new_value
|
|
104
|
+
else
|
|
105
|
+
self.value.to_f.public_send(operator, other)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
###
|
|
111
|
+
# Additional initialization methods
|
|
112
|
+
###
|
|
113
|
+
def self.from_fraction(val = 0, options = {})
|
|
114
|
+
val = val.to_i if val.is_a?(String) && !(val['/'] || val['%'] || val['.'])
|
|
115
|
+
val = val.to_d if val.is_a?(Integer)
|
|
116
|
+
|
|
117
|
+
self.new val, options
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def self.from_amount(val = 0, options = {})
|
|
121
|
+
val = val.to_r if val.is_a?(String) && val['/']
|
|
122
|
+
val = val.to_d if val.is_a?(String)
|
|
123
|
+
val = val / 100 if val.is_a?(Numeric) && !val.integer?
|
|
124
|
+
|
|
125
|
+
self.new val, options
|
|
126
|
+
end
|
|
127
|
+
end
|
data/percent.gemspec
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Gem::Specification.new do |s|
|
|
2
|
+
s.name = 'percent'
|
|
3
|
+
s.version = '0.0.1'
|
|
4
|
+
s.platform = Gem::Platform::RUBY
|
|
5
|
+
s.license = 'MIT'
|
|
6
|
+
s.authors = ['Joe Kennedy']
|
|
7
|
+
s.email = ['joseph.stephen.kennedy@gmail.com']
|
|
8
|
+
s.summary = 'Percent objects and integration with Rails'
|
|
9
|
+
s.homepage = 'https://github.com/JoeKennedy/percent'
|
|
10
|
+
|
|
11
|
+
s.files = Dir.glob('{lib,spec}/**/*') + %w(percent.gemspec)
|
|
12
|
+
s.files.delete 'spec/percent.sqlite3'
|
|
13
|
+
|
|
14
|
+
s.test_files = s.files.grep(%r{^spec/})
|
|
15
|
+
|
|
16
|
+
s.require_path = 'lib'
|
|
17
|
+
|
|
18
|
+
s.add_dependency 'activesupport', '~> 4.2'
|
|
19
|
+
|
|
20
|
+
s.add_development_dependency 'activerecord', '~> 4.2'
|
|
21
|
+
s.add_development_dependency 'bundler', '~> 1.11'
|
|
22
|
+
s.add_development_dependency 'rake', '~> 10'
|
|
23
|
+
s.add_development_dependency 'rspec', '~> 3.0'
|
|
24
|
+
s.add_development_dependency 'sqlite3', '~> 1.3'
|
|
25
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
class JobTracker < ActiveRecord::Base; end
|
|
4
|
+
|
|
5
|
+
if defined? ActiveRecord
|
|
6
|
+
describe Percent::ActiveRecord::MigrationExtensions::SchemaStatements do
|
|
7
|
+
before :all do
|
|
8
|
+
@connection = ActiveRecord::Base.connection
|
|
9
|
+
@connection.send :extend, Percent::ActiveRecord::MigrationExtensions::SchemaStatements
|
|
10
|
+
|
|
11
|
+
@connection.drop_table :job_trackers if @connection.table_exists? :job_trackers
|
|
12
|
+
@connection.create_table :job_trackers
|
|
13
|
+
|
|
14
|
+
@options = { default: 1, null: true }
|
|
15
|
+
|
|
16
|
+
@connection.add_percentage :job_trackers, :percentage
|
|
17
|
+
@connection.add_percentage :job_trackers, :full_options, @options
|
|
18
|
+
|
|
19
|
+
JobTracker.reset_column_information
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
describe '#add_percentage' do
|
|
23
|
+
context 'default options' do
|
|
24
|
+
subject { JobTracker.columns_hash['percentage_fraction'] }
|
|
25
|
+
|
|
26
|
+
it 'should default to 0' do
|
|
27
|
+
expect(subject.default).to eql '0'
|
|
28
|
+
expect(JobTracker.new.public_send subject.name).to eql 0
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it 'should not allow null values' do
|
|
32
|
+
expect(subject.null).to be false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'should be of type decimal' do
|
|
36
|
+
expect(subject.type).to eql :decimal
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
context 'full options' do
|
|
41
|
+
subject { JobTracker.columns_hash['full_options_fraction'] }
|
|
42
|
+
|
|
43
|
+
it 'should default to 1' do
|
|
44
|
+
expect(subject.default).to eql '1'
|
|
45
|
+
expect(JobTracker.new.public_send subject.name).to eql 1
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it 'should allow null values' do
|
|
49
|
+
expect(subject.null).to be true
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it 'should be of type decimal' do
|
|
53
|
+
expect(subject.type).to eql :decimal
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
describe '#remove_percentage' do
|
|
59
|
+
before :all do
|
|
60
|
+
@connection.remove_percentage :job_trackers, :percentage
|
|
61
|
+
@connection.remove_percentage :job_trackers, :full_options, @options
|
|
62
|
+
|
|
63
|
+
JobTracker.reset_column_information
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it 'should remove percentage columns' do
|
|
67
|
+
expect(JobTracker.columns_hash['percentage_fraction']).to be nil
|
|
68
|
+
expect(JobTracker.columns_hash['full_options_fraction']).to be nil
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
class JobTracker < ActiveRecord::Base; end
|
|
4
|
+
|
|
5
|
+
if defined? ActiveRecord
|
|
6
|
+
describe Percent::ActiveRecord::MigrationExtensions::Table do
|
|
7
|
+
before :all do
|
|
8
|
+
@connection = ActiveRecord::Base.connection
|
|
9
|
+
@connection.send :extend, Percent::ActiveRecord::MigrationExtensions::SchemaStatements
|
|
10
|
+
|
|
11
|
+
@options = { default: 1, null: true }
|
|
12
|
+
|
|
13
|
+
@connection.drop_table :job_trackers if @connection.table_exists? :job_trackers
|
|
14
|
+
@connection.create_table :job_trackers do |t|
|
|
15
|
+
t.percentage :percentage
|
|
16
|
+
t.percentage :full_options, @options
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
JobTracker.reset_column_information
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
describe '#percentage' do
|
|
23
|
+
context 'default options' do
|
|
24
|
+
subject { JobTracker.columns_hash['percentage_fraction'] }
|
|
25
|
+
|
|
26
|
+
it 'should default to 0' do
|
|
27
|
+
expect(subject.default).to eql '0'
|
|
28
|
+
expect(JobTracker.new.public_send subject.name).to eql 0
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it 'should not allow null values' do
|
|
32
|
+
expect(subject.null).to be false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'should be of type decimal' do
|
|
36
|
+
expect(subject.type).to eql :decimal
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
context 'full options' do
|
|
41
|
+
subject { JobTracker.columns_hash['full_options_fraction'] }
|
|
42
|
+
|
|
43
|
+
it 'should default to 1' do
|
|
44
|
+
expect(subject.default).to eql '1'
|
|
45
|
+
expect(JobTracker.new.public_send subject.name).to eql 1
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it 'should allow null values' do
|
|
49
|
+
expect(subject.null).to be true
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it 'should be of type decimal' do
|
|
53
|
+
expect(subject.type).to eql :decimal
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
describe '#remove_percentage' do
|
|
59
|
+
before :all do
|
|
60
|
+
@connection.change_table :job_trackers do |t|
|
|
61
|
+
t.remove_percentage :percentage
|
|
62
|
+
t.remove_percentage :full_options, @options
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
JobTracker.reset_column_information
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it 'should remove percentage columns' do
|
|
69
|
+
expect(JobTracker.columns_hash['percentage_fraction']).to be nil
|
|
70
|
+
expect(JobTracker.columns_hash['full_options_fraction']).to be nil
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|