sheng 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ea5e2737e44d4a27d5d268bdfcefe21e1a3ff51b
4
- data.tar.gz: 3245c5ef173eb186beaf1fb78acd487bfd42e04e
3
+ metadata.gz: a8b81b58b522c27f9eae0f86940da9f8b92743d5
4
+ data.tar.gz: 5ba6ec64dd3891eb409550801fde1dbe95cdb4f5
5
5
  SHA512:
6
- metadata.gz: bd6309f22f7c16ddb93dc94d79fe8dbe4cb34c7ec4eb835883fe8afb1127271f8380c70dc79c262ff8da3f7c8d3bb442bd3e6f9b22fcbc3496b17ae4366f532c
7
- data.tar.gz: 513affc5751aa7de19dc060c13e99974605d9e30afc57ae1711b8666fcd23294493d1c7790e8df6968fba145bd574d520d11a437e9e185ec3c3d1bcce15bc780
6
+ metadata.gz: 9c40035114fcc183da71cb81b1e38e315163399fe7925ef1080f35e414d0f8dd800157e0b027ceb3aafdb5f7eb54b90c1d71d9a9e0374e133d969f008a41173e
7
+ data.tar.gz: 50dc168d3817c280d37e916d77893b8d66ebc81b4c201ce6e1fa90eb4676b9357bb7a2699c7cf20d75672d45fb0aa53c574d12b9e692a3a7161cd7a78a7f18f7
data/.travis.yml ADDED
@@ -0,0 +1,3 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1.7
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Sheng
2
2
 
