dyi 0.0.0

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.
Files changed (48) hide show
  1. data/COPYING +674 -0
  2. data/README +28 -0
  3. data/examples/class_diagram.rb +151 -0
  4. data/examples/data/03311056.xlsx +0 -0
  5. data/examples/data/currency.xlsx +0 -0
  6. data/examples/data/money.csv +12 -0
  7. data/examples/line_and_bar.rb +26 -0
  8. data/examples/line_chart.rb +30 -0
  9. data/examples/logo.rb +68 -0
  10. data/examples/pie_chart.rb +19 -0
  11. data/examples/simple_shapes.rb +15 -0
  12. data/lib/dyi.rb +49 -0
  13. data/lib/dyi/chart.rb +34 -0
  14. data/lib/dyi/chart/array_reader.rb +136 -0
  15. data/lib/dyi/chart/base.rb +580 -0
  16. data/lib/dyi/chart/csv_reader.rb +93 -0
  17. data/lib/dyi/chart/excel_reader.rb +100 -0
  18. data/lib/dyi/chart/line_chart.rb +468 -0
  19. data/lib/dyi/chart/pie_chart.rb +141 -0
  20. data/lib/dyi/chart/table.rb +201 -0
  21. data/lib/dyi/color.rb +218 -0
  22. data/lib/dyi/coordinate.rb +224 -0
  23. data/lib/dyi/drawing.rb +32 -0
  24. data/lib/dyi/drawing/canvas.rb +100 -0
  25. data/lib/dyi/drawing/clipping.rb +61 -0
  26. data/lib/dyi/drawing/color_effect.rb +118 -0
  27. data/lib/dyi/drawing/filter.rb +74 -0
  28. data/lib/dyi/drawing/pen.rb +231 -0
  29. data/lib/dyi/drawing/pen_3d.rb +270 -0
  30. data/lib/dyi/font.rb +132 -0
  31. data/lib/dyi/formatter.rb +36 -0
  32. data/lib/dyi/formatter/base.rb +245 -0
  33. data/lib/dyi/formatter/emf_formatter.rb +253 -0
  34. data/lib/dyi/formatter/eps_formatter.rb +397 -0
  35. data/lib/dyi/formatter/svg_formatter.rb +260 -0
  36. data/lib/dyi/formatter/svg_reader.rb +113 -0
  37. data/lib/dyi/formatter/xaml_formatter.rb +317 -0
  38. data/lib/dyi/length.rb +399 -0
  39. data/lib/dyi/matrix.rb +122 -0
  40. data/lib/dyi/painting.rb +177 -0
  41. data/lib/dyi/shape.rb +1332 -0
  42. data/lib/dyi/svg_element.rb +149 -0
  43. data/lib/dyi/type.rb +104 -0
  44. data/lib/ironruby.rb +326 -0
  45. data/lib/util.rb +231 -0
  46. data/test/path_command_test.rb +217 -0
  47. data/test/test_length.rb +91 -0
  48. metadata +114 -0
