From d6e167289343dae795f50cc6e661a73dc6c1156f Mon Sep 17 00:00:00 2001 From: Niklaus Giger Date: Wed, 14 May 2014 18:07:12 +0200 Subject: [PATCH] Added commented oddb2xml.xsd. Include rspec tests --- README.md | 11 +- lib/oddb2xml/builder.rb | 110 +--- oddb2xml.xsd | 1080 +++++++++++++++++++++++++++++++++++++++ spec/builder_spec.rb | 69 ++- 4 files changed, 1168 insertions(+), 102 deletions(-) create mode 100644 oddb2xml.xsd diff --git a/README.md b/README.md index ec11bd2..89a7a83 100644 --- a/README.md +++ b/README.md @@ -100,14 +100,17 @@ See also http://bugs.ruby-lang.org/projects/ruby/wiki/ReleaseEngineering ## XSD files -If you need the XSD files, generate them yourself using the javabeans tool: + +The file oddb2xml.xsd was manually created by merging the output of the xmlbeans tools inst2xsd and trang * http://xmlbeans.apache.org/docs/2.0.0/guide/tools.html#inst2xsd +* http://www.thaiopensource.com/relaxng/trang.html + +Running rake spec will validated the XML-files generated during the tests using the Nokogiri validator. -this will generate you a valid XSD file that can be used to validate against the XML file. +Manually you can also validate (assuming that you have installed the xmlbeans tools) all generated XML-files using -i.e.: -* /home/zeno/.software/xmlbeans-2.6.0/bin/inst2xsd oddb_article.xml -outPrefix oddb_article +* xsdvalidate oddb2xml.xsd *.xml ## XML files diff --git a/lib/oddb2xml/builder.rb b/lib/oddb2xml/builder.rb index ad1e2f8..303cdd3 100644 --- a/lib/oddb2xml/builder.rb +++ b/lib/oddb2xml/builder.rb @@ -15,12 +15,21 @@ def create_element name, *args, &block end module Oddb2xml + XML_OPTIONS = { + 'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema', + 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', + 'xmlns' => 'http://wiki.oddb.org/wiki.php?pagename=Swissmedic.Datendeklaration', + 'CREATION_DATETIME' => Time.new.strftime('%FT%T%z'), + 'PROD_DATE' => Time.new.strftime('%FT%T%z'), + 'VALID_DATE' => Time.new.strftime('%FT%T%z') + } class Builder attr_accessor :subject, :index, :items, :flags, :lppvs, :actions, :migel, :orphans, :fridges, :infos, :packs, :prices, :ean14, :tag_suffix, - :companies, :people + :companies, :people, + :xsd def initialize(args = {}) @options = args @subject = nil @@ -265,12 +274,7 @@ def build_substance xml.doc.tag_suffix = @tag_suffix datetime = Time.new.strftime('%FT%T%z') xml.SUBSTANCE( - 'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema', - 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', - 'xmlns' => 'http://wiki.oddb.org/wiki.php?pagename=Swissmedic.Datendeklaration', - 'CREATION_DATETIME' => datetime, - 'PROD_DATE' => datetime, - 'VALID_DATE' => datetime + XML_OPTIONS ) { Oddb2xml.log "build_substance #{@substances.size} substances" exit 2 if @options[:extended] and @substances.size == 0 @@ -299,14 +303,7 @@ def build_limitation _builder = Nokogiri::XML::Builder.new(:encoding => 'utf-8') do |xml| xml.doc.tag_suffix = @tag_suffix datetime = Time.new.strftime('%FT%T%z') - xml.LIMITATION( - 'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema', - 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', - 'xmlns' => 'http://wiki.oddb.org/wiki.php?pagename=Swissmedic.Datendeklaration', - 'CREATION_DATETIME' => datetime, - 'PROD_DATE' => datetime, - 'VALID_DATE' => datetime - ) { + xml.LIMITATION(XML_OPTIONS) { @limitations.each do |lim| xml.LIM('DT' => '') { case lim[:key] @@ -340,20 +337,16 @@ def build_limitation end _builder.to_xml end + def xml_and_comment(xml, *args, &block) + $stderr.puts "xml_and_comment #{args[0]}" + end def build_interaction prepare_interactions prepare_codes _builder = Nokogiri::XML::Builder.new(:encoding => 'utf-8') do |xml| xml.doc.tag_suffix = @tag_suffix datetime = Time.new.strftime('%FT%T%z') - xml.INTERACTION( - 'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema', - 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', - 'xmlns' => 'http://wiki.oddb.org/wiki.php?pagename=Swissmedic.Datendeklaration', - 'CREATION_DATETIME' => datetime, - 'PROD_DATE' => datetime, - 'VALID_DATE' => datetime - ) { + xml.INTERACTION(XML_OPTIONS) { Oddb2xml.log "build_interaction #{@interactions.size} interactions" @interactions.sort_by{|ix| ix[:ixno] }.each do |ix| xml.IX('DT' => '') { @@ -411,14 +404,7 @@ def build_code _builder = Nokogiri::XML::Builder.new(:encoding => 'utf-8') do |xml| xml.doc.tag_suffix = @tag_suffix datetime = Time.new.strftime('%FT%T%z') - xml.CODE( - 'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema', - 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', - 'xmlns' => 'http://wiki.oddb.org/wiki.php?pagename=Swissmedic.Datendeklaration', - 'CREATION_DATETIME' => datetime, - 'PROD_DATE' => datetime, - 'VALID_DATE' => datetime - ) { + xml.CODE(XML_OPTIONS) { @codes.each_pair do |val, definition| xml.CD('DT' => '') { xml.CDTYP definition[:int] @@ -451,20 +437,13 @@ def build_product _builder = Nokogiri::XML::Builder.new(:encoding => 'utf-8') do |xml| xml.doc.tag_suffix = @tag_suffix datetime = Time.new.strftime('%FT%T%z') - xml.PRODUCT( - 'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema', - 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', - 'xmlns' => 'http://wiki.oddb.org/wiki.php?pagename=Swissmedic.Datendeklaration', - 'CREATION_DATETIME' => datetime, - 'PROD_DATE' => datetime, - 'VALID_DATE' => datetime - ) { + xml.PRODUCT(XML_OPTIONS) { list = [] length = 0 @products.each do |obj| seq = obj[:seq] length += 1 - xml.PRD('DT' => '') { + xml.PRD('DT' => '') { ean = obj[:ean].to_s xml.GTIN ean ppac = ((_ppac = @packs[ean[4..11].intern] and !_ppac[:is_tier]) ? _ppac : {}) @@ -605,14 +584,7 @@ def build_article _builder = Nokogiri::XML::Builder.new(:encoding => 'utf-8') do |xml| xml.doc.tag_suffix = @tag_suffix datetime = Time.new.strftime('%FT%T%z') - xml.ARTICLE( - 'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema', - 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', - 'xmlns' => 'http://wiki.oddb.org/wiki.php?pagename=Swissmedic.Datendeklaration', - 'CREATION_DATETIME' => datetime, - 'PROD_DATE' => datetime, - 'VALID_DATE' => datetime - ) { + xml.ARTICLE(XML_OPTIONS) { @articles.each do |obj| idx += 1 Oddb2xml.log "build_article #{idx} of #{@articles.size} articles" if idx % 500 == 0 @@ -636,9 +608,9 @@ def build_article if !@prices.empty? && ean && @prices[ean] price = @prices[ean] # zurrose end - xml.ART('DT' => '') { - xml.PHAR de_idx[:pharmacode] unless de_idx[:pharmacode].empty? - #xml.GRPCD + xml.ART('DT' => '') { + xml.PHAR de_idx[:pharmacode] unless de_idx[:pharmacode].empty? + #xml.GRPCD #xml.CDS01 #xml.CDS02 if ppac @@ -660,7 +632,7 @@ def build_article end if de_idx - xml.SALECD(de_idx[:status].empty? ? 'N' : de_idx[:status]) + xml.SALECD(de_idx[:status].empty? ? 'N' : de_idx[:status]) # XML_OPTIONS end if pac and pac[:limitation_points] #xml.INSLIM @@ -804,14 +776,7 @@ def build_fi _builder = Nokogiri::XML::Builder.new(:encoding => 'utf-8') do |xml| xml.doc.tag_suffix = @tag_suffix datetime = Time.new.strftime('%FT%T%z') - xml.KOMPENDIUM( - 'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema', - 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', - 'xmlns' => 'http://wiki.oddb.org/wiki.php?pagename=Swissmedic.Datendeklaration', - 'CREATION_DATETIME' => datetime, - 'PROD_DATE' => datetime, - 'VALID_DATE' => datetime - ) { + xml.KOMPENDIUM(XML_OPTIONS) { length = 0 %w[de fr].each do |lang| infos = @infos[lang].uniq {|i| i[:monid] } @@ -848,14 +813,7 @@ def build_fi_product _builder = Nokogiri::XML::Builder.new(:encoding => 'utf-8') do |xml| xml.doc.tag_suffix = @tag_suffix datetime = Time.new.strftime('%FT%T%z') - xml.KOMPENDIUM_PRODUCT( - 'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema', - 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', - 'xmlns' => 'http://wiki.oddb.org/wiki.php?pagename=Swissmedic.Datendeklaration', - 'CREATION_DATETIME' => datetime, - 'PROD_DATE' => datetime, - 'VALID_DATE' => datetime - ) { + xml.KOMPENDIUM_PRODUCT(XML_OPTIONS) { length = 0 info_index = {} %w[de fr].each do |lang| @@ -893,13 +851,7 @@ def build_company _builder = Nokogiri::XML::Builder.new(:encoding => 'utf-8') do |xml| xml.doc.tag_suffix = @tag_suffix datetime = Time.new.strftime('%FT%T%z') - xml.Betriebe( - 'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema', - 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', - 'xmlns' => 'http://wiki.oddb.org/wiki.php?pagename=Swissmedic.Datendeklaration', - 'CREATION_DATETIME' => datetime, - 'VALID_DATE' => datetime - ) { + xml.Betriebe(XML_OPTIONS) { @companies.each do |c| xml.Betrieb('DT' => '') { xml.GLN_Betrieb c[:gln] unless c[:gln].empty? @@ -930,13 +882,7 @@ def build_person _builder = Nokogiri::XML::Builder.new(:encoding => 'utf-8') do |xml| xml.doc.tag_suffix = @tag_suffix datetime = Time.new.strftime('%FT%T%z') - xml.Personen( - 'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema', - 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', - 'xmlns' => 'http://wiki.oddb.org/wiki.php?pagename=Swissmedic.Datendeklaration', - 'CREATION_DATETIME' => datetime, - 'VALID_DATE' => datetime - ) { + xml.Personen(XML_OPTIONS) { @people.each do |p| xml.Person('DT' => '') { xml.GLN_Person p[:gln] unless p[:gln].empty? diff --git a/oddb2xml.xsd b/oddb2xml.xsd new file mode 100644 index 0000000..f948417 --- /dev/null +++ b/oddb2xml.xsd @@ -0,0 +1,1080 @@ + + + + + oddb2xml is a ruby gem developed and maintained by yweese GmbH. + The source code is available under https://github.com/zdavatz/oddb2xml. + It might be installed (Ruby >= 1.9 required) via "gem install oddb2xml". + Under http://dev.ywesee.com/Main/Oddb2xml you find more information on how the data is generated. + + In this XSD file we refer to the following sources: + # swissINDEX + ## Pharma: https://index.ws.e-mediat.net/Swissindex/Pharma/ws_Pharma_V101.asmx?WSDL" + ## NonPharma https://index.ws.e-mediat.net/Swissindex/NonPharma/ws_NonPharma_V101.asmx?WSDL" + # Preparations.xml + ## Extracted from http://bag.e-mediat.net/SL2007.Web.External/File.axd?file=XMLPublications.zip + # Packungen.xls https://www.swissmedic.ch/arzneimittel/00156/00221/00222/00230/index.html?lang=de + # Prices (ZurRose) http://zurrose.com/fileadmin/main/lib/download.php?file=/fileadmin/user_upload/downloads/ProduktUpdate/IGM11_mit_MwSt/Vollstamm/transfer.dat + # https://raw.github.com/zdavatz/oddb2xml_files/master/BM_Update.txt + see Anhang 1, 4, 5 und 6 zur AMZV; SR 821.212.22. http://www.admin.ch/opc/de/classified-compilation/20011693/ + # LPPV: https://raw.github.com/zdavatz/oddb2xml_files/master/LPPV.txt + # https://www.medregbm.admin.ch/Publikation/CreateExcelListBetriebs + # https://www.medregbm.admin.ch/Publikation/CreateExcelListMedizinalPersons + # (epha-)interactions https://download.epha.ch/cleaned/matrix.csv + + For historical reasons the generated *.XML have not a common layout and some fields have different meanings in differen files. + + The two files oddb_article and oddb_product are not normalized. There if for one swissmedic IKSNR several + packages are available you will find an entry inside oddb_article and oddb_product for each of them. + The GTIN (Global Trade Item Number, aka EAN13) is emitted as field GTIN in oddb_product.xml. Inside + oddb_article you find it as element BC inside ARTBAR (Article barcode). + + Some comments for invidual fields: + NINCD: possible values are 10 => BAG-XML (SL/LS), 13 -> MiGel, 20 => (LPPV) Limitation, empty => NonPharma) + GENCD: possible values are 'O' for original and 'G' for generic. We don't have a list of possible generics for a given original. + It is however possible find generics via ch.oddb.org where a sophisticated algorithm searches for similar medicaments + taking into account all ATC-codes and galenic information. + PHAR Pharmacode: Taken from swissINDEX or ZurRose characters 3..9 + PEXF Price Ex-Factory (exkl. VAT): Taken from the ZurRose.dat characters 60..65 + PPUB Public Price (inkl. VAT): Taken from the ZurRose.dat characters 66..71 + SLOPLUS Selbsbehalt/deductilbe, where 1 => 20%, 2 => 10%, '' => not known + + FIRST import all data from swissINDEX + * GTIN + * PHAR + * Status + * Since STDATE + * Bezeichnung (DE / FR) + * QTY Quantity, e.g. 3 Flaschen 5 ml. The EAN13 specified that this package contains 3 bottles. Think of it as + as description of volume/size of the content. + * ATC + * company_name (only for migel) + * GLN company_ean, field COMPNO in oddb_article + + then you add the following flags via Preparations.xml + * Ex-Factory Price + * Public Price + * SL Price valid from + * decuctible/SLOPLUS + * Original / Generic + * all Limitations (go into oddb_limitations.xml) + * narcotics (FlagNarcosis) emitted as element BG with value 'Y' or 'N' + + then you add the following flags via Packungen.xls + * Abgabekategorie (column 'N') as field SMCAT + * ATC (if missing from swissINDEX) -> Field SubstanceSwissmedic in oddb_article.xml + * Wirkstoff (column 'O') -> Field SubstanceSwissmedic in oddb_article.xml + * Packungsgrösse (column 'L') -> Field PackGrSwissmedic in oddb_article.xml + * Packungseinheit(column 'L') -> Field EinheitSwissmedic in oddb_article.xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spec/builder_spec.rb b/spec/builder_spec.rb index be1e12e..aa47a1d 100644 --- a/spec/builder_spec.rb +++ b/spec/builder_spec.rb @@ -18,6 +18,18 @@ def buildr_capture(stream) end end +def check_validation_via_xsd + @oddb2xml_xsd = File.expand_path(File.join(File.dirname(__FILE__), '..', 'oddb2xml.xsd')) + File.exists?(@oddb2xml_xsd).should be_true + files = Dir.glob('*.xml') + xsd = Nokogiri::XML::Schema(File.read(@oddb2xml_xsd)) + files.each{ + |file| + doc = Nokogiri::XML(File.read(@article_xml)) + xsd.validate(doc).each do |error| error.message.should be_nil end + } +end + describe Oddb2xml::Builder do NrExtendedArticles = 71 NrPharmaAndNonPharmaArticles = 61 @@ -34,6 +46,23 @@ def buildr_capture(stream) Dir.chdir @savedDir if @savedDir and File.directory?(@savedDir) end + context 'XSD-generation: ' do + let(:cli) do + opts = {} + @oddb2xml_xsd = File.expand_path(File.join(File.dirname(__FILE__), '..', 'oddb2xml.xsd')) + @article_xml = File.expand_path(File.join(Oddb2xml::WorkDir, 'oddb_article.xml')) + @product_xml = File.expand_path(File.join(Oddb2xml::WorkDir, 'oddb_product.xml')) + Oddb2xml::Cli.new(opts) + end + + it 'should return true when validating oddb_article.xml against oddb_article.xsd' do + res = buildr_capture(:stdout){ cli.run } + File.exists?(@article_xml).should be_true + File.exists?(@product_xml).should be_true + check_validation_via_xsd + end + end + context 'should handle BAG-articles with and without pharmacode' do it { dat = File.read(File.expand_path('../data/Preparations.xml', __FILE__)) @@ -50,12 +79,16 @@ def buildr_capture(stream) Oddb2xml::Cli.new(opts) end + it 'should pass validating via oddb2xml.xsd' do + check_validation_via_xsd + end + it 'should generate a valid oddb_product.xml' do res = buildr_capture(:stdout){ cli.run } res.should match(/products/) - article_filename = File.expand_path(File.join(Oddb2xml::WorkDir, 'oddb_article.xml')) - File.exists?(article_filename).should be_true - article_xml = IO.read(article_filename) + @article_xml = File.expand_path(File.join(Oddb2xml::WorkDir, 'oddb_article.xml')) + File.exists?(@article_xml).should be_true + article_xml = IO.read(@article_xml) product_filename = File.expand_path(File.join(Oddb2xml::WorkDir, 'oddb_product.xml')) File.exists?(product_filename).should be_true unless /1\.8\.7/.match(RUBY_VERSION) @@ -118,14 +151,18 @@ def buildr_capture(stream) Oddb2xml::Cli.new(opts) end + it 'should pass validating via oddb2xml.xsd' do + check_validation_via_xsd + end + it 'should handle not duplicate pharmacode 5366964' do res = buildr_capture(:stdout){ cli.run } res.should match(/NonPharma/i) res.should match(/NonPharma products: #{NrPharmaAndNonPharmaArticles}/) - article_filename = File.expand_path(File.join(Oddb2xml::WorkDir, 'oddb_article.xml')) - File.exists?(article_filename).should be_true - article_xml = IO.read(article_filename) - doc = REXML::Document.new File.new(article_filename) + @article_xml = File.expand_path(File.join(Oddb2xml::WorkDir, 'oddb_article.xml')) + File.exists?(@article_xml).should be_true + article_xml = IO.read(@article_xml) + doc = REXML::Document.new File.new(@article_xml) dscrds = XPath.match( doc, "//ART" ) XPath.match( doc, "//PHAR" ).find_all{|x| x.text.match('5366964') }.size.should == 1 dscrds.size.should == NrExtendedArticles @@ -137,10 +174,10 @@ def buildr_capture(stream) res = buildr_capture(:stdout){ cli.run } res.should match(/NonPharma/i) res.should match(/NonPharma products: #{NrPharmaAndNonPharmaArticles}/) - article_filename = File.expand_path(File.join(Oddb2xml::WorkDir, 'oddb_article.xml')) - File.exists?(article_filename).should be_true - article_xml = IO.read(article_filename) - doc = REXML::Document.new File.new(article_filename) + @article_xml = File.expand_path(File.join(Oddb2xml::WorkDir, 'oddb_article.xml')) + File.exists?(@article_xml).should be_true + article_xml = IO.read(@article_xml) + doc = REXML::Document.new File.new(@article_xml) dscrds = XPath.match( doc, "//ART" ) dscrds.size.should == NrExtendedArticles XPath.match( doc, "//PHAR" ).find_all{|x| x.text.match('1699947') }.size.should == 1 # swissmedic_packages Cardio-Pulmo-Rénal Sérocytol, suppositoire @@ -166,10 +203,10 @@ def buildr_capture(stream) it 'should emit a correct oddb_article.xml' do res = buildr_capture(:stdout){ cli.run } - article_filename = File.expand_path(File.join(Oddb2xml::WorkDir, 'oddb_article.xml')) - File.exists?(article_filename).should be_true - article_xml = IO.read(article_filename) - doc = REXML::Document.new File.new(article_filename) + @article_xml = File.expand_path(File.join(Oddb2xml::WorkDir, 'oddb_article.xml')) + File.exists?(@article_xml).should be_true + article_xml = IO.read(@article_xml) + doc = REXML::Document.new File.new(@article_xml) unless /1\.8\.7/.match(RUBY_VERSION) # check articles article_xml.should match(/3TC/) @@ -195,7 +232,7 @@ def buildr_capture(stream) article_xml.should match(/7680555580054/) # ZYVOXID article_xml.should match(/ZYVOXID/i) - doc = REXML::Document.new File.new article_filename + doc = REXML::Document.new File.new @article_xml dscrds = XPath.match( doc, "//DSCRD" ) dscrds.find_all{|x| x.text.match('ZYVOXID Filmtabl 600 mg') }.size.should == 1