diff --git a/.gitignore b/.gitignore index f7bebc4..eb6d70f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ .bundle .config .yardoc +.DS_Store Gemfile.lock InstalledFiles _yardoc diff --git a/Rakefile b/Rakefile index d86a3e4..4239338 100644 --- a/Rakefile +++ b/Rakefile @@ -1,5 +1,7 @@ require "bundler/gem_tasks" +task :default => [:test] + desc 'Test specs' task 'test' do sh "bundle exec bacon -a" diff --git a/example/Sample-osx.csv b/example/Sample-osx.csv new file mode 100644 index 0000000..a0f47bc --- /dev/null +++ b/example/Sample-osx.csv @@ -0,0 +1,17 @@ +Case ID,Activity,Start Date,End Date,Agent Position,Customer ID,Product,Service Type,Resource +Case 1,Inbound Call,9.3.10 8:05,9.3.10 8:10,FL,Customer 1,MacBook Pro,Referred to Servicer,Helen +Case 1,Handle Case,11.3.10 10:30,11.3.10 10:32,FL,Customer 1,MacBook Pro,Referred to Servicer,Helen +Case 1,Call Outbound,11.3.10 11:45,11.3.10 11:52,FL,Customer 1,MacBook Pro,Referred to Servicer,Henk +Case 2,Inbound Call,4.3.10 11:43,4.3.10 11:46,FL,Customer 2,MacBook Pro,Referred to Servicer,Susi +Case 3,Inbound Call,25.3.10 9:32,25.3.10 9:33,FL,Customer 3,MacBook Pro,Referred to Servicer,Mary +Case 4,Inbound Call,6.3.10 11:41,6.3.10 11:51,FL,Customer 4,iPhone,Referred to Servicer,Fred +Case 5,Inbound Call,18.3.10 10:54,18.3.10 11:01,FL,Customer 5,MacBook Pro,Product Assistance,Kenny +Case 6,Inbound Call,25.3.10 17:09,25.3.10 17:13,FL,Customer 6,MacBook Pro,Referred to Servicer,Harold +Case 6,Inbound Call,25.3.10 17:16,25.3.10 17:18,FL,Customer 6,MacBook Pro,Referred to Servicer,Nancy +Case 6,Inbound Call,26.3.10 8:36,26.3.10 8:40,FL,Customer 6,MacBook Pro,Referred to Servicer,Elena +Case 7,Inbound Call,18.3.10 11:49,18.3.10 11:50,FL,Customer 7,MacBook Pro,Product Assistance,Karen +Case 8,Inbound Call,11.3.10 9:20,11.3.10 9:23,FL,Customer 8,MacBook Pro,Referred to Servicer,Karen +Case 9,Inbound Email,19.3.10 19:47,21.3.10 8:17,FL,Customer 9,MacBook Pro,Product Assistance,Samuil +Case 9,Call Outbound,21.3.10 8:32,21.3.10 8:33,FL,Customer 9,MacBook Pro,Product Assistance,Samuil +Case 9,Handle Email,21.3.10 8:33,21.3.10 8:33,FL,Customer 9,MacBook Pro,Product Assistance,Samuil +Case 10,Handle Email,27.3.10 11:29,27.3.10 11:30,FL,Customer 10,iPhone,Product Assistance,Jochem diff --git a/example/example.rb b/example/example.rb new file mode 100644 index 0000000..2d1abd7 --- /dev/null +++ b/example/example.rb @@ -0,0 +1,92 @@ +# The behaviour of this example will depend on which, if any, ruby-xes gems you have installed +# +# 0. No gem installed +# Uncomment the line to specify the load path and use local code +# 1. ruby-xes-0.1.0 +# Comment the line to specify the load path and use ruby-xes-0.1.0 +# +# The key thing to note is that the only lice of this program that needs to be different +# when using ruby-xes-0.2.0 is the cline to write the XML to the file + +$LOAD_PATH.unshift('../lib') + +require 'csv' +require 'xes' + +puts "Example using ruby-xes-#{XES::VERSION}" + +# Get some test data +csv = CSV.parse(File.read('Sample-osx.csv'), :headers => true) + +doc = XES::Document.new +doc.log = log = XES::Log.default +#log = XES::Log.default +# This example does illustrate the use of nested attributes +#doc.log.xes_features = "nested-attributes" + +# Add Global attributes here +doc.log.trace_global = XES::Global.new('trace', [ + XES::int('CaseID', ''), XES::int('CustomerID', '') + ] +) +doc.log.event_global = XES::Global.new('event', [ + XES::string('Activity', ''), XES::date('StartDate', ''), XES::date('EndDate', ''), + XES::string('AgentPosition', ''), XES::string('Product', ''), + XES::string('ServiceType', ''), XES::string('Resource', '') + ] +) + +# Add Classifiers here +doc.log.classifiers = [ + XES::Classifier.new('Activity', 'Activity'), + XES::Classifier.new("Resource", "Resource") + ] + +id = 0 +trace = XES::Trace.new +csv.each do |row| + + #puts row['Case ID'] + caseId = row['Case ID'].split + custId = row['Customer ID'].split + + # CSV file is sorted on 'Case ID', so + # Each time we see a new 'Case ID' then we need to start a new trace + if not row['Case ID'].eql?(id) + trace = XES::Trace.new + trace.attributes << XES::int('CaseID', caseId[1]) + trace.attributes << XES::int('CustomerID', custId[1]) + end + + # Each row of the CSV file is an event, and + # Each column of the CSV file is an event attribute + event = XES::Event.new + event.attributes << XES::string('Activity', row['Activity']) + event.attributes << XES::container('times', [ + XES::date('StartDate', row['Start Date'], [ + XES::boolean('UTC-format', 'false') + ]), + XES::date('EndDate', row['End Date'], [ + XES::boolean('UTC-format', 'false') + ]) + ]) + event.attributes << XES::string('AgentPosition', row['Agent Position']) + event.attributes << XES::string('Product', row['Product']) + event.attributes << XES::string('ServiceType', row['Service Type']) + event.attributes << XES::string('Resource', row['Resource']) + trace.events << event + + if not row['Case ID'].eql?(id) + id = row['Case ID'] + doc.log.traces << trace + end + +end + +f = open("example-#{XES::VERSION}.xes", 'w') +if XES::VERSION.eql?("0.2.0") + f.write(doc.format) +else + doc.format.write(f, 2) +end +f.close() diff --git a/lib/xes.rb b/lib/xes.rb index 305b4d7..3e05fe4 100644 --- a/lib/xes.rb +++ b/lib/xes.rb @@ -1,4 +1,4 @@ -require "rexml/document" +require "nokogiri" require "time" require "xes/version" diff --git a/lib/xes/attribute.rb b/lib/xes/attribute.rb index c522cf5..cb23bde 100644 --- a/lib/xes/attribute.rb +++ b/lib/xes/attribute.rb @@ -68,13 +68,17 @@ def formattable? end # Format as a XML element. - def format + def format(doc) raise FormatError.new(self) unless formattable? - REXML::Element.new(type).tap do |attribute| - attribute.attributes["key"] = @key - attribute.attributes["value"] = format_value - meta.each {|m| attribute.elements << m.format if m.formattable?} + Nokogiri::XML::Element.new(@type, doc).tap do |el| + el["key"] = "#{@key}" + if not (@type.eql?('list') or @type.eql?('container')) + el["value"] = "#{@value}" + end + meta.each do |m| + el.add_child(m.format(doc)) if m.formattable? + end end end @@ -197,5 +201,29 @@ def boolean(key, value, meta=[]) def id(key, value, meta=[]) Attribute.new("id", key, value, meta) end + + # Return list attribute object. + # + # @param key [String] + # attribute name + # @param meta [Array] + # meta attributes + # @return [Attribute] + # list attribute + def list(key, meta=[]) + Attribute.new("list", key, '', meta) + end + + # Return container attribute object. + # + # @param key [String] + # attribute name + # @param meta [Array] + # meta attributes + # @return [Attribute] + # container attribute + def container(key, meta=[]) + Attribute.new("container", key, '', meta) + end end end diff --git a/lib/xes/classifier.rb b/lib/xes/classifier.rb index 90a3ec1..8581660 100644 --- a/lib/xes/classifier.rb +++ b/lib/xes/classifier.rb @@ -30,14 +30,14 @@ def formattable? # Format as a XML element. # - # @return [REXML::Element] + # @return [Nokogiri::XML::Element] # XML element - def format + def format(doc) raise FormatError.new(self) unless formattable? - REXML::Element.new("classifier").tap do |ext| - ext.attributes["name"] = @name - ext.attributes["keys"] = @keys + Nokogiri::XML::Element.new("classifier", doc).tap do |cls| + cls["name"] = "#{@name}" + cls["keys"] = "#{@keys}" end end diff --git a/lib/xes/document.rb b/lib/xes/document.rb index b988c12..540fbbc 100644 --- a/lib/xes/document.rb +++ b/lib/xes/document.rb @@ -20,17 +20,16 @@ def formattable? # Format as a XML document. # - # @return [REXML::Document] + # @return [Nokogiri::XML::Document] # XML document # @raise FormatError # format error when the document is not formattable def format raise FormatError.new(self) unless formattable? - REXML::Document.new.tap do |doc| - doc << REXML::XMLDecl.new - doc.elements << @log.format - end + doc = Nokogiri::XML::Document.new + doc.add_child(@log.format(doc)) + doc.to_xml end # @api private diff --git a/lib/xes/event.rb b/lib/xes/event.rb index 931b18b..1aba97f 100644 --- a/lib/xes/event.rb +++ b/lib/xes/event.rb @@ -25,12 +25,12 @@ def formattable? # Format as a XML element. # - # @return [REXML::Element] + # @return [Nokogiri::XML::Element] # XML element - def format - REXML::Element.new("event").tap do |event| + def format(doc) + Nokogiri::XML::Element.new("event", doc).tap do |evt| @attributes.each do |attribute| - event.elements << attribute.format if attribute.formattable? + evt.add_child(attribute.format(doc)) if attribute.formattable? end end end diff --git a/lib/xes/extension.rb b/lib/xes/extension.rb index 1de3c4e..a5ef037 100644 --- a/lib/xes/extension.rb +++ b/lib/xes/extension.rb @@ -37,15 +37,15 @@ def formattable? # Format as a XML element. # - # @return [REXML::Element] + # @return [Nokogiri::XML::Element] # XML element - def format + def format(doc) raise FormatError.new(self) unless formattable? - REXML::Element.new("extension").tap do |ext| - ext.attributes["name"] = @name - ext.attributes["prefix"] = @prefix - ext.attributes["uri"] = @uri + Nokogiri::XML::Element.new("extension", doc).tap do |ext| + ext["name"] = "#{@name}" + ext["prefix"] = "#{@prefix}" + ext["uri"] = "#{@uri}" end end diff --git a/lib/xes/global.rb b/lib/xes/global.rb index 96d7aa1..ad303d1 100644 --- a/lib/xes/global.rb +++ b/lib/xes/global.rb @@ -50,14 +50,16 @@ def formattable? # Format as a XML element. # - # @return [REXML::Element] + # @return [Nokogiri::XML::Element] # XML element - def format + def format(doc) raise FormatError.new(self) unless formattable? - REXML::Element.new("global").tap do |global| - global.attributes["scope"] = @scope.to_s - @attributes.each {|attribute| global.elements << attribute.format} + Nokogiri::XML::Element.new("global", doc).tap do |gbl| + gbl["scope"] = "#{@scope.to_s}" + @attributes.each do |attribute| + gbl.add_child(attribute.format(doc)) if attribute.formattable? + end end end diff --git a/lib/xes/log.rb b/lib/xes/log.rb index fd3c4ec..6132f64 100644 --- a/lib/xes/log.rb +++ b/lib/xes/log.rb @@ -30,7 +30,7 @@ class << self # new log instance def default new.tap do |log| - log.xes_version = "1.4" + log.xes_version = "2.0" log.xes_features = "nested-attributes" log.openxes_version = "1.0RC7" log.extensions = [ @@ -90,7 +90,7 @@ def default attr_accessor :traces def initialize - @xes_version = "1.4" + @xes_version = "2.0" @xes_features = "" @openxes_version = nil @xmlns = "http://www.xes-standard.org/" @@ -112,24 +112,32 @@ def formattable? # Format as a XML element. # - # @return [REXML::Element] + # @return [Nokogiri::XML::Element] # XML element # @raise FormatError # format error when the log is formattable - def format + def format(doc) raise FormatError.new(self) unless formattable? - REXML::Element.new("log").tap do |log| - log.attributes["xes.version"] = @xes_version.to_s if @xes_version - log.attributes["xes.features"] = @xes_features.to_s if @xes_features - log.attributes["openxes.version"] = @openxes_version.to_s if @openxes_version - log.attributes["xmlns"] = @xmlns.to_s if @xmlns - @extensions.each {|ext| log.elements << ext.format if ext.formattable?} - @classifiers.each {|classifier| log.elements << classifier.format if classifier.formattable?} - log.elements << @event_global.format if @event_global.formattable? - log.elements << @trace_global.format if @trace_global.formattable? - @attributes.each {|attribute| log.elements << attribute.format if attribute.formattable?} - @traces.each {|trace| log.elements << trace.format if trace.formattable?} + Nokogiri::XML::Element.new("log", doc).tap do |log| + log["xes.version"] = @xes_version.to_s if @xes_version + log["xes.features"] = @xes_features.to_s if @xes_features + log["openxes.version"] = @openxes_version.to_s if @openxes_version + log["xmlns"] = @xmlns.to_s if @xmlns + @extensions.each do |extension| + log.add_child(extension.format(doc)) if extension.formattable? + end + @classifiers.each do |classifier| + log.add_child(classifier.format(doc)) if classifier.formattable? + end + log.add_child(@trace_global.format(doc)) if @trace_global.formattable? + log.add_child(@event_global.format(doc)) if @event_global.formattable? + @attributes.each do |attribute| + log.add_child(attribute.format(doc)) if attribute.formattable? + end + @traces.each do |trace| + log.add_child(trace.format(doc)) if trace.formattable? + end end end diff --git a/lib/xes/trace.rb b/lib/xes/trace.rb index d903216..5827492 100644 --- a/lib/xes/trace.rb +++ b/lib/xes/trace.rb @@ -30,15 +30,19 @@ def formattable? # Format as a XML element. # - # @return [REXML::Element] + # @return [Nokogiri::XML::Element] # XML element # @raise FormatError - def format + def format(doc) raise FormatError.new(self) unless formattable? - REXML::Element.new("trace").tap do |trace| - @attributes.each {|attribute| trace.elements << attribute.format if attribute.formattable?} - @events.each {|event| trace.elements << event.format if event.formattable?} + Nokogiri::XML::Element.new("trace", doc).tap do |trc| + @attributes.each do |attribute| + trc.add_child(attribute.format(doc)) if attribute.formattable? + end + @events.each do |event| + trc.add_child(event.format(doc)) if event.formattable? + end end end diff --git a/lib/xes/version.rb b/lib/xes/version.rb index 42a62c9..aca3666 100644 --- a/lib/xes/version.rb +++ b/lib/xes/version.rb @@ -1,4 +1,4 @@ module XES # version of ruby-xes - VERSION = "0.1.0" + VERSION = "0.2.0" end diff --git a/ruby-xes.gemspec b/ruby-xes.gemspec index be98731..cd87cfe 100644 --- a/ruby-xes.gemspec +++ b/ruby-xes.gemspec @@ -9,16 +9,19 @@ Gem::Specification.new do |gem| gem.version = XES::VERSION gem.authors = ["Keita Yamaguchi"] gem.email = ["keita.yamaguchi@gmail.com"] - gem.description = "ruby-xes is a library for generating XES event log." - gem.summary = "ruby-xes is a library for generating XES event log." + gem.description = "ruby-xes is a library for generating XES v2.0 event logs" + gem.summary = "ruby-xes is a library for generating XES event logs" gem.homepage = "https://github.com/pione/ruby-xes" + gem.license = "MIT" gem.files = `git ls-files`.split($/) gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) gem.require_paths = ["lib"] - gem.add_development_dependency "bacon" - gem.add_development_dependency "yard", "~> 0.8.5" - gem.add_development_dependency "redcarpet" + gem.add_development_dependency "bacon", "~> 1.2", ">= 1.2.0" + gem.add_development_dependency "yard", "~> 0.8", ">= 0.8.5" + gem.add_development_dependency "redcarpet", "~> 3.2", ">= 3.2.3" + gem.add_development_dependency "rake", "~> 10.3", ">= 10.3.2" + gem.add_development_dependency "nokogiri", "~> 1.6" end diff --git a/test/spec_attribute.rb b/test/spec_attribute.rb index efc3058..13a7f51 100644 --- a/test/spec_attribute.rb +++ b/test/spec_attribute.rb @@ -43,6 +43,16 @@ XES::Attribute.new("id", "identity:id", "123") end + it 'should make list attribute' do + XES.list("identity:id").should == + XES::Attribute.new("list", "identity:id", "") + end + + it 'should make container attribute' do + XES.container("identity:id").should == + XES::Attribute.new("container", "identity:id", "") + end + it 'should get the type' do XES::Attribute.new("string", "concept:name", "A").type.should == "string" end @@ -110,41 +120,53 @@ end it 'should format string attribute as XML' do - XES.string("concept:name", "A").format.to_s.should == - "" + XES.string("concept:name", "A").format(Nokogiri::XML::Document.new).to_s.should == + %Q{} end it 'should format date attribute as XML' do Time.now.tap do |time| - XES.date("time:timestamp", time).format.to_s.should == - "" % time.iso8601 + XES.date("time:timestamp", time.iso8601(3)).format(Nokogiri::XML::Document.new).to_s.should == + '' % time.iso8601(3) end end it 'should format int attribute as XML' do - XES.int("counter", 123).format.to_s.should == - "" + XES.int("counter", 123).format(Nokogiri::XML::Document.new).to_s.should == + %Q{} end it 'should format float attribute as XML' do - XES.float("counter", 0.123).format.to_s.should == - "" + XES.float("counter", 0.123).format(Nokogiri::XML::Document.new).to_s.should == + %Q{} end it 'should format boolean attribute as XML' do - XES.boolean("truth", true).format.to_s.should == - "" + XES.boolean("truth", true).format(Nokogiri::XML::Document.new).to_s.should == + %Q{} end it 'should format id attribute as XML' do - XES.id("identity:id", "123456").format.to_s.should == - "" + XES.id("identity:id", "123456").format(Nokogiri::XML::Document.new).to_s.should == + %Q{} + end + + it 'should format list attribute as XML' do + XES.list("identity:id").format(Nokogiri::XML::Document.new).to_s.should == + %Q{} + end + + it 'should format container attribute as XML' do + XES.container("identity:id").format(Nokogiri::XML::Document.new).to_s.should == + %Q{} end it 'should format attribute with meta attribute as XML' do XES.string("concept:name", "A").tap do |x| x.identity_id = "123456" - end.format.to_s.should == - "" + end.format(Nokogiri::XML::Document.new).to_s.should == + %Q{ + +} end end diff --git a/test/spec_classifier.rb b/test/spec_classifier.rb index 756f248..7230c1a 100644 --- a/test/spec_classifier.rb +++ b/test/spec_classifier.rb @@ -20,7 +20,7 @@ end it "should format as XML element" do - XES::Classifier.new("Event Name", "concept:name").format.to_s.should == - "" + XES::Classifier.new("Event Name", "concept:name").format(Nokogiri::XML::Document.new).to_s.should == + '' end end diff --git a/test/spec_event.rb b/test/spec_event.rb index b60fc2b..ee0145d 100644 --- a/test/spec_event.rb +++ b/test/spec_event.rb @@ -96,7 +96,9 @@ end it "should format as XML" do - XES::Event.new([XES.string("concept:name", "A")]).format.to_s.should == - "" + XES::Event.new([XES.string("concept:name", "A")]).format(Nokogiri::XML::Document.new).to_s.should == + %Q{ + +} end end diff --git a/test/spec_global.rb b/test/spec_global.rb index 3cb5795..2293a62 100644 --- a/test/spec_global.rb +++ b/test/spec_global.rb @@ -120,12 +120,16 @@ it "should format event global as XML element" do XES::Global.event.tap do |global| global.concept_name = "A" - end.format.to_s.should == "" + end.format(Nokogiri::XML::Document.new).to_s.should == %Q{ + +} end it "should format trace global as XML element" do XES::Global.trace.tap do |global| global.concept_name = "A" - end.format.to_s.should == "" + end.format(Nokogiri::XML::Document.new).to_s.should == %Q{ + +} end end diff --git a/test/spec_log.rb b/test/spec_log.rb index 24c0d83..a52f882 100644 --- a/test/spec_log.rb +++ b/test/spec_log.rb @@ -84,7 +84,13 @@ event.attributes << XES.string("concept:name", "test") end end - end.format.to_s.should == - "" + end.format(Nokogiri::XML::Document.new).to_s.should == + %Q{ + + + + + +} end end diff --git a/test/spec_trace.rb b/test/spec_trace.rb index d437340..bd69761 100644 --- a/test/spec_trace.rb +++ b/test/spec_trace.rb @@ -101,13 +101,13 @@ it "should raise FormatError because the trace has no events" do should.raise(XES::FormatError) do - XES::Trace.new.format + XES::Trace.new.format(Nokogiri::XML::Document.new) end end it "should raise FormatError because events of the trace are invalid" do should.raise(XES::FormatError) do - XES::Trace.new.tap{|x| x.events << XES::Event.new}.format + XES::Trace.new.tap{|x| x.events << XES::Event.new}.format(Nokogiri::XML::Document.new) end end end