#!/usr/bin/perl require 5; ##===================================================================## ## Program name: ebind ## ## Version: 1.0 (MS Windows 3.1 version) ## ## Author: Alvin Pollock (apollock@library.berkeley.edu) ## ## Date completed: 1/17/97 ## ## ## ## Description: Ebind generates a complete ebind sgml file and ## ## an associated file of entity declarations when ## ## given an ebind worksheet as input. ## ## ## ## Note: Perl version 5 is required to run ebind. ## ##===================================================================## $DefaultFileExt = ".tif"; $DefaultNotation = "tiff"; $datfile = $ARGV[0]; $outfile = $datfile; $entfile = $datfile; if ($outfile =~ /\./) { substr ($outfile, rindex($outfile, '.')) = '.sgm'; } else { $outfile = "$outfile\.sgm"; } if ($entfile =~ /\./) { substr ($entfile, rindex($entfile, '.')) = '.ent'; } else { $entfile = "$entfile\.ent"; } $datfile =~ /([-a-z0-9]+)(\.[-a-z0-9]*)?$/i && ($rootname = $1); $rootname =~ tr/A-Z/a-z/; &romanstart; open (DATFILE, $datfile) || die "Couldn't open $datfile: $!\nAaaaaaaaaaaaaaaaaaah!\n[Crash and burn]\n"; &Types; $valid = 1; while ($line = ) { $linenum++; next if $line =~ /^\s*$/; # Skip blank lines next if $line =~ /^\s*[#\/]/; # Skip comments unless ($structflag || $seqflag) { $line =~ /^\s*ID Num:\s*/i && (chomp($IDNum = $'), next); $line =~ /^\s*Author:\s*/i && (chomp($Author = $'), next); $line =~ /^\s*Title:\s*/i && (chomp($Title = $'), next); $line =~ /^\s*Subtitle:\s*/i && (chomp($Subtitle = $'), next); $line =~ /^\s*Edition:\s*/i && (chomp($Edition = $'), next); $line =~ /^\s*Pubplace:\s*/i && (chomp($Pubplace = $'), next); $line =~ /^\s*Publisher:\s*/i && (chomp($Publisher = $'), next); $line =~ /^\s*Pubyear:\s*/i && (chomp($Pubyear = $'), next); $line =~ /^\s*Series:\s*/i && (chomp($Series = $'), next); } if ($line =~ /--\s*structure/i) { $structflag = 1; $structexist = 1; $seqflag = 0; next; } if ($line =~ /--\s*sequence/i) { $seqflag = 1; $structflag = 0; next; } if ($line =~ /^\s*front\s*$/i) { $docpart = "front"; $frontexists = 1; next; } if ($line =~ /^\s*body\s*$/i) { $docpart = "body"; next; } if ($line =~ /^\s*back\s*$/i) { $docpart = "back"; next; } if ($structflag) { &BuildStruct($line); next; } if ($seqflag) { &BuildSeq($line); next; } } close DATFILE; &UpdateStruct; &ValidateSeq; &ValidateStruct; &GenerateSGML if $valid; &PrintOutput if $valid; ## End Program ##=================================================================## ## Subroutines. ## ##=================================================================## sub BuildStruct { my($line) = @_; my($nestlevel, $type, $seq, $head, $explicit); if ($line =~ /^\s*\d\s/) { $explicit = 1; ($nestlevel, $type, $seqrange, $head) = $line =~ /^\s*(\d)\s+([^\s]+)\s+([^\s]+)\s*(.*)$/; } else { $nestlevel = 0; $explicit = 0; ($type, $seqrange, $head) = $line =~ /^\s*([^\s]+)\s+([^\s]+)\s*(.*)$/; } my($record) = {}; # Initialize a reference to an anonymous hash; $record->{docpart} = $docpart if $docpart; $record->{seqrange} = $seqrange; $record->{nestlevel} = $nestlevel; $record->{type} = $type; $record->{explicit} = $explicit; if ($seqrange =~ /([^-]+)-(.*)/) { $record->{seq1} = $1; $record->{seq2} = $2; } else { $record->{seq1} = $seqrange; $record->{seq2} = $seqrange; } $record->{head} = $head; $record->{linenum} = $linenum; push @struct, $record; # Add the record to a global array; } sub BuildSeq { my($line) = @_; my($seqrange, $filerange, $pagerange) = $line =~ /^\s*([^\s]+)\s+([^\s]+)\s*([^\s]+)?\s*$/; my($record) = {}; $record->{seqrange} = $seqrange; $record->{filerange} = $filerange; $record->{pagerange} = $pagerange; if ($seqrange =~ /([^-]+)-(.*)/) { $record->{seq1} = $1; $record->{seq2} = $2; } else { $record->{seq1} = $seqrange; $record->{seq2} = $seqrange; } if ($filerange =~ /([^-]+)-(.*)/) { my $filename1 = $1; my $filename2 = $2; if ($filename1 =~ /(\..*)$/) { $record->{file1} = $`; $FileExt = $1; } else { $record->{file1} = $filename1; } if ($filename2 =~ /(\..*)$/) { $record->{file2} = $`; } else { $record->{file2} = $filename2; } } else { if ($filerange =~ /(\..*)$/) { $record->{file1} = $`; $record->{file2} = $`; $FileExt = $1; } else { $record->{file1} = $filerange; $record->{file2} = $filerange; } } if ($pagerange =~ /([^-]+)-(.*)/) { $record->{page1} = $1; $record->{page2} = $2; } else { $record->{page1} = $pagerange; $record->{page2} = $pagerange; } $record->{linenum} = $linenum; push @seq, $record; } ##-------------------------------------------------------------------## ## Update Structure ## ##-------------------------------------------------------------------## ## This subroutine studies the sequence of div types in the @struct ## ## array in an attempt to decide where the front, body, and back of ## ## the document begin and end. It will also intelligently order some ## ## elements hierarchically, e.g., a chapter following a part is ## ## assumed to fall *within* the part. ## ##-------------------------------------------------------------------## sub UpdateStruct { my($front, $body, $back, $lasttype, $type, $lastdocpart, $docpart); my($implied0, $implied1, $implied2); foreach $rec (@struct) { $lasttype = $type; $type = $rec->{type}; unless ($rec->{docpart}) { if (grep /$rec->{type}/i, @bodytrigger) { $body = 1; $rec->{docpart} = "body"; } elsif (grep /$rec->{type}/i, @backtrigger) { $body = 0; $back = 1; $rec->{docpart} = "back"; } elsif ($rec->{type} =~ /^intro/i && !$body) { foreach $seq (@seq) { if ($seq->{seq1} <= $rec->{seq1} && $rec->{seq1} <= $seq->{seq2}) { if ($seq->{pagerange} =~ /^[-ivxlcdm]+$/i) { $rec->{docpart} = "front"; $frontexists = 1; } elsif ($seq->{pagerange} =~ /^[-0-9]+$/) { $body = 1; $rec->{docpart} = "body"; } else { $rec->{docpart} = "front"; $frontexists = 1; } } } } elsif (!$body && !$back) { $rec->{docpart} = "front"; $frontexists = 1; } elsif ($back) { $rec->{docpart} = "back"; } else { $rec->{docpart} = "body"; } } $lastdocpart = $docpart; $docpart = $rec->{docpart}; unless ($rec->{explicit}) { if (grep /$type/i, @implied0) { $implied0 = 1; $implied1 = 0; $implied2 = 0; } elsif (grep /$type/i, @implied1) { $implied1 = 1; $implied2 = 0; if ($implied0) { $rec->{nestlevel} = 1; } } elsif (grep /$type/i, @implied2) { $implied2 = 1; if ($implied1) { if ($implied0) { $rec->{nestlevel} = 2; } else { $rec->{nestlevel} = 1; } } } else { $implied0 = 0; $implied1 = 0; $implied2 = 0; } } } } ##-------------------------------------------------------------------## ## Validate Sequence ## ##-------------------------------------------------------------------## ## This subroutine loops through the @seq array checking each record ## ## to see if is valid. It checks the following: ## ## 1. Sequence numbers are purely numeric and never decrease. ## ## If a range is given, the second sequence number of the ## ## range is never smaller than the first. There are no gaps, ## ## i.e., every sequence number is accounted for. ## ## 2. Filenames may "decrease", but if a range is given, the ## ## second filename may not be "smaller" than the first. Also, ## ## if a range is given, there must be a recognizable numeric ## ## component to the filename that increases by one. ## ## 3. Page numbers may decrease, but if a range is given, the ## ## second page number may not be smaller than the first. ## ## Page numbers must be either purely numeric or roman numerals## ## when part of a range. ## ## 4. Each of the three ranges in a record must be equal. ## ##-------------------------------------------------------------------## sub ValidateSeq { # print "Validating Sequence ..."; my($lastseq, $seq2, $lastrange, $seqrange, $linenum, $filenum1, $filenum2, $page1, $page2); foreach $rec (@seq) { $lastrange = $seqrange; $seqrange = $rec->{seqrange}; $lastseq = $seq2; $seq2 = $rec->{seq2}; $linenum = $rec->{linenum}; unless ($rec->{seq1} =~ /^\d+$/ && $rec->{seq2} =~ /^\d+$/) { print "Error line $linenum: \"$seqrange\" -- Sequence numbers must be numeric.\n"; $seqerror = 1; $nonseqnumerror = 1; $valid = 0; } if ($rec->{seq2} < $rec->{seq1}) { print "Error line $linenum: \"$seqrange\" -- Sequence range must be positive.\n" unless $nonseqnumerror; $seqerror = 1; $valid = 0; } if ($rec->{seq1} <= $lastseq) { print "Error lines ", $linenum-1, " & $linenum: \"$lastrange\n"; print " $seqrange\" -- Sequence ranges may not overlap.\n" unless $nonseqnumerror; $seqerror = 1; $valid = 0; } if ($rec->{seq1} > $lastseq + 1) { my($missing) = $rec->{seq1} - $lastseq; print "Error lines ", $linenum-1, " & $linenum: \"$lastrange\n" unless $nonseqnumerror; print " $seqrange\" -- " unless $nonseqnumerror; if ($missing == 2) { print "Sequence number ", $lastseq + 1, " missing.\n" unless $nonseqnumerror; } else { print "Sequence numbers ", $lastseq + 1, "-", $rec->{seq1} - 1, " missing.\n" unless $nonseqnumerror; } $seqerror = 1; $valid = 0; } if ($rec->{file1} =~ /(\d+)[a-zA-Z]$/) { $rec->{file1} =~ /(\d+)[a-zA-Z]$/ && ($filenum1 = $1); $rec->{file2} =~ /(\d+)[a-zA-Z]$/ && ($filenum2 = $1); } elsif ($rec->{file1} =~ /(\d+)$/) { $rec->{file1} =~ /(\d+)$/ && ($filenum1 = $1); $rec->{file2} =~ /(\d+)$/ && ($filenum2 = $1); } unless ($filenum2 - $filenum1 == $rec->{seq2} - $rec->{seq1}) { print "Error line $linenum: \"$rec->{filerange}\" -- File range does not equal sequence range.\n"; $seqerror = 1; $valid = 0; } if ($rec->{pagerange} =~ /^[-0-9]+$/) { $page1 = $rec->{page1}; $page2 = $rec->{page1}; } elsif ($rec->{pagerange} =~ /^[-ivxlcdm]+/i) { } elsif ($rec->{pagerange}) { print "Error line $linenum: \"$rec->{pagerange}\" -- Unrecognized paging scheme.\n"; $seqerror = 1; $valid = 0; } unless ($page2 - $page1 == $rec->{seq1} - $rec->{seq1}) { print "Error line $linenum: \"$rec->{pagerange}\" -- Page range does not equal sequence range.\n"; $seqerror = 1; $valid = 0; print "\$page1 = \"$page1\"\n"; print "\$page2 = \"$page2\"\n"; print "\$rec->{seq1} = \"$rec->{seq1}\"\n"; print "\$rec->{seq2} = \"$rec->{seq2}\"\n"; } } } ##-------------------------------------------------------------------## ## Validate Structure ## ##-------------------------------------------------------------------## ## This subroutine loops through the @struct array checking a ## ## few different things to make sure each record is valid, e.g., ## ## 1. Nest levels, if given, increment by no more than 1. ## ## Some types cannot have a nestlevel of anything other than ## ## 0, e.g., a titlepage or cover. ## ## 2. The struct type is valid according to the &Types ## ## subroutine provided at the end of this program. ## ## 3. Sequence numbers are purely numeric and never decrease. ## ## If a range is given, the second sequence number of the ## ## range is never smaller than the first. No sequence ## ## numbers are larger than the largest sequence number ## ## specified in the @seq array. ## ##-------------------------------------------------------------------## sub ValidateStruct { # print "Validating Structure ..."; my($lastnest, $nestlevel, $linenum); foreach $rec (@struct) { $lastnest = $nestlevel; $nestlevel = $rec->{nestlevel}; $linenum = $rec->{linenum}; if ($nestlevel - $lastnest > 1) { print "Error line $linenum: \"$rec->{nestlevel}\" -- Nesting level may not increase by more than 1.\n"; $valid = 0; } if ($nestlevel > 0 && $rec->{type} =~ /($nest0)/i) { print "Error line $linenum: \"$rec->{nestlevel} $rec->{type}\" -- \u$rec->{type} may not have a nesting level other than 0.\n"; $valid = 0; } if ($rec->{docpart} eq "front" && !grep /^$rec->{type}$/i, @front) { print "Error line $linenum: \"$rec->{type}\" -- \u$rec->{type} not allowed in front matter.\n"; $valid = 0; } if ($rec->{docpart} eq "body" && !grep /^$rec->{type}$/i, @body) { print "Error line $linenum: \"$rec->{type}\" -- \u$rec->{type} not allowed in document body.\n"; $valid = 0; } if ($rec->{docpart} eq "back" && !grep /^$rec->{type}$/i, @back) { print "Error line $linenum: \"$rec->{type}\" -- \u$rec->{type} not allowed in back matter.\n"; $valid = 0; } unless ($rec->{seq1} =~ /^\d+$/ && $rec->{seq2} =~ /^\d+$/) { print "Error line $linenum: \"$rec->{seqrange}\" -- Sequence numbers must be numeric.\n"; $valid = 0; } if ($rec->{seq2} < $rec->{seq1}) { print "Error line $linenum: \"$rec->{seqrange}\" -- Sequence range must be positive.\n" unless $nonseqnumerror; $valid = 0; } if ($rec->{seq2} > $seq[$#seq]{seq2} && !$seqerror) { print "Error line $linenum: \"$rec->{seqrange}\" -- Sequence number $rec->{seq2} too high. Max. value is $seq[$#seq]{seq2}.\n"; $valid = 0; } } } sub GenerateSGML { open (OUTFILE, ">$outfile") || die "Couldn't write to $outfile: $!\nAaaaaaaaaaaaaaaaaaah!\n[Crash and burn]\n"; open (ENTFILE, ">$entfile") || die "Couldn't write to $entfile: $!\nAaaaaaaaaaaaaaaaaaah!\n[Crash and burn]\n"; &PrintEntfileHeader; &PrintDeclaration; &PrintHeader; &PrintDocuments; } sub PrintEntfileHeader { print ENTFILE "\n\n"; } sub PrintDeclaration { print OUTFILE< \%$rootname; ]> EndDeclaration } sub PrintHeader { print OUTFILE "\n\n"; print OUTFILE "$IDNum\n" if $IDNum; print OUTFILE "\n\n$Title\n"; print OUTFILE "$Subtitle\n" if $Subtitle; print OUTFILE "$Author\n" if $Author; print OUTFILE "\n"; print OUTFILE "$Edition\n" if $Edition; if ($Pubplace || $Publisher || $Pubyear) { print OUTFILE "\n"; print OUTFILE "$Pubplace\n" if $Pubplace; print OUTFILE "$Publisher\n" if $Publisher; print OUTFILE "$Pubyear\n" if $Pubyear; print OUTFILE "\n"; } print OUTFILE "\n$Series\n\n" if $Series; print OUTFILE "\n\n"; } sub PrintDocuments { my($docpart, $lastdocpart, $nestlevel, $lastnestlevel, $divflag, $entityref, $nativeno); my($digit, $letter, $pad, $prefix, $suffix, $romanlowercase, $romanuppercase, $arabic, $roman); my $structcounter = 0; print OUTFILE "<$struct[0]->{docpart}>\n"; foreach $seq (@seq) { if ($seq->{page1} =~ /^[0-9]+$/) { $romanuppercase = 0; $romanlowercase = 0; } elsif ($seq->{page1} =~ /^[IVXLCDM]+/) { $romanuppercase = 1; $romanlowercase = 0; } elsif ($seq->{page1} =~ /^[ivxlcdm]+/i) { $romanlowercase = 1; $romanuppercase = 0; } $nativeno = $seq->{page1}; if ($seq->{file1} =~ /(\d+)([a-zA-Z])$/ && ($digit = $1, $letter = $2, $prefix = $`)) { } elsif ($seq->{file1} =~ /(\d+)$/ && ($digit = $1, $prefix = $`)) { if ($digit =~ /^0\d+$/ && ($pad = length($digit))) { $suffix = sprintf("%0${pad}d", $digit); } else { $suffix = $digit; } } for ($i = $seq->{seq1}; $i <= $seq->{seq2}; $i++) { while ($i == $struct[$structcounter]->{seq1}) { $lastnestlevel = $nestlevel; $nestlevel = $struct[$structcounter]->{nestlevel}; $lastdocpart = $docpart; $docpart = $struct[$structcounter]->{docpart}; if ($struct[$structcounter]->{type} =~ /^cover/i) { print OUTFILE "\n"; } else { if ($nestlevel == 0 && $divflag) { if ($lastnestlevel == 5) { print OUTFILE "\n"; } elsif ($lastnestlevel == 4) { print OUTFILE "\n"; } elsif ($lastnestlevel == 3) { print OUTFILE "\n"; } elsif ($lastnestlevel == 2) { print OUTFILE "\n"; } elsif ($lastnestlevel == 1) { print OUTFILE "\n"; } elsif ($lastnestlevel == 0) { print OUTFILE "\n"; } else { } } elsif ($nestlevel == 1) { if ($lastnestlevel == 5) { print OUTFILE "\n"; } elsif ($lastnestlevel == 4) { print OUTFILE "\n"; } elsif ($lastnestlevel == 3) { print OUTFILE "\n"; } elsif ($lastnestlevel == 2) { print OUTFILE "\n"; } elsif ($lastnestlevel == 1) { print OUTFILE "\n"; } else { } } elsif ($nestlevel == 2) { if ($lastnestlevel == 5) { print OUTFILE "\n"; } elsif ($lastnestlevel == 4) { print OUTFILE "\n"; } elsif ($lastnestlevel == 3) { print OUTFILE "\n"; } elsif ($lastnestlevel == 2) { print OUTFILE "\n"; } else { } } elsif ($nestlevel == 3) { if ($lastnestlevel == 5) { print OUTFILE "\n"; } elsif ($lastnestlevel == 4) { print OUTFILE "\n"; } elsif ($lastnestlevel == 3) { print OUTFILE "\n"; } else { } } elsif ($nestlevel == 4) { if ($lastnestlevel == 5) { print OUTFILE "\n"; } elsif ($lastnestlevel == 4) { print OUTFILE "\n"; } else { } } elsif ($nestlevel == 5) { if ($lastnestlevel == 5) { print OUTFILE "\n"; } else { } } $divflag = 1; if ($lastdocpart ne $docpart && $docpart ne "front") { print OUTFILE "\n<$docpart>\n"; } print OUTFILE "{nestlevel} type=\"$types{$struct[$structcounter]->{type}}\">\n"; print OUTFILE "$struct[$structcounter]->{head}\n" if $struct[$structcounter]->{head}; } $structcounter++; } # end while $entityref = $prefix.$suffix; &PrintEntities($entityref); print OUTFILE "\n"; $suffix++; if ($seq->{page1}) { if ($romanlowercase) { $arabic = &arabic($nativeno); $nativeno = &roman(++$arabic); } elsif ($romanuppercase) { $arabic = &arabic($nativeno); $nativeno = uc(&roman(++$arabic)); } else { $nativeno++; } } } # end for $seq1..$seq2 } # end foreach @seq if ($nestlevel == 5) { print OUTFILE "\n"; } elsif ($nestlevel == 4) { print OUTFILE "\n"; } elsif ($nestlevel == 3) { print OUTFILE "\n"; } elsif ($nestlevel == 2) { print OUTFILE "\n"; } elsif ($nestlevel == 1) { print OUTFILE "\n"; } elsif ($nestlevel == 0) { print OUTFILE "\n"; } print OUTFILE "\n\n"; } sub PrintEntities { my($entityref) = @_; my($notation); print ENTFILE "\n"; } sub PrintOutput { my($docpart, $lastdocpart); foreach $struct (@struct) { $lastdocpart = $docpart; $docpart = $struct->{docpart}; if ($docpart ne $lastdocpart) { print "FRONT\n" if $docpart eq "front"; print "\nBODY\n" if $docpart eq "body"; print "\nBACK\n" if $docpart eq "back"; } print " " x ($struct->{nestlevel} + 1); print "$struct->{type}\n"; } } sub Types { %types = ('cover' => 'cover', 'titlepage' => 'titlepage', 'tp' => 'titlepage', 'toc' => 'contents', 'contents' => 'contents', 'acknowledgements' => 'acknowledgements', 'ack' => 'acknowledgements', 'advertisements' => 'advertisements', 'advertisement' => 'advertisements', 'ads' => 'advertisements', 'dedication' => 'dedication', 'errata' => 'errata', 'portrait' => 'portrait', 'introduction' => 'introduction', 'intro' => 'introduction', 'section' => 'section', 'subsection' => 'subsection', 'plates' => 'plates', 'foreword' => 'foreword', 'notes' => 'notes', 'volume' => 'volume', 'part' => 'part', 'chapter' => 'chapter', 'article' => 'article', 'index' => 'index', 'entry' => 'entry', 'bibliography' => 'bibliography', 'bib' => 'bibliography', 'bibl' => 'bibliography'); @front = ('cover', 'titlepage', 'tp', 'toc', 'contents', 'acknowledgements', 'ack', 'advertisements', 'advertisement', 'ads', 'dedication', 'errata', 'portrait', 'introduction', 'intro', 'section', 'subsection', 'plates', 'foreword', 'notes'); @body = ('book', 'volume', 'part', 'chapter', 'entry', 'errata', 'introduction', 'intro', 'section', 'subsection', 'bibliography', 'plates', 'index', 'advertisements', 'advertisement', 'ads', 'article', 'notes'); @back = ('bibliography', 'index', 'advertisements', 'advertisement', 'ads', 'toc', 'contents', 'acknowledgements', 'ack', 'dedication', 'errata', 'notes', 'plates', 'portrait'); $nest0 = "tp|titlepage|cover"; @fronttrigger = ('tp', 'titlepage', 'cover', 'foreword', 'toc', 'acknowledgements', 'ack', 'dedication', 'contents'); @bodytrigger = ('book', 'volume', 'part', 'chapter', 'article', 'entry'); @backtrigger = ('bibliography', 'index', 'advertisements', 'advertisement', 'ads', 'toc', 'contents', 'acknowledgements', 'ack', 'dedication', 'notes'); @implied0 = ('book', 'volume', 'part'); @implied1 = ('chapter', 'intro', 'introduction', 'foreword', 'acknowledgements', 'ack', 'dedication', 'notes', 'bibliography', 'index', 'article'); @implied2 = ('section', 'entry', 'article'); @implied3 = ('subsection'); } ##===========================================================## ## I can't get my win16 version of perl to import the roman ## ## numeral manipulation module roman.pm so I am including ## ## the functions here inside the program instead. ## ##===========================================================## ##======================================================================## ## DESCRIPTION: ## ## This package provides some functions which help conversion of ## ## numeric notation between Roman and Arabic. ## ## AUTHOR: ## ## OZAWA Sakuro ## ## COPYRIGHT: ## ## Copyright (c) 1995 OZAWA Sakuro. All rights reserved. This ## ## program is free software; you can redistribute it and/or modify ## ## it under the same terms as Perl itself. ## ##======================================================================## sub isroman { my($arg) = shift; $arg ne '' and $arg =~ /^(?: M{0,3}) (?: D?C{0,3} | C[DM]) (?: L?X{0,3} | X[LC]) (?: V?I{0,3} | I[VX])$/ix; } sub arabic { my(%roman2arabic) = qw(I 1 V 5 X 10 L 50 C 100 D 500 M 1000); my($arg) = shift; isroman $arg or return undef; my($last_digit) = 1000; my($arabic); foreach (split(//, uc $arg)) { my($digit) = $roman2arabic{$_}; $arabic -= 2 * $last_digit if $last_digit < $digit; $arabic += ($last_digit = $digit); } $arabic; } sub romanstart { %roman_digit = qw(1 IV 10 XL 100 CD 1000 MMMMMM); @figure = reverse sort keys %roman_digit; grep($roman_digit{$_} = [split(//, $roman_digit{$_}, 2)], @figure); } sub Roman { my($arg) = shift; 0 < $arg and $arg < 4000 or return undef; my($x, $roman); foreach (@figure) { my($digit, $i, $v) = (int($arg / $_), @{$roman_digit{$_}}); if (1 <= $digit and $digit <= 3) { $roman .= $i x $digit; } elsif ($digit == 4) { $roman .= "$i$v"; } elsif ($digit == 5) { $roman .= $v; } elsif (6 <= $digit and $digit <= 8) { $roman .= $v . $i x ($digit - 5); } elsif ($digit == 9) { $roman .= "$i$x"; } $arg -= $digit * $_; $x = $i; } $roman; } sub roman { lc Roman shift; }