@@ -0,0 +1,113 @@
1
+ # -*- encoding: UTF-8 -*-
2
+
3
+ # Copyright (c) 2009-2011 Sound-F Co., Ltd. All rights reserved.
4
+ #
5
+ # Author:: Mamoru Yuo
6
+ #
7
+ # This file is part of DYI.
8
+ #
9
+ # DYI is free software: you can redistribute it and/or modify it
10
+ # under the terms of the GNU General Public License as published by
11
+ # the Free Software Foundation, either version 3 of the License, or
12
+ # (at your option) any later version.
13
+ #
14
+ # DYI is distributed in the hope that it will be useful,
15
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
16
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
+ # GNU General Public License for more details.
18
+ #
19
+ # You should have received a copy of the GNU General Public License
20
+ # along with DYI. If not, see <http://www.gnu.org/licenses/>.
21
+
22
+ require 'rexml/document'
23
+
24
+ module DYI #:nodoc:
25
+ module Formatter #:nodoc:
26
+
27
+ class SvgReader
28
+ class << self
29
+ def read(file_name)
30
+ doc = REXML::Document.new(open(file_name))
31
+ container = DYI::Shape::ShapeGroup.new
32
+ doc.root.elements.each do |element|
33
+ container.child_elements.push(create_shape(element))
34
+ end
35
+ container
36
+ end
37
+
38
+ private
39
+
40
+ def create_shape(element)
41
+ case element.name
42
+ when 'g' then create_shape_group(element)
43
+ when 'polygon' then create_polygon(element)
44
+ when 'path' then create_path(element)
45
+ end
46
+ end
47
+
48
+ def create_shape_group(element)
49
+ group = DYI::Shape::ShapeGroup.new
50
+ element.elements.each do |child|
51
+ child_element = create_shape(child)
52
+ group.child_elements.push(child_element) if child_element
53
+ end
54
+ group
55
+ end
56
+
57
+ def create_polygon(element)
58
+ color = DYI::Color.new(element.attributes['fill']) if element.attributes['fill'] != 'none'
59
+ points = element.attributes['points'].split(/\s+/).map {|pt| pt.scan(/-?[\.0-9]+/).map {|s| s.to_f}}
60
+ path = nil
61
+ points.each do |pt|
62
+ if path
63
+ path.line_to(pt)
64
+ else
65
+ path = DYI::Shape::Polygon.new(pt, :painting => {:fill => color})
66
+ end
67
+ end
68
+ path
69
+ end
70
+
71
+ def create_path(element)
72
+ color = DYI::Color.new(element.attributes['fill']) if element.attributes['fill'] != 'none'
73
+ pathes = element.attributes['d'].scan(/(M|L|l|H|h|V|v|C|c|z)\s*(\s*(?:\s*,?[\-\.0-9]+)*)/)
74
+ path = nil
75
+ if pathes.first.first == 'M'
76
+ pathes.each do |p_el|
77
+ coors = p_el[1].scan(/-?[\.0-9]+/).map {|s| s.to_f}
78
+ case p_el.first
79
+ when 'M'
80
+ if path
81
+ path.move_to(coors)
82
+ else
83
+ path = DYI::Shape::Path.new(coors, :painting => {:fill => color})
84
+ end
85
+ when 'L'
86
+ path.line_to(coors)
87
+ when 'l'
88
+ path.line_to(coors, true)
89
+ when 'H'
90
+ p 'H'
91
+ path.line_to([coors.first, path.current_point.y])
92
+ when 'h'
93
+ path.line_to([coors.first, 0], true)
94
+ when 'V'
95
+ p 'V'
96
+ path.line_to([path.current_point.x, coors.first])
97
+ when 'v'
98
+ path.line_to([0, coors.first], true)
99
+ when 'C'
100
+ path.curve_to([coors[0..1], coors[2..3], coors[4..5]])
101
+ when 'c'
102
+ path.curve_to([coors[0..1], coors[2..3], coors[4..5]], true)
103
+ when 'z'
104
+ path.close_path
105
+ end
106
+ end
107
+ end
108
+ path
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,317 @@
1
+ # -*- encoding: UTF-8 -*-
2
+
3
+ # Copyright (c) 2009-2011 Sound-F Co., Ltd. All rights reserved.
4
+ #
5
+ # Author:: Mamoru Yuo
6
+ #
7
+ # This file is part of DYI.
8
+ #
9
+ # DYI is free software: you can redistribute it and/or modify it
10
+ # under the terms of the GNU General Public License as published by
11
+ # the Free Software Foundation, either version 3 of the License, or
12
+ # (at your option) any later version.
13
+ #
14
+ # DYI is distributed in the hope that it will be useful,
15
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
16
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
+ # GNU General Public License for more details.
18
+ #
19
+ # You should have received a copy of the GNU General Public License
20
+ # along with DYI. If not, see <http://www.gnu.org/licenses/>.
21
+
22
+ module DYI #:nodoc:
23
+ module Formatter #:nodoc:
24
+
25
+ class XamlFormatter < XmlFormatter
26
+
27
+ FONT_STYLE = {'normal'=>'Normal','italic'=>'Italic'}
28
+ FONT_WEIGHT = {'normal'=>'Normal','bold'=>'Bold','100'=>'Thin','200'=>'ExtraLight','300'=>'Light','400'=>'Normal','500'=>'Medium','600'=>'SemiBold','700'=>'Bold','800'=>'ExtraBold','900'=>'Black'}
29
+ FONT_STRETCH = {'normal'=>'Normal','ultra-condensed'=>'UltraCondensed','extra-condensed'=>'ExtraCondensed','condensed'=>'Condensed','semi-condensed'=>'SemiCondensed','semi-expanded'=>'SemiExpanded','expanded'=>'Expanded','extra-expanded'=>'ExtraExpanded','ultra-expanded'=>'UltraExpanded'}
30
+ STROKE_LINE_CAP = {'butt'=>'Flat','round'=>'Round','square'=>'Square'}
31
+ STROKE_LINE_JOIN = {'miter'=>'Miter','round'=>'Round','bevel'=>'Bevel'}
32
+ TEXT_ANCHOR = {'start'=>'Left','middle'=>'Center','end'=>'Right'}
33
+ SPREAD_METHOD = {'pad'=>'Pad','reflect'=>'Reflect','repeat'=>'Repeat'}
34
+
35
+ def puts(io=$>)
36
+ StringFormat.set_default_formats(:coordinate => 'x,y') {
37
+ super
38
+ }
39
+ end
40
+
41
+ def write_canvas(canvas, io)
42
+ create_node(io, 'UserControl',
43
+ :xmlns => "http://schemas.microsoft.com/winfx/2006/xaml/presentation",
44
+ :"xmlns:x" => "http://schemas.microsoft.com/winfx/2006/xaml",
45
+ # :"xmlns:navigation" => "clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Navigation",
46
+ # :"xmlns:d" => "http://schemas.microsoft.com/expression/blend/2008",
47
+ # :"xmlns:mc" => "http://schemas.openxmlformats.org/markup-compatibility/2006",
48
+ :Width => canvas.width,
49
+ :Height => canvas.height) {
50
+ create_node(io, 'Canvas'){
51
+ canvas.child_elements.each do |element|
52
+ element.write_as(self, io)
53
+ end
54
+ }
55
+ }
56
+ end
57
+
58
+ def write_rectangle(shape, io)
59
+ attrs, attr_creator = common_attributes(shape, :shape)
60
+ attrs.merge!(:"Canvas.Left"=>shape.left, :"Canvas.Top"=>shape.top, :Width=>shape.width, :Height=>shape.height)
61
+ attrs[:RadiusX] = shape.attributes[:rx] if shape.attributes[:rx]
62
+ attrs[:RadiusY] = shape.attributes[:ry] if shape.attributes[:ry]
63
+ if attr_creator
64
+ create_node(io, 'Rectangle', attrs) {
65
+ attr_creator.call(io, 'Rectangle')
66
+ }
67
+ else
68
+ create_leaf_node(io, 'Rectangle', attrs)
69
+ end
70
+ end
71
+
72
+ def write_circle(shape, io)
73
+ attrs, attr_creator = common_attributes(shape, :shape)
74
+ attrs.merge!(:"Canvas.Left"=>shape.center.x - shape.radius, :"Canvas.Top"=>shape.center.y - shape.radius, :Width=>shape.radius * 2, :Height=>shape.radius * 2)
75
+ if attr_creator
76
+ create_node(io, 'Ellipse', attrs) {
77
+ attr_creator.call(io, 'Ellipse')
78
+ }
79
+ else
80
+ create_leaf_node(io, 'Ellipse', attrs)
81
+ end
82
+ end
83
+
84
+ def write_ellipse(shape, io)
85
+ attrs, attr_creator = common_attributes(shape, :shape)
86
+ attrs.merge!(:"Canvas.Left"=>shape.center.x - shape.radius, :"Canvas.Top"=>shape.center.y - shape.radius, :Width=>shape.radius_x * 2, :Height=>shape.radius_y * 2)
87
+ if attr_creator
88
+ create_node(io, 'Ellipse', attrs) {
89
+ attr_creator.call(io, 'Ellipse')
90
+ }
91
+ else
92
+ create_leaf_node(io, 'Ellipse', attrs)
93
+ end
94
+ end
95
+
96
+ def write_line(shape, io)
97
+ attrs, attr_creator = common_attributes(shape, :line)
98
+ attrs.merge!(:X1 => shape.start_point.x, :Y1 => shape.start_point.y, :X2 => shape.end_point.x, :Y2 => shape.end_point.y)
99
+ if attr_creator
100
+ create_node(io, 'Line', attrs) {
101
+ attr_creator.call(io, 'Line')
102
+ }
103
+ else
104
+ create_leaf_node(io, 'Line', attrs)
105
+ end
106
+ end
107
+
108
+ def write_polyline(shape, io)
109
+ attrs, attr_creator = common_attributes(shape, :line)
110
+ attrs.merge!(:Points => shape.points.join(' '))
111
+ if attr_creator
112
+ create_node(io, 'Polyline', attrs) {
113
+ attr_creator.call(io, 'Polyline')
114
+ }
115
+ else
116
+ create_leaf_node(io, 'Polyline', attrs)
117
+ end
118
+ end
119
+
120
+ def write_polygon(shape, io)
121
+ attrs, attr_creator = common_attributes(shape, :shape)
122
+ attrs.merge!(:Points => shape.points.join(' '))
123
+ if attr_creator
124
+ create_node(io, 'Polygon', attrs) {
125
+ attr_creator.call(io, 'Polygon')
126
+ }
127
+ else
128
+ create_leaf_node(io, 'Polygon', attrs)
129
+ end
130
+ end
131
+
132
+ def write_path(shape, io)
133
+ attrs, attr_creator = common_attributes(shape, :line)
134
+ attrs.merge!(:Data => shape.concise_path_data)
135
+ if attr_creator
136
+ create_node(io, 'Path', attrs) {
137
+ attr_creator.call(io, 'Path')
138
+ }
139
+ else
140
+ create_leaf_node(io, 'Path', attrs)
141
+ end
142
+ end
143
+
144
+ def write_text(shape, io)
145
+ attrs, attr_creator = common_attributes(shape, :text)
146
+ attrs.merge!(:"Canvas.Left" => shape.point.x, :"Canvas.Top" => shape.point.y)
147
+ # attrs[:"text-decoration"] = shape.attributes[:text_decoration] if shape.attributes[:text_decoration]
148
+ case shape.attributes[:alignment_baseline]
149
+ when nil then attrs[:"Canvas.Top"] -= shape.font_height * 0.85
150
+ when 'middle' then attrs[:"Canvas.Top"] -= shape.font_height * 0.5
151
+ when 'bottom' then attrs[:"Canvas.Top"] -= shape.font_height
152
+ end
153
+ case text_anchor = TEXT_ANCHOR[shape.attributes[:text_anchor]]
154
+ when 'Left'
155
+ attrs[:TextAlignment] = text_anchor
156
+ when 'Center'
157
+ attrs[:Width] = @canvas.width
158
+ attrs[:"Canvas.Left"] -= @canvas.width.quo(2)
159
+ attrs[:TextAlignment] = text_anchor
160
+ when 'Right'
161
+ attrs[:Width] = @canvas.width
162
+ attrs[:"Canvas.Left"] -= @canvas.width
163
+ attrs[:TextAlignment] = text_anchor
164
+ end
165
+ # attrs[:"writing-mode"] = shape.attributes[:writing_mode] if shape.attributes[:writing_mode]
166
+ text = shape.formated_text
167
+ if text =~ /(\r\n|\n|\r)/
168
+ attrs[:Text] = $`.strip
169
+ create_node(io, 'TextBlock', attrs) {
170
+ attr_creator.call(io, 'TextBlock') if attr_creator
171
+ create_leaf_node(io, 'LineBreak')
172
+ $'.each_line do |line|
173
+ create_leaf_node(io, 'Run', line.strip)
174
+ end
175
+ }
176
+ else
177
+ attrs[:Text] = text
178
+ if attr_creator
179
+ create_node(io, 'TextBlock', attrs, &attr_creator)
180
+ else
181
+ create_leaf_node(io, 'TextBlock', attrs)
182
+ end
183
+ end
184
+ end
185
+
186
+ def write_group(shape, io)
187
+ attrs, attr_creator = common_attributes(shape)
188
+ create_node(io, 'Canvas', attrs) {
189
+ attr_creator.call(io, 'Canvas') if attr_creator
190
+ create_node(io, 'Canvas.RenderTransform') {
191
+ create_transform_node(shape, io)
192
+ } unless shape.transform.empty?
193
+ shape.child_elements.each do |element|
194
+ element.write_as(self, io)
195
+ end
196
+ }
197
+ end
198
+
199
+ def write_linear_gradient(shape, io)
200
+ attr = {
201
+ :StartPoint => "#{shape.start_point[0]},#{shape.start_point[1]}",
202
+ :EndPoint => "#{shape.stop_point[0]},#{shape.stop_point[1]}"}
203
+ if spread_method = SPREAD_METHOD[shape.spread_method]
204
+ attr[:SpreadMethod] = spread_method
205
+ end
206
+ create_node(io, 'LinearGradientBrush', attr) {
207
+ shape.child_elements.each do |element|
208
+ element.write_as(self, io)
209
+ end
210
+ }
211
+ end
212
+
213
+ def write_gradient_stop(shape, io)
214
+ attrs = {:Offset=>shape.offset}
215
+ attrs[:Color] = shape.color.to_s16(shape.opacity) if shape.color
216
+ create_leaf_node(io, 'GradientStop', attrs)
217
+ end
218
+
219
+ def write_clip(shape, io)
220
+ # TODO
221
+ end
222
+
223
+ private
224
+
225
+ def common_attributes(shape, type=nil) # :nodoc:
226
+ attributes = {}
227
+ font = create_font_attr(shape)
228
+ painting, attr_creator = create_painting_attr(shape, type)
229
+ attributes.merge!(font) unless font.empty?
230
+ attributes.merge!(painting) unless painting.empty?
231
+ [attributes, attr_creator]
232
+ end
233
+
234
+ def create_font_attr(shape) # :nodoc:
235
+ attr = {}
236
+ if shape.respond_to?(:font) && (font = shape.font) && !font.empty?
237
+ attr[:FontFamily] = font.font_family if font.font_family
238
+ if font_style = FONT_STYLE[font.style]
239
+ attr[:FontStyle] = font_style
240
+ end
241
+ if font_weight = FONT_WEIGHT[font.weight]
242
+ attr[:FontWeight] = font_weight
243
+ end
244
+ if font_stretch = FONT_STRETCH[font.stretch]
245
+ attr[:FontStretch] = font_stretch
246
+ end
247
+ end
248
+ attr[:FontSize] = shape.font_height.to_user_unit if shape.respond_to?(:font_height)
249
+ attr
250
+ end
251
+
252
+ def create_painting_attr(shape, type) # :nodoc:
253
+ attr = {}
254
+ attr_creator = nil
255
+ if shape.respond_to?(:painting) && (painting = shape.painting) && !painting.empty?
256
+ if painting.fill
257
+ if painting.fill.respond_to?(:write_as)
258
+ case type
259
+ when :shape,:line
260
+ attr_creator = proc {|io, tag_name|
261
+ create_node(io, "#{tag_name}.Fill") {
262
+ painting.fill.write_as(self, io)
263
+ }
264
+ }
265
+ when :text
266
+ attr_creator = proc {|io, tag_name|
267
+ create_node(io, "#{tag_name}.Foreground") {
268
+ painting.fill.write_as(self, io)
269
+ }
270
+ }
271
+ end
272
+ else
273
+ case type
274
+ when :shape,:line then attr[:Fill] = painting.fill.to_s16(painting.fill_opacity)
275
+ when :text then attr[:Foreground] = painting.fill.to_s16(painting.fill_opacity)
276
+ end
277
+ end
278
+ end
279
+ attr[:Stroke] = painting.stroke.to_s16(painting.stroke_opacity) if painting.stroke
280
+ attr[:StrokeDashArray] = painting.stroke_dasharray.join(',') if painting.stroke_dasharray
281
+ attr[:StrokeDashOffset] = painting.stroke_dashoffset.to_user_unit if painting.stroke_dashoffset
282
+ if type == :line && linecap = STROKE_LINE_CAP[painting.stroke_linecap]
283
+ attr[:StrokeStartLineCap] = linecap
284
+ attr[:StrokeEndLineCap] = linecap
285
+ end
286
+ if linejoin = STROKE_LINE_JOIN[painting.stroke_linejoin]
287
+ attr[:StrokeLineJoin] = linejoin
288
+ end
289
+ attr[:StrokeMitterLimit] = painting.stroke_miterlimit if painting.stroke_miterlimit
290
+ if painting.stroke_width
291
+ attr[:StrokeThickness] = painting.stroke_width.to_user_unit
292
+ end
293
+ end
294
+ [attr, attr_creator]
295
+ end
296
+
297
+ def create_transform_node(shape, io) # :nodoc:
298
+ create_node(io, 'TransformGroup') {
299
+ shape.transform.each do |tr|
300
+ case tr.first
301
+ when :translate
302
+ create_leaf_node(io, 'TranslateTransform', :X=>tr[1], :Y=>tr[2])
303
+ when :scale
304
+ create_leaf_node(io, 'ScaleTransform', :ScaleX=>tr[1], :ScaleY=>tr[2])
305
+ when :rotate
306
+ create_leaf_node(io, 'RotateTransform', :Angle=>tr[1])
307
+ when :skewX
308
+ create_leaf_node(io, 'ScaleTransform', :AngleX=>tr[1])
309
+ when :skewY
310
+ create_leaf_node(io, 'ScaleTransform', :AngleY=>tr[1])
311
+ end
312
+ end
313
+ }
314
+ end
315
+ end
316
+ end
317
+ end
@@ -0,0 +1,399 @@
1
+ # -*- encoding: UTF-8 -*-
2
+
3
+ # Copyright (c) 2009-2011 Sound-F Co., Ltd. All rights reserved.
4
+ #
5
+ # Author:: Mamoru Yuo
6
+ # Documentation:: Mamoru Yuo
7
+ #
8
+ # This file is part of DYI.
9
+ #
10
+ # DYI is free software: you can redistribute it and/or modify it
11
+ # under the terms of the GNU General Public License as published by
12
+ # the Free Software Foundation, either version 3 of the License, or
13
+ # (at your option) any later version.
14
+ #
15
+ # DYI is distributed in the hope that it will be useful,
16
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
17
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18
+ # GNU General Public License for more details.
19
+ #
20
+ # You should have received a copy of the GNU General Public License
21
+ # along with DYI. If not, see <http://www.gnu.org/licenses/>.
22
+ #
23
+ # == Overview
24
+ #
25
+ # This file provides the DYI::Length class, which provides length
26
+ # supports for DYI scripts. The length is a distance measurement.
27
+ #
28
+ # See the documentation to the DYI::Length class for more details and
29
+ # examples of usage.
30
+ #
31
+
32
+ module DYI #:nodoc:
33
+
34
+ # Class representing a length. See documentation for the file
35
+ # dyi/length.rb for an overview.
36
+ #
37
+ # == Introduction
38
+ #
39
+ # This class works with an amount and a unit. The lists of unit identifiers
40
+ # matches the list of unit identifiers in CSS: em, ex, px, pt, pc, cm, mm, in
41
+ # and percentages(%). When a unit is not given, then the length is assumed
42
+ # to be in user units (i.e., a value in the current user coordinate sytem).
43
+ #
44
+ # * As in CSS, the _em_ and _ex_ unit identifiers are relative to the current
45
+ # font's font-size and x-height, respectively.
46
+ # * One _px_ unit is defined to be equal to one user unit.
47
+ # * 1<em>pt</em> equals 1.25 user units.
48
+ # * 1<em>pc</em> equals 15 user units.
49
+ # * 1<em>mm</em> equals 3.543307 user units.
50
+ # * 1<em>cm</em> equals 35.43307 user units.
51
+ # * 1<em>in</em> equals 90 user units.
52
+ # * For percentage values that are defined to be relative to the size of
53
+ # parent element.
54
+ #
55
+ # == Ways of comparing and calculating
56
+ #
57
+ # This class include +Comparable+ module, therefore you can use the
58
+ # comparative operators. In the comparison between DYI::Length
59
+ # objects, the unit of each objects are arranged and it does. The equality
60
+ # operator '<tt>==</tt>' does not test equality instance but test equality
61
+ # value.
62
+ #
63
+ # This class suports following arithmetic operators and methods: <tt>+</tt>,
64
+ # <tt>-</tt>, <tt>*</tt>, <tt>/</tt>, <tt>%</tt>, <tt>**</tt>, +#div+, +#quo+,
65
+ # +#modulo+. The operators '<tt>+</tt>', '<tt>-</tt>' coerced right hand
66
+ # operand into Length, and then calculate.
67
+ #
68
+ # See the documentation to each operators and methods class for details.
69
+ #
70
+ # == Examples of use
71
+ #
72
+ # length1 = DYI::Length.new(10) # 10 user unit (equals to 10px).
73
+ # length2 = DYI::Length.new('10') # it is 10px too.
74
+ # DYI::Length.new('1in') > DYI::Length.new(50)
75
+ # # => true, 1in equals 90px.
76
+ # DYI::Length.new('1in') > DYI::Length.new('2em')
77
+ # # => Error, 'em' is not comparable unit.
78
+ # DYI::Length.new('10cm') == DYI::Length.new('100mm')
79
+ # # => true
80
+ class Length
81
+ include Comparable
82
+
83
+ # Array of unit that can be used.
84
+ UNITS = ['px', 'pt', '%', 'cm', 'mm', 'in', 'em', 'ex', 'pc']
85
+
86
+ @@units = {'px'=>1.0,'pt'=>1.25,'cm'=>35.43307,'mm'=>3.543307,'in'=>90.0,'pc'=>15.0}
87
+ @@default_format = '0.###U'
88
+
89
+ # Returns a new DYI::Length object.
90
+ #
91
+ # +length+ is instance of +Numeric+, +String+, or
92
+ # <tt>DYI::Length</tt> class.
93
+ def initialize(length)
94
+ case length
95
+ when Length
96
+ @value = length._num
97
+ @unit = length._unit
98
+ when Numeric
99
+ @value = length
100
+ @unit = nil
101
+ when String
102
+ unless /^\s*(-?[\d]*(?:\d\.|\.\d|\d)[0\d]*)(#{UNITS.join('|')})?\s*$/ =~ length
103
+ raise ArgumentError, "`#{length}' is string that could not be understand"
104
+ end
105
+ __value, __unit = $1, $2
106
+ @value = __value.include?('.') ? __value.to_f : __value.to_i
107
+ @unit = (__unit == 'px' || @value == 0) ? nil : __unit
108
+ else
109
+ raise TypeError, "#{length.class} can't be coerced into Length"
110
+ end
111
+ end
112
+
113
+ # This constant is zoro length.
114
+ ZERO = new(0)
115
+
116
+ # Returns the receiver's value.
117
+ def +@
118
+ self
119
+ end
120
+
121
+ # Returns the receiver's value, negated.
122
+ def -@
123
+ new_length(-@value)
124
+ end
125
+
126
+ # Returns a new length which is the sum of the receiver and +other+.
127
+ def +(other)
128
+ other = self.class.new(other)
129
+ if @unit == other._unit
130
+ new_length(@value + other._num)
131
+ else
132
+ self.class.new(to_f + other.to_f)
133
+ end
134
+ end
135
+
136
+ # Returns a new length which is the difference of the receiver and +other+.
137
+ def -(other)
138
+ other = self.class.new(other)
139
+ if @unit == other._unit
140
+ new_length(@value - other._num)
141
+ else
142
+ self.class.new(to_f - other.to_f)
143
+ end
144
+ end
145
+
146
+ # Returns a new muliplicative length of the receiver by +number+.
147
+ def *(number)
148
+ new_length(@value * number)
149
+ end
150
+
151
+ # Raises a length the number power.
152
+ def **(number)
153
+ new_length(@value ** number)
154
+ end
155
+
156
+ # Returns a new length which is the result of dividing the receiver by
157
+ # +other+.
158
+ def div(other)
159
+ case other
160
+ when Length
161
+ if @unit == other.unit
162
+ @value.div(other._num)
163
+ else
164
+ to_f.div(other.to_f)
165
+ end
166
+ else
167
+ raise TypeError, "#{other.class} can't be coerced into Length"
168
+ end
169
+ end
170
+
171
+ # Return a new length which is the modulo after division of the receiver by
172
+ # +other+.
173
+ def % (other)
174
+ case other
175
+ when Length
176
+ if @unit == other.unit
177
+ new_length(@value % other._num)
178
+ else
179
+ self.class.new(to_f % other.to_f)
180
+ end
181
+ else
182
+ raise TypeError, "#{other.class} can't be coerced into Length"
183
+ end
184
+ end
185
+
186
+ # If argument +number+ is a numeric, returns a new divisional length of the
187
+ # receiver by +other+. If argument +number+ is a length, returns a divisional
188
+ # float of the receiver by +other+.
189
+ #
190
+ # DYI::Length.new(10) / 4 # => 2.5px
191
+ # DYI::Length.new(10) / DYI::Length.new(4) # => 2.5
192
+ def /(number)
193
+ case number
194
+ when Numeric
195
+ new_length(@value.quo(number.to_f))
196
+ when Length
197
+ if @unit == number.unit
198
+ @value.quo(number._num.to_f)
199
+ else
200
+ to_f.quo(number.to_f)
201
+ end
202
+ else
203
+ raise TypeError, "#{number.class} can't be coerced into Numeric or Length"
204
+ end
205
+ end
206
+
207
+ alias quo /
208
+ alias modulo %
209
+
210
+ def clone #:nodoc:
211
+ raise TypeError, "allocator undefined for Length"
212
+ end
213
+
214
+ def dup #:nodoc:
215
+ raise TypeError, "allocator undefined for Length"
216
+ end
217
+
218
+ # Returns +true+ if the receiver has a zero length, +false+ otherwise.
219
+ def zero?
220
+ @value == 0
221
+ end
222
+
223
+ # Returns +self+ if the receiver is not zero, +nil+ otherwise.
224
+ def nonzero?
225
+ @value == 0 ? nil : self
226
+ end
227
+
228
+ # Returns the absolute length of the receiver.
229
+ def abs
230
+ @value >= 0 ? self : -self
231
+ end
232
+
233
+ # Returns +-1+, +0+, <tt>+1</tt> or +nil+ depending on whether the receiver
234
+ # is less than, equal to, greater than real or not comparable.
235
+ def <=>(other)
236
+ return nil unless other.kind_of?(Length)
237
+ if @unit == other._unit
238
+ @value <=> other._num
239
+ else
240
+ to_f <=> other.to_f rescue nil
241
+ end
242
+ end
243
+
244
+ # Returns the receiver's unit. If receiver has no unit, returns 'px'.
245
+ def unit
246
+ @unit.nil? ? 'px' : @unit
247
+ end
248
+
249
+ # :call-seq:
250
+ # step (limit, step) {|length| ...}
251
+ # step (limit, step)
252
+ #
253
+ # Invokes block with the sequence of length starting at receiver,
254
+ # incremented by +step+ on each call. The loop finishes when +length+ to be
255
+ # passed to the block is greater than +limit+ (if +step+ is positive) or
256
+ # less than +limit+ (if +step+ is negative).
257
+ #
258
+ # If no block is given, an enumerator is returned instead.
259
+ def step(limit, step)
260
+ if @unit == limit._unit && @unit == step._unit
261
+ self_value, limit_value, step_value = @value, limit._num, step._num
262
+ else
263
+ self_value, limit_value, step_value = to_f, limit.to_f, step.to_f
264
+ end
265
+ enum = Enumerator.new {|y|
266
+ self_value.step(limit_value, step_value) do |value|
267
+ self.new_length(value)
268
+ end
269
+ }
270
+ if block_given?
271
+ enum.each(&proc)
272
+ self
273
+ else
274
+ enum
275
+ end
276
+ end
277
+
278
+ # Returns a new length that converted into length of user unit.
279
+ def to_user_unit
280
+ @unit ? self.class.new(to_f) : self
281
+ end
282
+
283
+ # Returns a string representing obj.
284
+ #
285
+ # Format string can be specified for the argument. If no argument is given,
286
+ # +default_format+ of this class is used as format string. About format
287
+ # string, see the documentation to +default_format+ method.
288
+ def to_s(format=nil)
289
+ fmts = (format || @@default_format).split('\\\\')
290
+ fmts = fmts.map do |fmt|
291
+ fmt.gsub(/(?!\\U)(.|\G)U/, '\\1' + @unit.to_s).gsub(/(?!\\u)(.|\G)u/, '\\1' + unit)
292
+ end
293
+ @value.strfnum(fmts.join('\\\\'))
294
+ end
295
+
296
+ # Returns amount part of a length converted into given unit as float. If
297
+ # parameter +unit+ is given, converts into user unit.
298
+ def to_f(unit=nil)
299
+ unless self_ratio = @unit ? @@units[@unit] : 1.0
300
+ raise RuntimeError, "unit `#{@unit}' can not convert into user unit"
301
+ end
302
+ unless param_ratio = unit ? @@units[unit] : 1.0
303
+ if UNITS.include?(unit)
304
+ raise RuntimeError, "unit `#{@unit}' can not convert into user unit"
305
+ else
306
+ raise ArgumentError, "unit `#{@unit}' is unknown unit"
307
+ end
308
+ end
309
+ (@value * self_ratio.quo(param_ratio)).to_f
310
+ end
311
+
312
+ def inspect #:nodoc:
313
+ @value.to_s + @unit.to_s
314
+ end
315
+
316
+ protected
317
+
318
+ def _num #:nodoc:
319
+ @value
320
+ end
321
+
322
+ def _unit #:nodoc:
323
+ @unit
324
+ end
325
+
326
+ private
327
+
328
+ def new_length(value) #:nodoc:
329
+ other = self.class.allocate
330
+ other.instance_variable_set(:@value, value)
331
+ other.instance_variable_set(:@unit, @unit)
332
+ other
333
+ end
334
+
335
+ class << self
336
+
337
+ public
338
+
339
+ def new(*args) #:nodoc:
340
+ return args.first if args.size == 1 && args.first.instance_of?(self)
341
+ super
342
+ end
343
+
344
+ # Returns new instance as +new+ method when an argument is not +nil+.
345
+ # If an argument is +nil+, returns +nil+.
346
+ def new_or_nil(*args)
347
+ (args.size == 1 && args.first.nil?) ? nil : new(*args)
348
+ end
349
+
350
+ # Returns a coefficient that is used for conversion from any unit into
351
+ # user unit.
352
+ def unit_ratio(unit)
353
+ raise ArgumentError, "unit `#{unit}' can not convert other unit" unless ratio = @@units[unit.to_s]
354
+ ratio
355
+ end
356
+
357
+ # :call-seq:
358
+ # set_default_format (format) {...}
359
+ # set_default_format (format)
360
+ #
361
+ # Invokes block with given +format+ string as default format. After
362
+ # invokes block, formar format is used.
363
+ #
364
+ # If no block is given, sets default format setring as +default_format=+
365
+ # method.
366
+ def set_default_format(format)
367
+ if block_given?
368
+ org_format = @@default_format
369
+ self.default_format = format
370
+ yield
371
+ @@default_format = org_format
372
+ self
373
+ else
374
+ self.default_format = format
375
+ end
376
+ end
377
+
378
+ # Returns format that is used when called to_s. See the documentation to
379
+ # +default_format=+ method too.
380
+ def default_format
381
+ @@default_format
382
+ end
383
+
384
+ # Sets format that is used when called to_s.
385
+ #
386
+ # The format string is same as <tt>Numeric#strfnum</tt> format. See the
387
+ # documentation to <tt>Numeric#strfnum</tt> method. In addition to
388
+ # place-holder of +strfnum+, following placeholder can be used.
389
+ #
390
+ # +u+:: (unit placeholder) Placeholder '+u+' is replaced as a unit. If
391
+ # the unit is user unit, '+u+' is repleced as 'px'.
392
+ # +U+:: (unit placeholder) Placeholder '+U+' is replaced as a unit. If
393
+ # the unit is user unit, '+U+' is replece as empty string.
394
+ def default_format=(fromat)
395
+ @@default_format = fromat.dup
396
+ end
397
+ end
398
+ end
399
+ end