6

I have already seen few answers on various places, in regards to setting the order of XML elements being returned by XMLout. However, I am not able to solve a problem using those answers/examples.

I have a script that needs to output some XML data, and certain elements need to be printed in certain order. Hash is pretty complex, and I was not able to achieve any results by overriding sorted_keys in XML::Simple object. Well, I did, but not in the way I wanted.

Sample code is below, details on the problem are below the code.

#!/usr/bin/perl

use strict;
use warnings;
use XML::Simple;

package MyXMLSimple;
use base 'XML::Simple';

sub sorted_keys
{
 my ($self, $name, $hashref) = @_;
 # ... 
 return $self->SUPER::sorted_keys($name, $hashref);
}

package main;

my $xmlParser = MyXMLSimple->new;

my $items = {
 'status' => 'OK',
 'fields' => {
  'i1' => {
   'header' => 'Header 1',
   'max_size' => '3'
  },
  'i2' => {
   'header' => 'Header 2',
   'max_size' => '8'
  }
 },
 'item_list' => {
  'GGG' => {
   'index' => '3',
   'i' => 3,
   'points' => {
    'p5' => {
     'data' => '10',
    }
   },
  },
  'AAA' => {
   'index' => '1',
   'i' => 2,
   'points' => {
    'p7' => {
     'data' => '22',
    }
   },
  },
  'ZZZ' => {
   'index' => '2',
   'i' => 1,
   'points' => {
    'p6' => {
     'data' => '15',
    }
   },
  }
 }
};

my $xml = $xmlParser->XMLout($items);
print "$xml";

So, the output of this script will be this:

<opt status="OK">
  <fields name="i1" header="Header 1" max_size="3" />
  <fields name="i2" header="Header 2" max_size="8" />
  <item_list name="AAA" i="2" index="1">
    <points name="p7" data="22" />
  </item_list>
  <item_list name="GGG" i="3" index="3">
    <points name="p5" data="10" />
  </item_list>
  <item_list name="ZZZ" i="1" index="2">
    <points name="p6" data="15" />
  </item_list>
</opt>

item_list elements are printed out, and output order is alphabetically, by sorting on name attribute. Output order is AAA, GGG, ZZZ.

However, what I would need is to have the output while being sorted (numerically, from lowest to highest) on i element. So that output will be in order ZZZ, AAA, GGG.

I have no control over order in the hash (not without using Tie::... module), so I can not do it that way. If I use NoSort => 1, output will not be sorted by anything in particular, so I'll end up getting random output.

So, I am pretty sure that there must be a way to sort this out the way I want it by overriding sorted_keys subroutine. However, I couldn't get results I wanted, because sorted_keys gets invoked for each instance of item_list. When sorted_keys is invoked for opt element, then I simply have access to whole hash reference, but again no means to guarantee output ordering without relying on Tie:: module.

Now, I have managed to get this to work the way I want, by using Tie::IxHash module, then overriding sorted_keys and (re)creating a subhash item_list, by reinserting values from original hash into new (ordered) one, then deleting subhash in original hash, and substituting it with new ordered hash.

Something like this:

sub sorted_keys
{
 my ($self, $name, $hashref) = @_;
 if ($name eq "opt")
 {
  my $clist = { };
  tie %{$clist}, "Tie::IxHash";

  my @sorted_keys = sort { $hashref->{item_list}->{$a}->{i} <=> $hashref->{item_list}->{$b}->{i} } keys %{$hashref->{item_list}};
  foreach my $sorted_key (@sorted_keys)
  {
   $clist->{$sorted_key} = $hashref->{item_list}->{$sorted_key};
  }

  delete $hashref->{item_list};
  $hashref->{item_list} = $clist;
 }
 return $self->SUPER::sorted_keys($name, $hashref);
}

Although this works (and so far seems to work reliably), I do believe that there must be a way to achieve this without using Tie::IxHash module and doing all that hash recreation/reordering, and only by somehow sorting/returning certain data from within sorted_keys.

I just can't figure it out, and I don't really understand how sorted_keys is supposed to work (especially when you get different results with different/complex sets of input data ;), but I hope there is someone out there who knows this.

I mean, I have tried modifying XML/Simple.pm itself and changing sort order in the last return line of sorted_keys subroutine, but I was still getting alphanumerically sorted output. I am afraid I can't figure out how I would modify it so it doesn't sort on name but on i.

1
  • TLDR, bit +1 for thoroughness of (IMHO, futile) effort - would have done +2 if I could :) Commented Nov 12, 2010 at 17:08

2 Answers 2

2

I believe at this point you have outgrown XML::Simple. If you care about the order of children in an element, then it's time to use a more XML-ish module. For the style of XML creation you want, maybe XML::TreeBuilder, look at the new_from_lol method. Or XML::LibXML, XML::Twig, XML::Writer...

I also have tried mixing Tie::IxHash and XML::Simple in the past, and it wasn't pretty. you actually got pretty far here. But I believe this way lies madness

Sign up to request clarification or add additional context in comments.

Comments

0

Maybe take a look at overriding hash_to_array? Worked out for me. http://perlmaven.com/xml-simple-sorting

I wanted to perform a natural sort against a tag key and managed to achieve by overriding XML::Simple hash_to_array. I basically copied the method from XML::Simple.pm and made a small edit for the natural sort - like this:

package MyXMLSimple;      # my XML::Simple subclass
use base 'XML::Simple';

sub hash_to_array {
  my $self    = shift;
  my $parent  = shift;
  my $hashref = shift;

  my $arrayref = [];

  my($key, $value);

  if ( $parent eq "mytag" ) {
  my @keys = $self->{opt}->{nosort} ? keys %$hashref : sort {$a<=>$b} keys %$hashref;
  foreach $key (@keys) {
    $value = $hashref->{$key};
    return($hashref) unless(UNIVERSAL::isa($value, 'HASH'));

    if(ref($self->{opt}->{keyattr}) eq 'HASH') {
      return($hashref) unless(defined($self->{opt}->{keyattr}->{$parent}));
      push @$arrayref, $self->copy_hash(
        $value, $self->{opt}->{keyattr}->{$parent}->[0] => $key
      );
    }
    else {
      push(@$arrayref, { $self->{opt}->{keyattr}->[0] => $key, %$value });
    }
  }

  } else {
  my @keys = $self->{opt}->{nosort} ? keys %$hashref : sort keys %$hashref;
  foreach $key (@keys) {
    $value = $hashref->{$key};
    return($hashref) unless(UNIVERSAL::isa($value, 'HASH'));

    if(ref($self->{opt}->{keyattr}) eq 'HASH') {
      return($hashref) unless(defined($self->{opt}->{keyattr}->{$parent}));
      push @$arrayref, $self->copy_hash(
        $value, $self->{opt}->{keyattr}->{$parent}->[0] => $key
      );
    }
    else {
      push(@$arrayref, { $self->{opt}->{keyattr}->[0] => $key, %$value });
    }
  }
  }

  return($arrayref);
}

my $xmlParser = MyXMLSimple->new(KeepRoot => 1);
$xmlParser->XMLout($self->{_xml}, KeyAttr => { ...

Sure, it's ugly, sentinel would need to set 'i' as KeyAttr and its 5 years too late but it works and now I can go do something else :)

1 Comment

While this link may answer the question, it is better to include the essential parts of the answer here and provide the link for reference. Link-only answers can become invalid if the linked page changes

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.