3
+ [![Build Status](https://travis-ci.org/renewablefunding/sheng.svg?branch=master)](https://travis-ci.org/renewablefunding/sheng)
4
+
3
5
  A Ruby gem for data merging Word documents. Given a set of data (as a Hash), and a `.docx` template created in [a specific way](docs/creating_templates.md), you can generate a new Word document with the values from the data substituted in place.
4
6
 
5
7
  ## Installation
Binary file
@@ -18,6 +18,8 @@ Note that any instructions on how specifically to use certain commands in Word a
18
18
  * [Basic MergeField Substitution](#basic-mergefield-substitution)
19
19
  * [Inline Basic MergeFields](#inline-basic-mergefields)
20
20
  * [Filters on String Values](#filters-on-string-values)
21
+ * [Filters on Numeric Values](#filters-on-numeric-values)
22
+ * [Basic Arithmetic Operations](#basic-arithmetic-operations)
21
23
  * [Checkboxes](#checkboxes)
22
24
  * [Sequences](#sequences)
23
25
  * [Arrays of Primitives](#arrays-of-primitives)
@@ -223,10 +225,32 @@ Filters can also be chained, and will be applied in which they appear (from left
223
225
 
224
226
  «a_basic_integer|downcase»
225
227
 
228
+ ## Filters on Numeric Values
229
+
230
+ There are also built-in filters available for modifying the output of numeric values (e.g. to round a number or display it as currency). These filters, if used on non-numeric values, will have no effect. Strings that can be read as numeric values (like "145.2") will work with these filters. Unlike the string filters, these numeric filters can accept arguments, to clarify what the filter should do. The filters are:
231
+
232
+ | Name | Description | Example Input | Example Command | Example Output |
233
+ | --- | --- | --- | --- | --- |
234
+ | round(n) | Rounds number to n decimal places | 2360.7853 | round(2) | 2360.79 |
235
+ | floor | Truncates decimal portion of number | 2360.7853 | floor | 2360 |
236
+ | currency(x) | Formats number as currency (2 fixed decimals, thousands separator), prepending optional argument | 149020.5 | currency($) | $149,020.50 |
237
+
238
+ Filters can also be chained, and will be applied in which they appear (from left to right).
239
+
240
+ ### Examples:
241
+
242
+ «a_basic_float|round(1)»
243
+
244
+ «a_basic_float|currency($)»
245
+
246
+ «a_basic_float|floor»
247
+
226
248
  ## Basic Arithmetic Operations
227
249
 
228
250
  You can perform basic arithmetic operations within a mergefield, using either hardcoded numeric values or variable names. The operators allowed are + (addition), - (subtraction), * (multiplication), and / (division). The usual order of precedence applies with these operators, and you can use parenthesis to control that precedence.
229
251
 
252
+ Note, however, that adding formatted numbers (like "1,400.50" and "24,000.20") will result in an unformatted number ("25400.7"), so if you want currency formatting or rounding, you'll want to use the numeric formatting methods shown above. You can combine arithmetic with filters to accomplish this, as seen in the last example below.
253
+
230
254
  ### Examples:
231
255
 
232
256
  «25 * 6»
@@ -237,6 +261,8 @@ You can perform basic arithmetic operations within a mergefield, using either ha
237
261
 
238
262
  «(hair.length * 2) + head.height»
239
263
 
264
+ «principal + interest | currency($)»
265
+
240
266
  # Checkboxes
241
267
 
242
268
  To create a checkbox:
@@ -0,0 +1,23 @@
1
+ module Sheng
2
+ module Filters
3
+ class Base
4
+ attr_reader :value, :method, :arguments
5
+
6
+ def initialize(method: method, arguments: [])
7
+ @method = method
8
+ @arguments = arguments
9
+ end
10
+
11
+ def self.implements(*names)
12
+ names.each do |name|
13
+ Sheng::Filters.registry.merge!({ name.to_sym => self })
14
+ end
15
+ end
16
+
17
+ def filter(value)
18
+ return value unless value.respond_to?(method)
19
+ value.send(method, *arguments)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,16 @@
1
+ require_relative "base"
2
+
3
+ module Sheng
4
+ module Filters
5
+ class CurrencyFormattingFilter < Base
6
+ implements :currency
7
+
8
+ def filter(value)
9
+ return value unless Sheng::Support.is_numeric?(value)
10
+ integer, fractional = ("%00.2f" % value).split(".")
11
+ integer.reverse!.gsub!(/(\d{3})(?=\d)/, '\\1,').reverse!
12
+ "#{arguments.first}#{integer}.#{fractional}"
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,14 @@
1
+ require_relative "base"
2
+
3
+ module Sheng
4
+ module Filters
5
+ class NumericFilter < Base
6
+ implements :round, :floor
7
+
8
+ def filter(value)
9
+ value = Sheng::Support.typecast_numeric(value)
10
+ super
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,9 @@
1
+ require_relative "base"
2
+
3
+ module Sheng
4
+ module Filters
5
+ class StringFilter < Base
6
+ implements :upcase, :downcase, :capitalize, :titleize, :reverse
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,23 @@
1
+ module Sheng
2
+ module Filters
3
+ class UnsupportedFilterError < StandardError; end
4
+
5
+ class << self
6
+ def registry
7
+ @registry ||= {}
8
+ end
9
+
10
+ def filter_for(filter_string)
11
+ filter_method, args_list = filter_string.split(/[\(\)]/)
12
+ args = (args_list || "").split(/\s*,\s*/).map { |arg| Sheng::Support.typecast_numeric(arg) }
13
+ filter_class = registry[filter_method.to_sym]
14
+ raise UnsupportedFilterError.new(filter_string) unless filter_class
15
+ filter_class.new(method: filter_method, arguments: args)
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ require_relative "filters/string_filter"
22
+ require_relative "filters/numeric_filter"
23
+ require_relative "filters/currency_formatting_filter"
@@ -5,10 +5,8 @@ module Sheng
5
5
  MATH_TOKENS = %w[+ - / * ( )]
6
6
  REGEXES = {
7
7
  instruction_text: /^\s*MERGEFIELD(.*)\\\* MERGEFORMAT\s*$/,
8
- key_string: /^(?<prefix>start:|end:|if:|end_if:|unless:|end_unless:)?\s*(?<key>[^\|]+)\s*\|?(?<filters>.*)?/,
9
- numeric_string: /^[-+]?[0-9]*\.?[0-9]+$/
8
+ key_string: /^(?<prefix>start:|end:|if:|end_if:|unless:|end_unless:)?\s*(?<key>[^\|]+)\s*\|?(?<filters>.*)?/
10
9
  }
11
- ALLOWED_FILTERS = [:upcase, :downcase, :capitalize, :titleize, :reverse]
12
10
 
13
11
  class NotAMergeFieldError < StandardError; end
14
12
 
@@ -25,7 +23,7 @@ module Sheng
25
23
  def initialize(element)
26
24
  @element = element
27
25
  @xml_document = element.document
28
- @instruction_text = mergefield_instruction_text
26
+ @instruction_text = Sheng::Support.extract_mergefield_instruction_text(element)
29
27
  end
30
28
 
31
29
  def ==(other)
@@ -46,11 +44,7 @@ module Sheng
46
44
  end
47
45
 
48
46
  def raw_key
49
- @raw_key ||= mergefield_instruction_text.gsub(REGEXES[:instruction_text], '\1').strip
50
- end
51
-
52
- def mergefield_instruction_text
53
- Sheng::Support.extract_mergefield_instruction_text(element)
47
+ @raw_key ||= @instruction_text.gsub(REGEXES[:instruction_text], '\1').strip
54
48
  end
55
49
 
56
50
  def styling_paragraph
@@ -184,15 +178,22 @@ module Sheng
184
178
  end
185
179
 
186
180
  def replace_mergefield(value)
181
+ value_as_string = if value.is_a?(BigDecimal)
182
+ value.to_s("F")
183
+ else
184
+ value.to_s
185
+ end
186
+
187
187
  new_run = Sheng::Support.new_text_run(
188
- value, xml_document: xml_document, style_run: styling_run
188
+ value_as_string, xml_document: xml_document, style_run: styling_run
189
189
  )
190
190
  xml.before(new_run)
191
191
  xml.remove
192
192
  end
193
193
 
194
194
  def key_parts
195
- @key_parts ||= key.gsub(".", "_DOTSEPARATOR_").
195
+ @key_parts ||= key.gsub(",", "").
196
+ gsub(".", "_DOTSEPARATOR_").
196
197
  split(/\b|\s/).
197
198
  map(&:strip).
198
199
  reject(&:empty?).
@@ -203,7 +204,7 @@ module Sheng
203
204
 
204
205
  def required_variables
205
206
  key_parts.reject { |token|
206
- REGEXES[:numeric_string].match(token) || MATH_TOKENS.include?(token)
207
+ Support.is_numeric?(token) || MATH_TOKENS.include?(token)
207
208
  }
208
209
  end
209
210
 
@@ -224,7 +225,7 @@ module Sheng
224
225
 
225
226
  def get_value(data_set)
226
227
  interpolated_string = key_parts.map { |token|
227
- if REGEXES[:numeric_string].match(token) || MATH_TOKENS.include?(token)
228
+ if Support.is_numeric?(token) || MATH_TOKENS.include?(token)
228
229
  token
229
230
  else
230
231
  data_set.fetch(token)
@@ -233,13 +234,13 @@ module Sheng
233
234
 
234
235
  return interpolated_string unless key_has_math?
235
236
 
236
- Dentaku::Calculator.new.evaluate!(interpolated_string)
237
+ Dentaku::Calculator.new.evaluate!(interpolated_string.gsub(",", ""))
237
238
  end
238
239
 
239
240
  def interpolate(data_set)
240
241
  value = get_value(data_set)
241
242
  replace_mergefield(filter_value(value))
242
- rescue DataSet::KeyNotFound, Dentaku::UnboundVariableError
243
+ rescue DataSet::KeyNotFound, Dentaku::UnboundVariableError, Filters::UnsupportedFilterError
243
244
  # Ignore this error; we'll collect all uninterpolated fields later and
244
245
  # raise a new exception, so we can list all the fields in an error
245
246
  # message.
@@ -247,12 +248,9 @@ module Sheng
247
248
  end
248
249
 
249
250
  def filter_value(value)
250
- filters.inject(value) { |val, filter|
251
- if ALLOWED_FILTERS.include?(filter.to_sym) && val.respond_to?(filter.to_sym)
252
- val.send(filter)
253
- else
254
- val
255
- end
251
+ filters.inject(value) { |val, filter_string|
252
+ filterer = Filters.filter_for(filter_string)
253
+ filterer.filter(val)
256
254
  }
257
255
  end
258
256
  end
data/lib/sheng/support.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  module Sheng
2
2
  module Support
3
3
  class << self
4
+ NUMERIC_REGEX = /^(0|[1-9][0-9]*|[1-9][0-9]{0,2}(,[0-9]{3,3})*)(\.[0-9]+)?$/
5
+
4
6
  def merge_required_hashes(hsh1, hsh2)
5
7
  hsh1.merge(hsh2) do |key, old_value, new_value|
6
8
  if [old_value, new_value].all? { |v| v.is_a?(Hash) }
@@ -52,6 +54,20 @@ module Sheng
52
54
  end
53
55
  label
54
56
  end
57
+
58
+ def is_numeric?(value)
59
+ value.is_a?(Numeric) || !!NUMERIC_REGEX.match(value)
60
+ end
61
+
62
+ def typecast_numeric(value)
63
+ return value if value.is_a?(Numeric) || !NUMERIC_REGEX.match(value)
64
+ val = value.gsub(",", "").to_d
65
+ if val.frac == 0
66
+ val.to_i
67
+ else
68
+ val
69
+ end
70
+ end
55
71
  end
56
72
  end
57
73
  end
data/lib/sheng/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Sheng
2
- VERSION = "0.4.0"
2
+ VERSION = "0.5.0"
3
3
  end
data/lib/sheng.rb CHANGED
@@ -7,6 +7,7 @@ require 'sheng/data_set'
7
7
  require 'sheng/docx'
8
8
  require 'sheng/wml_file'
9
9
  require 'sheng/merge_field_set'
10
+ require 'sheng/filters'
10
11
  require 'sheng/merge_field'
11
12
  require 'sheng/sequence'
12
13
  require 'sheng/conditional_block'
@@ -0,0 +1,14 @@
1
+ <w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
2
+ <w:p>
3
+ <w:fldSimple w:instr=" MERGEFIELD robots * 6250 + 18.3 | currency(£) \* MERGEFORMAT ">
4
+ <w:r>
5
+ <w:rPr>
6
+ <w:b/>
7
+ <w:i/>
8
+ <w:noProof/>
9
+ </w:rPr>
10
+ <w:t>«robots * 6250 + 18.3 | currency(£)»</w:t>
11
+ </w:r>
12
+ </w:fldSimple>
13
+ </w:p>
14
+ </w:document>
@@ -1,13 +1,13 @@
1
1
  <w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
2
2
  <w:p>
3
- <w:fldSimple w:instr=" MERGEFIELD baskets.count * (3 + origami) \* MERGEFORMAT ">
3
+ <w:fldSimple w:instr=" MERGEFIELD baskets.count * (3,500 + 2 - origami) \* MERGEFORMAT ">
4
4
  <w:r>
5
5
  <w:rPr>
6
6
  <w:b/>
7
7
  <w:i/>
8
8
  <w:noProof/>
9
9
  </w:rPr>
10
- <w:t>«baskets.count * (3 + origami)»</w:t>
10
+ <w:t>«baskets.count * (3,500 + 2 - origami)»</w:t>
11
11
  </w:r>
12
12
  </w:fldSimple>
13
13
  </w:p>
@@ -0,0 +1,12 @@
1
+ <w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
2
+ <w:p>
3
+ <w:r>
4
+ <w:rPr>
5
+ <w:b/>
6
+ <w:i/>
7
+ <w:noProof/>
8
+ </w:rPr>
9
+ <w:t>£18,768.30</w:t>
10
+ </w:r>
11
+ </w:p>
12
+ </w:document>
@@ -6,7 +6,7 @@
6
6
  <w:i/>
7
7
  <w:noProof/>
8
8
  </w:rPr>
9
- <w:t>22</w:t>
9
+ <w:t>8036447.4</w:t>
10
10
  </w:r>
11
11
  </w:p>
12
12
  </w:document>
@@ -0,0 +1,20 @@
1
+ require "shared_examples/filters"
2
+
3
+ describe Sheng::Filters::CurrencyFormattingFilter do
4
+ test_cases = [
5
+ { method: :currency, input: 3452341.826, output: "3,452,341.83" },
6
+ { method: :currency, input: "1645.3", output: "1,645.30" },
7
+ { method: :currency, arguments: ["¥"], input: "12351.184", output: "¥12,351.18" }
8
+ ]
9
+
10
+ it_behaves_like "a filter", test_cases
11
+
12
+ context "with non-numeric value" do
13
+ describe "#filter" do
14
+ it "returns unmodified value" do
15
+ subject = described_class.new(method: :currency)
16
+ expect(subject.filter("apples")).to eq("apples")
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,19 @@
1
+ require "shared_examples/filters"
2
+
3
+ describe Sheng::Filters::NumericFilter do
4
+ test_cases = [
5
+ { method: :round, arguments: [2], input: 149.3783, output: 149.38 },
6
+ { method: :floor, input: 149.3783, output: 149 }
7
+ ]
8
+
9
+ it_behaves_like "a filter", test_cases
10
+
11
+ context "with value that does not respond to method" do
12
+ describe "#filter" do
13
+ it "returns unmodified value" do
14
+ subject = described_class.new(method: :round)
15
+ expect(subject.filter("apples")).to eq("apples")
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,22 @@
1
+ require "shared_examples/filters"
2
+
3
+ describe Sheng::Filters::StringFilter do
4
+ test_cases = [
5
+ { method: :upcase, input: "some Good thing", output: "SOME GOOD THING" },
6
+ { method: :downcase, input: "Emerald CITY", output: "emerald city" },
7
+ { method: :reverse, input: "A Man a Plan", output: "nalP a naM A" },
8
+ { method: :capitalize, input: "good will hunting", output: "Good will hunting" },
9
+ { method: :titleize, input: "good will hunting", output: "Good Will Hunting" }
10
+ ]
11
+
12
+ it_behaves_like "a filter", test_cases
13
+
14
+ context "with value that does not respond to method" do
15
+ describe "#filter" do
16
+ it "returns unmodified value" do
17
+ subject = described_class.new(method: :upcase)
18
+ expect(subject.filter(12345)).to eq(12345)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ describe Sheng::Filters do
2
+ before(:each) do
3
+ allow(described_class).to receive(:registry).
4
+ and_return(bazinga: Sheng::Filters::Base)
5
+ end
6
+
7
+ describe ".filter_for" do
8
+ it "returns a filter instance that supports the given string" do
9
+ allow(Sheng::Filters::Base).to receive(:new).
10
+ with(method: "bazinga", arguments: [12, "hats"]).
11
+ and_return(:the_filter)
12
+ expect(described_class.filter_for("bazinga(12, hats)")).
13
+ to eq(:the_filter)
14
+ end
15
+
16
+ it "raises exception if filter class not found" do
17
+ expect {
18
+ described_class.filter_for("unregistered(12, hats)")
19
+ }.to raise_error(Sheng::Filters::UnsupportedFilterError)
20
+ end
21
+ end
22
+ end
@@ -27,6 +27,22 @@ describe Sheng::MergeField do
27
27
  end
28
28
  end
29
29
 
30
+ describe "mergefield with math and currency formatting" do
31
+ let(:fragment) { xml_fragment('input/merge_field/currency_merge_field') }
32
+ let(:element) { fragment.xpath("//w:fldSimple[contains(@w:instr, 'MERGEFIELD')]").first }
33
+
34
+ describe '#interpolate' do
35
+ it 'interpolates values from dataset into mergefield' do
36
+ dataset = Sheng::DataSet.new({
37
+ :robots => 3
38
+ })
39
+
40
+ subject.interpolate(dataset)
41
+ expect(subject.xml_document).to be_equivalent_to xml_fragment('output/merge_field/currency_merge_field')
42
+ end
43
+ end
44
+ end
45
+
30
46
  describe "mergefield with math operations" do
31
47
  let(:fragment) { xml_fragment('input/merge_field/math_merge_field') }
32
48
  let(:element) { fragment.xpath("//w:fldSimple[contains(@w:instr, 'MERGEFIELD')]").first }
@@ -35,9 +51,9 @@ describe Sheng::MergeField do
35
51
  it 'interpolates values from dataset into mergefield' do
36
52
  dataset = Sheng::DataSet.new({
37
53
  :baskets => {
38
- :count => 2
54
+ :count => "2,300.40"
39
55
  },
40
- :origami => 8
56
+ :origami => 8.5
41
57
  })
42
58
 
43
59
  subject.interpolate(dataset)
@@ -157,8 +173,8 @@ describe Sheng::MergeField do
157
173
  end
158
174
 
159
175
  it "performs math operations on values from dataset" do
160
- allow(subject).to receive(:key).and_return("(numbers.first * numbers.second) + 5")
161
- expect(subject.get_value(dataset)).to eq(189)
176
+ allow(subject).to receive(:key).and_return("(numbers.first * numbers.second) + 5.3")
177
+ expect(subject.get_value(dataset)).to eq(189.3)
162
178
  end
163
179
 
164
180
  it "performs math operations with no dataset lookup" do
@@ -207,6 +223,13 @@ describe Sheng::MergeField do
207
223
  expect(subject).to receive(:replace_mergefield).never
208
224
  expect(subject.interpolate(:a_dataset)).to be_nil
209
225
  end
226
+
227
+ it "does not replace and returns nil if unsupported filter requested" do
228
+ allow(subject).to receive(:get_value).with(:a_dataset).and_return(:got_value)
229
+ allow(subject).to receive(:filter_value).with(:got_value).and_raise(Sheng::Filters::UnsupportedFilterError)
230
+ expect(subject).to receive(:replace_mergefield).never
231
+ expect(subject.interpolate(:a_dataset)).to be_nil
232
+ end
210
233
  end
211
234
 
212
235
  describe "#xml" do
@@ -314,44 +337,29 @@ describe Sheng::MergeField do
314
337
  end
315
338
 
316
339
  describe "#filter_value" do
317
- it "can upcase" do
318
- allow(subject).to receive(:filters).and_return(["upcase"])
319
- expect(subject.filter_value("HorSes")).to eq("HORSES")
320
- end
321
-
322
- it "can downcase" do
323
- allow(subject).to receive(:filters).and_return(["downcase"])
324
- expect(subject.filter_value("HorSes")).to eq("horses")
325
- end
326
-
327
- it "can reverse" do
328
- allow(subject).to receive(:filters).and_return(["reverse"])
329
- expect(subject.filter_value("Maple")).to eq("elpaM")
330
- end
331
-
332
- it "can titleize" do
333
- allow(subject).to receive(:filters).and_return(["titleize"])
334
- expect(subject.filter_value("ribbons are grand")).to eq("Ribbons Are Grand")
335
- end
336
-
337
- it "can capitalize" do
338
- allow(subject).to receive(:filters).and_return(["capitalize"])
339
- expect(subject.filter_value("ribbons are grand")).to eq("Ribbons are grand")
340
+ it "looks up filter and returns filtered result" do
341
+ filter_double = instance_double(Sheng::Filters::Base)
342
+ allow(subject).to receive(:filters).and_return(["foo"])
343
+ allow(Sheng::Filters).to receive(:filter_for).with("foo").
344
+ and_return(filter_double)
345
+ allow(filter_double).to receive(:filter).with("HorSes").
346
+ and_return("ponies")
347
+ expect(subject.filter_value("HorSes")).to eq("ponies")
340
348
  end
341
349
 
342
350
  it "works with multiple filters" do
343
- allow(subject).to receive(:filters).and_return(["reverse", "capitalize"])
344
- expect(subject.filter_value("maple")).to eq("Elpam")
345
- end
346
-
347
- it "does nothing if filter not recognized" do
348
- allow(subject).to receive(:filters).and_return(["elephantize"])
349
- expect(subject.filter_value("ribbons are grand")).to eq("ribbons are grand")
350
- end
351
-
352
- it "does nothing if value doesn't respond to filter" do
353
- allow(subject).to receive(:filters).and_return(["upcase"])
354
- expect(subject.filter_value(130)).to eq(130)
351
+ filter1_double = instance_double(Sheng::Filters::Base)
352
+ filter2_double = instance_double(Sheng::Filters::Base)
353
+ allow(subject).to receive(:filters).and_return(["foo", "bar"])
354
+ allow(Sheng::Filters).to receive(:filter_for).with("foo").
355
+ and_return(filter1_double)
356
+ allow(Sheng::Filters).to receive(:filter_for).with("bar").
357
+ and_return(filter2_double)
358
+ allow(filter1_double).to receive(:filter).with("HorSes").
359
+ and_return("ponies")
360
+ allow(filter2_double).to receive(:filter).with("ponies").
361
+ and_return("Scuba Gear")
362
+ expect(subject.filter_value("HorSes")).to eq("Scuba Gear")
355
363
  end
356
364
  end
357
365
 
@@ -0,0 +1,53 @@
1
+ describe Sheng::Support do
2
+ describe ".is_numeric?" do
3
+ it "returns true if given integer" do
4
+ expect(described_class.is_numeric?(123)).to be true
5
+ end
6
+
7
+ it "returns true if given float" do
8
+ expect(described_class.is_numeric?(123.4)).to be true
9
+ end
10
+
11
+ it "returns true if given BigDecimal" do
12
+ expect(described_class.is_numeric?(123.4.to_d)).to be true
13
+ end
14
+
15
+ it "returns true if given valid numeric string" do
16
+ expect(described_class.is_numeric?("123.4")).to be true
17
+ end
18
+
19
+ it "returns false if given invalid numeric string" do
20
+ expect(described_class.is_numeric?("0123.4")).to be false
21
+ end
22
+
23
+ it "returns true if given valid numeric string with appropriate commas" do
24
+ expect(described_class.is_numeric?("1,234.5")).to be true
25
+ end
26
+
27
+ it "returns false if given valid numeric string with appropriate commas" do
28
+ expect(described_class.is_numeric?("12,34.5")).to be false
29
+ end
30
+
31
+ it "returns false if given non-numeric string" do
32
+ expect(described_class.is_numeric?("foobar")).to be false
33
+ end
34
+ end
35
+
36
+ describe ".typecast_numeric" do
37
+ it "returns given numeric" do
38
+ expect(described_class.typecast_numeric(123.4)).to eq(123.4)
39
+ end
40
+
41
+ it "returns given non-numeric string" do
42
+ expect(described_class.typecast_numeric("foobar")).to eq("foobar")
43
+ end
44
+
45
+ it "returns numeric version of integer string" do
46
+ expect(described_class.typecast_numeric("123")).to eq(123)
47
+ end
48
+
49
+ it "returns numeric version of decimal string" do
50
+ expect(described_class.typecast_numeric("123.4")).to eq(123.4)
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,18 @@
1
+ RSpec.shared_examples_for "a filter" do |test_cases|
2
+ test_cases.map { |tc| tc[:method] }.uniq.each do |method|
3
+ it "registers self for #{method} method" do
4
+ expect(Sheng::Filters.registry[method]).to eq(described_class)
5
+ end
6
+ end
7
+
8
+ test_cases.each do |test_case|
9
+ context "with #{test_case[:method]} method" do
10
+ describe "#filter" do
11
+ it "returns filtered output" do
12
+ subject = described_class.new(method: test_case[:method], arguments: test_case[:arguments] || [])
13
+ expect(subject.filter(test_case[:input])).to eq(test_case[:output])
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sheng
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ravi Gadad
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2016-01-27 00:00:00.000000000 Z
12
+ date: 2016-03-24 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
@@ -162,6 +162,7 @@ files:
162
162
  - ".gitignore"
163
163
  - ".rspec"
164
164
  - ".ruby-version"
165
+ - ".travis.yml"
165
166
  - ".watchr"
166
167
  - Gemfile
167
168
  - LICENSE.txt
@@ -175,6 +176,11 @@ files:
175
176
  - lib/sheng/conditional_block.rb
176
177
  - lib/sheng/data_set.rb
177
178
  - lib/sheng/docx.rb
179
+ - lib/sheng/filters.rb
180
+ - lib/sheng/filters/base.rb
181
+ - lib/sheng/filters/currency_formatting_filter.rb
182
+ - lib/sheng/filters/numeric_filter.rb
183
+ - lib/sheng/filters/string_filter.rb
178
184
  - lib/sheng/merge_field.rb
179
185
  - lib/sheng/merge_field_set.rb
180
186
  - lib/sheng/path_helpers.rb
@@ -207,6 +213,7 @@ files:
207
213
  - spec/fixtures/xml_fragments/input/merge_field/bad/not_a_real_mergefield_new.xml
208
214
  - spec/fixtures/xml_fragments/input/merge_field/bad/not_a_real_mergefield_old.xml
209
215
  - spec/fixtures/xml_fragments/input/merge_field/bad/unclosed_merge_field.xml
216
+ - spec/fixtures/xml_fragments/input/merge_field/currency_merge_field.xml
210
217
  - spec/fixtures/xml_fragments/input/merge_field/inline_merge_field.xml
211
218
  - spec/fixtures/xml_fragments/input/merge_field/math_merge_field.xml
212
219
  - spec/fixtures/xml_fragments/input/merge_field/merge_field.xml
@@ -238,6 +245,7 @@ files:
238
245
  - spec/fixtures/xml_fragments/output/conditional_block/inline_exists.xml
239
246
  - spec/fixtures/xml_fragments/output/conditional_block/unless_does_not_exist.xml
240
247
  - spec/fixtures/xml_fragments/output/conditional_block/unless_exists.xml
248
+ - spec/fixtures/xml_fragments/output/merge_field/currency_merge_field.xml
241
249
  - spec/fixtures/xml_fragments/output/merge_field/inline_merge_field.xml
242
250
  - spec/fixtures/xml_fragments/output/merge_field/math_merge_field.xml
243
251
  - spec/fixtures/xml_fragments/output/merge_field/merge_field.xml
@@ -259,10 +267,16 @@ files:
259
267
  - spec/lib/sheng/conditional_block_spec.rb
260
268
  - spec/lib/sheng/data_set_spec.rb
261
269
  - spec/lib/sheng/docx_spec.rb
270
+ - spec/lib/sheng/filters/currency_formatting_filter_spec.rb
271
+ - spec/lib/sheng/filters/numeric_filter_spec.rb
272
+ - spec/lib/sheng/filters/string_filter_spec.rb
273
+ - spec/lib/sheng/filters_spec.rb
262
274
  - spec/lib/sheng/merge_field_set_spec.rb
263
275
  - spec/lib/sheng/merge_field_spec.rb
264
276
  - spec/lib/sheng/sequence_spec.rb
277
+ - spec/lib/sheng/support_spec.rb
265
278
  - spec/lib/sheng/wml_file_spec.rb
279
+ - spec/shared_examples/filters.rb
266
280
  - spec/spec_helper.rb
267
281
  - spec/support/path_helper.rb
268
282
  - spec/support/xml_helper.rb
@@ -316,6 +330,7 @@ test_files:
316
330
  - spec/fixtures/xml_fragments/input/merge_field/bad/not_a_real_mergefield_new.xml
317
331
  - spec/fixtures/xml_fragments/input/merge_field/bad/not_a_real_mergefield_old.xml
318
332
  - spec/fixtures/xml_fragments/input/merge_field/bad/unclosed_merge_field.xml
333
+ - spec/fixtures/xml_fragments/input/merge_field/currency_merge_field.xml
319
334
  - spec/fixtures/xml_fragments/input/merge_field/inline_merge_field.xml
320
335
  - spec/fixtures/xml_fragments/input/merge_field/math_merge_field.xml
321
336
  - spec/fixtures/xml_fragments/input/merge_field/merge_field.xml
@@ -347,6 +362,7 @@ test_files:
347
362
  - spec/fixtures/xml_fragments/output/conditional_block/inline_exists.xml
348
363
  - spec/fixtures/xml_fragments/output/conditional_block/unless_does_not_exist.xml
349
364
  - spec/fixtures/xml_fragments/output/conditional_block/unless_exists.xml
365
+ - spec/fixtures/xml_fragments/output/merge_field/currency_merge_field.xml
350
366
  - spec/fixtures/xml_fragments/output/merge_field/inline_merge_field.xml
351
367
  - spec/fixtures/xml_fragments/output/merge_field/math_merge_field.xml
352
368
  - spec/fixtures/xml_fragments/output/merge_field/merge_field.xml
@@ -368,10 +384,16 @@ test_files:
368
384
  - spec/lib/sheng/conditional_block_spec.rb
369
385
  - spec/lib/sheng/data_set_spec.rb
370
386
  - spec/lib/sheng/docx_spec.rb
387
+ - spec/lib/sheng/filters/currency_formatting_filter_spec.rb
388
+ - spec/lib/sheng/filters/numeric_filter_spec.rb
389
+ - spec/lib/sheng/filters/string_filter_spec.rb
390
+ - spec/lib/sheng/filters_spec.rb
371
391
  - spec/lib/sheng/merge_field_set_spec.rb
372
392
  - spec/lib/sheng/merge_field_spec.rb
373
393
  - spec/lib/sheng/sequence_spec.rb
394
+ - spec/lib/sheng/support_spec.rb
374
395
  - spec/lib/sheng/wml_file_spec.rb
396
+ - spec/shared_examples/filters.rb
375
397
  - spec/spec_helper.rb
376
398
  - spec/support/path_helper.rb
377
399
  - spec/support/xml_helper.rb