C0 code coverage information
Generated on Mon Oct 30 22:55:21 CET 2006 with rcov 0.7.0
Code reported as executed by Ruby looks like this...
and this: this line is also marked as covered.
Lines considered as run by rcov, but not reported by Ruby, look like this,
and this: these lines were inferred by rcov (using simple heuristics).
Finally, here's a line marked as not executed.
Name |
Total lines |
Lines of code |
Total coverage |
Code coverage |
lib/xspf.rb
|
480
|
321
|
|
|
1 #--
2 # =============================================================================
3 # Copyright (c) 2006 Pau Garcia i Quiles (pgquiles@elpauer.org)
4 # All rights reserved.
5 #
6 # This library may be used only as allowed by either the Ruby license (or, by
7 # association with the Ruby license, the GPL). See the "doc" subdirectory of
8 # the XSPF distribution for the texts of these licenses.
9 # -----------------------------------------------------------------------------
10 # XSPF for Ruby website : http://xspf.rubyforge.org
11 # =============================================================================
12 #++
13
14 require 'rexml/document'
15 require 'xml/xslt'
16
17 # :include: USAGE
18 # :main: USAGE
19
20 module MetaGen #:nodoc:
21
22 # define the method
23 def self.add_method(klass, meth_name, body, meth_rdoc)
24 code = <<-CODE
25 # #{meth_rdoc}
26 def #{meth_name.downcase}
27 @#{meth_name}
28 end
29
30 def #{meth_name.downcase}=(value)
31 @#{meth_name.downcase} = value
32 end
33
34 private
35 def parse_#{meth_name.downcase}
36 begin
37 #{body}
38 rescue NoMethodError
39 return nil
40 end
41 end
42 CODE
43
44 klass.module_eval(code)
45
46 # hook to write klass + name attrib to a file
47 if $META_RDOC
48 open($META_RDOC, 'a+') do |f|
49 f.puts("class #{klass}\n #{code}\n end")
50 end
51 end
52
53 end
54
55 # output in different formats
56 # FIXME Only works in parse mode, not in generation mode.
57 def self.add_output_format(klass, format, meth_rdoc)
58 xslt_path = "'#{File.join( File.dirname(__FILE__), %Q{xspf2#{format}.xsl} )}'"
59 code = <<-CODE
60 # #{meth_rdoc}
61 def to_#{format}
62 xslt = XML::XSLT.new
63 xslt.xml = self.to_xml
64 xslt.xsl = REXML::Document.new( File.new( #{xslt_path} ) )
65 xslt.serve
66 end
67 CODE
68
69 klass.module_eval(code)
70
71 if $META_RDOC
72 open($META_RDOC, 'a+') do |f|
73 f.puts("class #{klass}\n #{code}\n end")
74 end
75 end
76
77 end
78
79 end
80
81 class XSPF
82
83 attr_reader :xspf
84
85 #:stopdoc:
86 ATTRIBUTES = %w{ version encoding }
87 VERSION_RDOC = 'Version for the XML document or _nil_ if not defined'
88 ENCODING_RDOC = 'Encoding of the XML document or _nil_ if not defined'
89
90 OUTPUT_FORMATS = %w{ m3u html smil soundblox }
91 M3U_RDOC = 'Creates a .m3u playlist from the XSPF document. This method makes use of the official XSPF to M3U XSLT transformation by Lucas Gonze.'
92 HTML_RDOC = 'Outputs the playlist as an HTML page. This method makes use of the official XSPF to HTML XSLT transformation by Lucas Gonze.'
93 SMIL_RDOC = 'Creates a .smil playlist from the XSPF document. This method makes use of the official XSPF to SMIL XSLT transformation by Lucas Gonze.'
94 SOUNDBLOX_RDOC = 'Creates a SoundBlox playlist from the XSPF document. This method makes use of the official XSPF to SoundBlox XSLT tranformation by Lucas Gonze.'
95
96 ATTRIBUTES.each do |attrib|
97 MetaGen.add_method(self, attrib, "@xspf.#{attrib}", eval(attrib.upcase + '_RDOC').to_s )
98 end
99
100 OUTPUT_FORMATS.each do |format|
101 MetaGen.add_output_format(self, format, eval(format.upcase + '_RDOC').to_s )
102 end
103
104 #:startdoc:
105
106 # Creates a XSPF object from a file or string (parse mode) or from a hash or nil (generation mode).
107 #
108 # Possible keys in the hash: :version, :encoding
109 def initialize(source = nil)
110 if ( source.nil? || source.instance_of?(Hash) ) then
111 @version = if source.nil? || !source.has_key?(:version)
112 '1.0'
113 else
114 source[:version]
115 end
116 @encoding = if source.nil? || !source.has_key?(:encoding)
117 'UTF-8'
118 else
119 source[:encoding]
120 end
121 @playlist = nil
122 @playlist = if !source.nil? && source.has_key?(:playlist) then
123 if source[:playlist].instance_of?(XSPF::Playlist)
124 source[:playlist]
125 else
126 raise(TypeError, 'You must pass a file/string (parsing mode) or a hash/nothing (generator mode) as argument to XSPF#new')
127 end
128 end
129
130 elsif ( source.instance_of?(File) || source.instance_of?(String) ) then
131 @xspf = REXML::Document.new(source)
132 ATTRIBUTES.each do |attrib|
133 eval('@' + attrib + '= parse_' + attrib)
134 end
135
136 @playlist = XSPF::Playlist.new(self)
137
138 else
139 raise(TypeError, 'You must pass a file/string (parsing mode) or a hash/nothing (generator mode) as argument to XSPF#new')
140 end
141 end
142
143 # A XSPF::Playlist object
144 def playlist
145 @playlist
146 end
147
148 def playlist=(value)
149 raise(TypeError, 'The playlist must be an instance of XSPF::Playlist') unless value.instance_of?(XSPF::Playlist)
150 @playlist = value
151 end
152
153 # Exports the XSPF object to XML
154 def to_xml
155 xml = REXML::Document.new
156 xml << REXML::XMLDecl.new(@version, @encoding)
157 xml << REXML::Document.new(@playlist.to_xml) unless @playlist.nil?
158 xml.to_s
159 end
160
161 # The <playlist> section of the XSPF document (outputs XML code). This method is only used while parsing.
162 protected
163 def playlist_xml
164 @xspf.root
165 end
166
167 end
168
169 class XSPF::Playlist < XSPF
170
171 attr_reader :playlist
172
173 #:stopdoc:
174 ATTRIBUTES = %w{ xmlns version }
175 ELEMENTS = %w{ title creator annotation info location identifier image date license attribution extension }
176 ATTRIBUTE_AND_ELEMENT = %w{ link meta }
177 ATTRIBUTION_CHILD_ELEMENTS = %w{ location identifier }
178 EXTENSION_CHILD_ELEMENTS = %w{ application content }
179
180 XMLNS_RDOC = 'The XML namespace. It must be http://xspf.org/ns/0/ for a valid XSPF document.'
181 XMLNS_DEFAULT = 'http://xspf.org/ns/0/'
182 VERSION_RDOC = 'The XSPF version. It may be 0 or 1, although 1 is strongly advised.'
183 VERSION_DEFAULT = '1'
184 TITLE_RDOC = 'A human-readable title for the playlist. xspf:playlist elements MAY contain exactly one.'
185 CREATOR_RDOC = 'Human-readable name of the entity (author, authors, group, company, etc) that authored the playlist. XSPF::Playlist objects MAY contain exactly one.'
186 ANNOTATION_RDOC = 'A human-readable comment on the playlist. This is character data, not HTML, and it may not contain markup. XSPF::Playlist objects elements MAY contain exactly one.'
187 INFO_RDOC = 'URL of a web page to find out more about this playlist. Likely to be homepage of the author, and would be used to find out more about the author and to find more playlists by the author. XSPF::Playlist objects MAY contain exactly one.'
188 LOCATION_RDOC = 'Source URL for this playlist. XSPF::Playlist objects MAY contain exactly one.'
189 IDENTIFIER_RDOC = 'Canonical ID for this playlist. Likely to be a hash or other location-independent name. MUST be a legal URN. XSPF::Playlist objects MAY contain exactly one.'
190 IMAGE_RDOC = 'URL of an image to display if XSPF::Playlist#image return nil. XSPF::Playlist objects MAY contain exactly one.'
191 DATE_RDOC = 'Creation date (not last-modified date) of the playlist, formatted as a XML schema dateTime. XSPF::Playlist objects MAY contain exactly one.'
192 LICENSE_RDOC = 'URL of a resource that describes the license under which this playlist was released. XSPF::Playlist objects MAY contain zero or one license element.'
193 ATTRIBUTION_RDOC = 'An ordered list of URIs. The purpose is to satisfy licenses allowing modification but requiring attribution. If you modify such a playlist, move its XSPF::Playlist#location or XSPF::Playlist#identifier element to the top of the items in the XSPF::Playlist#attribution element. XSPF::Playlist objects MAY contain exactly one attribution element. Please note that currently XSPF for Ruby does not parse the contents of XSPF::Playlist#attribution.'
194 EXTENSION_RDOC = 'The extension element allows non-XSPF XML to be included in XSPF documents without breaking XSPF validation. The purpose is to allow nested XML, which the meta and link elements do not. XSPF::Playlist objects MAY contain zero or more extension elements but currently XSPF for Ruby returns only the first one.'
195 LINK_REL_RDOC = 'The link element allows non-XSPF web resources to be included in XSPF documents without breaking XSPF validation. A valid _link_ element has a _rel_ attribute and a _content_ element, obtained with XSPF::Playlist#link_rel and XSPF::Playlist#link_content respectively. XSPF::Playlist objects MAY contain zero or more link elements, but currently XSPF for Ruby returns only the first one.'
196 LINK_CONTENT_RDOC = 'The link element allows non-XSPF web resources to be included in XSPF documents without breaking XSPF validation. A valid _link_ element has a _rel_ attribute and a _content_ element, obtained with XSPF::Playlist#link_rel and XSPF::Playlist#link_content respectively. XSPF::Playlist objects MAY contain zero or more meta elements, but currently XSPF for Ruby returns only the first one.'
197 META_REL_RDOC = 'The meta element allows non-XSPF metadata to be included in XSPF documents without breaking XSPF validation. A valid _meta_ element has a _rel_ attribute and a _content_ element, obtained with XSPF::Playlist#meta_rel and XSPF::Playlist#meta_content respectively. XSPF::Playlist objects MAY contain zero or more meta elements, but currently XSPF for Ruby returns only the first one.'
198 META_CONTENT_RDOC = 'The meta element allows non-XSPF metadata to be included in XSPF documents without breaking XSPF validation. A valid _meta_ element has a _rel_ attribute and a _content_ element, obtained with XSPF::Playlist#meta_rel and XSPF::Playlist#meta_content respectively. XSPF::Playlist objects MAY contain zero or more meta elements, but currently XSPF for Ruby returns only the first one.'
199
200 # FIXME Currently we only return the first "link"
201 # FIXME Currently we only return the first "meta"
202 # FIXME Currently we only return the first "extension"
203 # TODO Parse "attribution"
204 # TODO Parse "extension"
205
206 # Returns the value of the attribute or nil if the attribute is not present
207 ATTRIBUTES.each do |attrib|
208 MetaGen.add_method( self, attrib, "@playlist.root.attributes['#{attrib}']", eval(attrib.upcase + '_RDOC').to_s )
209 end
210
211 ELEMENTS.each do |element|
212 MetaGen.add_method( self, element, "@playlist.elements['#{element}'].text", eval(element.upcase + '_RDOC').to_s )
213 end
214
215 ATTRIBUTE_AND_ELEMENT.each do |ae|
216 MetaGen.add_method( self, "#{ae}_content", "@playlist.elements['#{ae}'].text", eval(ae.upcase + '_CONTENT_RDOC').to_s )
217 MetaGen.add_method( self, "#{ae}_rel", "@playlist.elements['#{ae}'].attributes['rel']", eval(ae.upcase + '_REL_RDOC').to_s )
218 end
219
220 #:startdoc:
221
222 # Creates a XSPF::Playlist from a XSPF document (parse mode) or from a hash of values (generation mode)
223 #
224 # Possible keys in the hash: :xmlns, :version, :title, :creator, :annotation, :info, :location, :identifier, :image, :date, :license, :attribution, :extension, :link_rel, :link_content, :meta_rel, :meta_content
225 def initialize(source = nil)
226
227 if ( source.instance_of?(Hash) || source.nil? ) then
228
229 ATTRIBUTES.each do |attrib|
230 add_instance_variable(source, attrib)
231 end
232
233 ELEMENTS.each do |element|
234 add_instance_variable(source, element)
235 end
236
237 ATTRIBUTE_AND_ELEMENT.each do |ae|
238 add_instance_variable(source, "#{ae}_content" )
239 add_instance_variable(source, "#{ae}_rel" )
240 end
241
242 @tracklist = if ( !source.nil? && source.has_key?(:tracklist) && source[:tracklist].instance_of?(XSPF::Tracklist) )
243 source[:tracklist]
244 else
245 nil
246 end
247
248 elsif source.instance_of?(XSPF) then
249
250 @playlist = source.playlist_xml
251
252 ATTRIBUTES.each do |attrib|
253 eval('@' + attrib.downcase + '= parse_' + attrib.downcase)
254 end
255
256 ELEMENTS.each do |element|
257 eval('@' + element.downcase + '= parse_' + element.downcase)
258 end
259
260 ATTRIBUTE_AND_ELEMENT.each do |ae|
261 eval('@' + ae.downcase + '_content = parse_' + ae.downcase + '_content')
262 eval('@' + ae.downcase + '_rel = parse_' + ae.downcase + '_rel')
263 end
264
265 @tracklist = XSPF::Tracklist.new(self)
266
267 else
268 raise(TypeError, 'You must pass a XSPF object (parsing mode) or a hash (generator mode) as argument to XSPF::Playlist#new')
269 end
270
271 end
272
273 # A XSPF::Tracklist object
274 def tracklist
275 @tracklist
276 end
277
278 def tracklist=(value)
279 raise(TypeError, 'The tracklist must be an instance of XSPF::Tracklist') unless value.instance_of?(XSPF::Tracklist)
280 @tracklist = value
281 end
282
283 alias :<< :tracklist=
284
285 # Exports the XSPF::Playlist to XML (only the <playlist> section)
286 def to_xml
287
288 xml = REXML::Element.new('playlist')
289
290 ATTRIBUTES.each do |attrib|
291 # TODO Sure there is a nicer way to do evaluate this condition...
292 unless eval('@' + attrib.downcase + '.nil?')
293 xml.attributes[attrib] = eval('@' + attrib.downcase)
294 end
295 end
296
297 ELEMENTS.each do |element|
298 # TODO Sure there is a nicer way to do evaluate this condition...
299 unless eval('@' + element.downcase + '.nil?')
300 el = REXML::Element.new(element)
301 el.add_text( eval('@' + element.downcase) )
302 xml.add_element(el)
303 end
304 end
305
306 ATTRIBUTE_AND_ELEMENT.each do |ae|
307 # TODO Sure there is a nicer way to do evaluate this condition...
308 unless eval('@' + ae.downcase + '_rel.nil? && @'+ ae.downcase + '_content.nil?')
309 el = REXML::Element.new(ae.downcase)
310 el.add_attribute('rel', eval('@' + ae.downcase + '_rel') )
311 el.add_text( eval('@' + ae.downcase + '_content') )
312 xml.add_element(el)
313 end
314 end
315
316 xml << REXML::Document.new(@tracklist.to_xml)
317
318 xml.to_s
319
320 end
321
322 # The <trackList> section of the XSPF document (outputs XML code). This method is only used while parsing.
323 protected
324 def tracklist_xml
325 @playlist.elements['trackList']
326 end
327
328 private
329 def add_instance_variable(hash, var)
330
331 if !hash.nil? && hash.has_key?(var.downcase.to_sym)
332 eval('@' + var.downcase + ' = \'' + hash[var.downcase.to_sym] + '\'')
333 else
334 eval('@' + var.downcase + ' = defined?(' + var.upcase + '_DEFAULT) ? ' + var.upcase + '_DEFAULT : nil')
335 end
336
337 end
338
339 end
340
341 class XSPF::Tracklist < XSPF::Playlist
342
343 attr_reader :tracklist
344
345 # Creates a XSPF::Tracklist from a XSPF::Playlist (parse mode) or without parameters (generation mode)
346 def initialize(playlist=nil)
347 if (playlist.instance_of?(Hash) || playlist.nil?) then
348 @tracklist = ''
349 @tracks = []
350 else
351 @tracklist = playlist.tracklist_xml
352 @tracks = @tracklist.elements.collect { |track| XSPF::Track.new(track) }
353 end
354 end
355
356 # Returns an array XSPF::Track objects
357 def tracks
358 @tracks
359 end
360
361 # Adds a new XSPF::Track to the XSPF::Tracklist
362 def <<(track)
363 @tracks << track
364 end
365
366 # Exports the XSPF::Tracklist to XML (only the <trackList> section)
367 def to_xml
368 xml = REXML::Element.new('trackList')
369 @tracks.each { |t| xml << REXML::Document.new(t.to_xml) }
370 xml.to_s
371 end
372
373 end
374
375 class XSPF::Track
376
377 attr_reader :track
378
379 #:stopdoc:
380 ELEMENTS = %w{ location identifier title creator annotation info image album trackNum duration extension }
381 ATTRIBUTE_AND_ELEMENT = %w{ link meta }
382
383 LOCATION_RDOC = 'URL of resource to be rendered. Probably an audio resource, but MAY be any type of resource with a well-known duration, such as video, a SMIL document, or an XSPF document. The duration of the resource defined in this element defines the duration of rendering. XSPF::Track objects MAY contain zero or more location elements, but a user-agent MUST NOT render more than one of the named resources. Currently, XSPF for Ruby returns only the first location.'
384 IDENTIFIER_RDOC = 'Canonical ID for this resource. Likely to be a hash or other location-independent name, such as a MusicBrainz identifier or isbn URN (if there existed isbn numbers for audio). MUST be a legal URN. XSPF::Track objects elements MAY contain zero or more identifier elements, but currently XSPF for Ruby returns only the first one.'
385 TITLE_RDOC = 'Human-readable name of the track that authored the resource which defines the duration of track rendering. This value is primarily for fuzzy lookups, though a user-agent may display it. XSPF::Track objects MAY contain exactly one.'
386 CREATOR_RDOC = 'Human-readable name of the entity (author, authors, group, company, etc) that authored the resource which defines the duration of track rendering. This value is primarily for fuzzy lookups, though a user-agent may display it. XSPF::Track objects MAY contain exactly one.'
387 ANNOTATION_RDOC = 'A human-readable comment on the track. This is character data, not HTML, and it may not contain markup. XSPF::Track objects MAY contain exactly one.'
388 INFO_RDOC = 'URL of a place where this resource can be bought or more info can be found.'
389 IMAGE_RDOC = 'URL of an image to display for the duration of the track. XSPF::Track objects MAY contain exactly one.'
390 ALBUM_RDOC = 'Human-readable name of the collection from which the resource which defines the duration of track rendering comes. For a song originally published as a part of a CD or LP, this would be the title of the original release. This value is primarily for fuzzy lookups, though a user-agent may display it. XSPF::Track objects MAY contain exactly one.'
391 TRACKNUM_RDOC = 'Integer with value greater than zero giving the ordinal position of the media on the XSPF::Track#album. This value is primarily for fuzzy lookups, though a user-agent may display it. XSPF::Track objects MAY contain exactly one. It MUST be a valid XML Schema nonNegativeInteger.'
392 DURATION_RDOC = 'The time to render a resource, in milliseconds. It MUST be a valid XML Schema nonNegativeInteger. This value is only a hint -- different XSPF generators will generate slightly different values. A user-agent MUST NOT use this value to determine the rendering duration, since the data will likely be low quality. XSPF::Track objects MAY contain exactly one duration element.'
393 EXTENSION_RDOC = 'The extension element allows non-XSPF XML to be included in XSPF documents without breaking XSPF validation. The purpose is to allow nested XML, which the meta and link elements do not. XSPF::Track objects MAY contain zero or more extension elements, but currently XSPF for Ruby returns only the first one.'
394 LINK_REL_RDOC = 'The link element allows non-XSPF web resources to be included in XSPF documents without breaking XSPF validation. A valid _link_ element has a _rel_ attribute and a _content_ element, obtained with XSPF::Track#link_rel and XSPF::Track#link_content respectively. XSPF::Track objects MAY contain zero or more link elements, but currently XSPF for Ruby returns only the first one.'
395 LINK_CONTENT_RDOC = 'The link element allows non-XSPF web resources to be included in XSPF documents without breaking XSPF validation. A valid _link_ element has a _rel_ attribute and a _content_ element, obtained with XSPF::Track#link_rel and XSPF::Track#link_content respectively. XSPF::Track objects MAY contain zero or more meta elements, but currently XSPF for Ruby returns only the first one.'
396 META_REL_RDOC = 'The meta element allows non-XSPF metadata to be included in XSPF documents without breaking XSPF validation. A valid _meta_ element has a _rel_ attribute and a _content_ element, obtained with XSPF::Track#meta_rel and XSPF::Track#meta_content respectively. XSPF::Track objects MAY contain zero or more meta elements, but currently XSPF for Ruby returns only the first one.'
397 META_CONTENT_RDOC = 'The meta element allows non-XSPF metadata to be included in XSPF documents without breaking XSPF validation. A valid _meta_ element has a _rel_ attribute and a _content_ element, obtained with XSPF::Track#meta_rel and XSPF::Track#meta_content respectively. XSPF::Track objects MAY contain zero or more meta elements, but currently XSPF for Ruby returns only the first one.'
398
399 ELEMENTS.each do |element|
400 MetaGen.add_method( self, element, "@track.elements['#{element}'].text", eval(element.upcase + '_RDOC').to_s )
401 end
402
403 ATTRIBUTE_AND_ELEMENT.each do |ae|
404 MetaGen.add_method( self, "#{ae}_content", "@track.elements['#{ae}'].text", eval(ae.upcase + '_CONTENT_RDOC').to_s )
405 MetaGen.add_method( self, "#{ae}_rel", "@track.elements['#{ae}'].attributes['rel']", eval(ae.upcase + '_REL_RDOC').to_s )
406 end
407
408 # :startdoc:
409
410 # Creates a XSPF::Track object from a <track> section of the XSPF document or from a hash of values
411 #
412 # Possible keys in the hash in generation mode: :location, :identifier, :title, :creator, :annotation, :info, :image, :album, :tracknum, :duration, :extension, :link_rel, :link_content, :meta_rel, :meta_content)
413 def initialize(tr)
414
415 if tr.instance_of?(Hash)
416
417 ELEMENTS.each do |element|
418 add_instance_variable(tr, element.downcase)
419 end
420
421 ATTRIBUTE_AND_ELEMENT.each do |ae|
422 add_instance_variable(tr, "#{ae.downcase}_content" )
423 add_instance_variable(tr, "#{ae.downcase}_rel" )
424 end
425
426 else
427 @track = tr
428
429 ELEMENTS.each do |element|
430 eval('@' + element.downcase + '= parse_' + element.downcase)
431 end
432
433 ATTRIBUTE_AND_ELEMENT.each do |ae|
434 eval('@' + ae.downcase + '_content = parse_' + ae.downcase + '_content')
435 eval('@' + ae.downcase + '_rel = parse_' + ae.downcase + '_rel')
436 end
437 end
438
439 end
440
441 # Exports the XSPF::Track to XML (only the <track> section)
442 def to_xml
443
444 xml = REXML::Element.new('track')
445
446 ELEMENTS.each do |element|
447 # TODO Sure there is a nicer way to do evaluate this condition...
448 unless eval('@' + element.downcase + '.nil?')
449 el = REXML::Element.new(element)
450 el.add_text( eval('@' + element.downcase) )
451 xml.add_element(el)
452 end
453 end
454
455 ATTRIBUTE_AND_ELEMENT.each do |ae|
456 # TODO Sure there is a nicer way to do evaluate this condition...
457 unless eval('@' + ae.downcase + '_rel.nil? && @'+ ae.downcase + '_content.nil?')
458 el = REXML::Element.new(ae.downcase)
459 el.add_attribute('rel', eval('@' + ae.downcase + '_rel') )
460 el.add_text( eval('@' + ae.downcase + '_content') )
461 xml.add_element(el)
462 end
463 end
464
465 xml.to_s
466
467 end
468
469 private
470 def add_instance_variable(hash, var)
471
472 if hash.has_key?(var.downcase.to_sym)
473 eval('@' + var.downcase + ' = \'' + hash[var.downcase.to_sym] + '\'')
474 else
475 eval('@' + var.downcase + ' = defined?(' + var.upcase + '_DEFAULT) ? ' + var.upcase + '_DEFAULT : nil')
476 end
477
478 end
479
480 end
Generated using the rcov code coverage analysis tool for Ruby version 0.7.0.