Verify Ant Task...Take Two

/*
 * Copyright (c) 2009 QOS.ch All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS  IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package ch.qos.cal10n.ant;

import org.apache.tools.ant.Task;

import java.util.LinkedList;
import java.util.List;

public class EnumTypesElement extends Task {
    private List<StringElement> enumTypes;

    public EnumTypesElement() {
        this.enumTypes = new LinkedList<StringElement>();
    }

    public void addEnumType(StringElement enumType) {
        this.enumTypes.add(enumType);
    }

    public List<StringElement> getEnumTypes() {
        return this.enumTypes;
    }
}

/*
 * Copyright (c) 2009 QOS.ch All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS  IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package ch.qos.cal10n.ant;

import org.apache.tools.ant.Task;

public class StringElement extends Task {
    private String str;

    public StringElement() {}

    public StringElement(String str) {
        this.str = str;
    }

    public void addText(String str) {
        this.str = this.getProject().replaceProperties(str);
    }

    public String getText() {
        return this.str;
    }

    @Override
    public String toString() {
        return this.str;
    }
}

/*
 * Copyright (c) 2009 QOS.ch All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS  IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package ch.qos.cal10n.ant;

import ch.qos.cal10n.CAL10NConstants;
import ch.qos.cal10n.verifier.IMessageKeyVerifier;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.types.LogLevel;
import org.apache.tools.ant.types.Path;
import org.apache.tools.ant.types.Reference;
import org.apache.tools.ant.util.ClasspathUtils;

import java.lang.reflect.Constructor;
import java.text.MessageFormat;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;

public class VerifyTask extends Task {
    private List<StringElement> enumTypes;
    private Path classpath;

    @Override
    public void init() throws BuildException {
        this.enumTypes = new LinkedList<StringElement>();
    }

    @Override
    public void execute() throws BuildException {
      if (this.enumTypes.isEmpty()) {
        throw new BuildException(CAL10NConstants.MISSING_ENUM_TYPES_MSG);
      }
      for (StringElement enumType : this.enumTypes) {
        IMessageKeyVerifier imcv = getMessageKeyVerifierInstance(enumType.getText());
        log("Checking all resource bundles for enum type [" + enumType + "]", LogLevel.INFO.getLevel());
        checkAllLocales(imcv);
      }
    }

    public void checkAllLocales(IMessageKeyVerifier mcv) {
        String enumClassAsStr = mcv.getEnumTypeAsStr();
        String[] localeNameArray = mcv.getLocaleNames();

        if (localeNameArray == null || localeNameArray.length == 0) {
            String errMsg = MessageFormat.format(CAL10NConstants.MISSING_LOCALE_DATA_ANNOTATION_MESSAGE, enumClassAsStr);
            log(errMsg, LogLevel.ERR.getLevel());
            throw new BuildException(errMsg);
        }

        boolean failure = false;
        for (String localeName : localeNameArray) {
            Locale locale = new Locale(localeName);
            List<String> errorList = mcv.typeIsolatedVerify(locale);
            if (errorList.size() == 0) {
                String resourceBundleName = mcv.getBaseName();
                log("SUCCESSFUL verification for resource bundle [" + resourceBundleName + "] for locale [" + locale + "]", LogLevel.INFO.getLevel());
            } else {
                failure = true;
                log("FAILURE during verification of resource bundle for locale ["
                        + locale + "] enum class [" + enumClassAsStr + "]", LogLevel.ERR.getLevel());
                for (String error : errorList) {
                    log(error, LogLevel.ERR.getLevel());
                }
            }
        }
        if (failure) {
            throw new BuildException("FAIL Verification of [" + enumClassAsStr + "] keys.");
        }
    }

    IMessageKeyVerifier getMessageKeyVerifierInstance(String enumClassAsStr) {
        String errMsg = "Failed to instantiate MessageKeyVerifier class";
        try {
            ClassLoader classLoader = ClasspathUtils.getClassLoaderForPath(this.getProject(), this.classpath, "cal10n.VerifyTask");
            Class<?> mkvClass = Class.forName(
                    CAL10NConstants.MessageKeyVerifier_FQCN, true, classLoader);
            Constructor<?> mkvCons = mkvClass.getConstructor(String.class);
            return (IMessageKeyVerifier) mkvCons.newInstance(enumClassAsStr);
        } catch (ClassNotFoundException e) {
            throw new BuildException(errMsg, e);
        } catch (NoClassDefFoundError e) {
            throw new BuildException(errMsg, e);
        } catch (Exception e) {
            throw new BuildException(errMsg, e);
        }
    }

    public void addClasspath(Path classpath) {
        this.classpath = classpath;
    }

    public void addConfiguredEnumTypes(EnumTypesElement enumTypes) {
        this.enumTypes.addAll(enumTypes.getEnumTypes());
    }

    public void setClasspath(Path classpath) {
        this.classpath = classpath;
    }

    public void setClasspathRef(Reference refId) {
        Path cp = new Path(this.getProject());
        cp.setRefid(refId);
        this.setClasspath(cp);
    }

    public void setEnumType(String enumType) {
        this.enumTypes.add(new StringElement(enumType));
    }
}

/*
 * Copyright (c) 2009 QOS.ch All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS  IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package ch.qos.cal10n.ant;

import junit.framework.TestSuite;
import org.apache.ant.antunit.junit3.AntUnitSuite;
import org.apache.ant.antunit.junit4.AntUnitSuiteRunner;
import org.junit.runner.RunWith;

import java.io.File;
import java.io.UnsupportedEncodingException;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLDecoder;

@RunWith(AntUnitSuiteRunner.class)
public class VerifyTaskTest {
    public static TestSuite suite() throws URISyntaxException {
        setProperties();
        URL resource = VerifyTask.class.getResource("/ch/qos/cal10n/ant/VerifyTaskTest.xml");
        File file = new File(resource.toURI());
        return new AntUnitSuite(file, VerifyTask.class);
    }

    /**
     * Set system properties for use in the AntUnit files.
     */
    private static void setProperties() {
        String name = VerifyTask.class.getName();
        final String resourceName = "/" + name.replace('.', '/') + ".class";
        String absoluteFilePath = VerifyTask.class.getResource(resourceName).getFile();
        try {
            absoluteFilePath = URLDecoder.decode(absoluteFilePath, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("Missing UTF-8 encoding in JVM.", e);
        }
        String classesDir = absoluteFilePath.substring(0, absoluteFilePath.length() - resourceName.length());
        System.setProperty("ch.qos.cal10n.ant.VerifyTaskTest.classes.dir", classesDir);
    }
}

/*
 * Copyright (c) 2009 QOS.ch All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS  IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package ch.qos.cal10n.ant.testdata;

import ch.qos.cal10n.BaseName;
import ch.qos.cal10n.Locale;
import ch.qos.cal10n.LocaleData;

@BaseName("colors")
@LocaleData({
        @Locale("en"),
        @Locale("fr")
})
public enum Colors {
    RED,
    BLUE
}

package ch.qos.cal10n.ant.testdata;

import ch.qos.cal10n.BaseName;
import ch.qos.cal10n.Locale;
import ch.qos.cal10n.LocaleData;

@BaseName("daysoftheweek")
@LocaleData({
        @Locale("en"),
        @Locale("fr")
})
public enum DaysOfTheWeek {
    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY,
    SUNDAY
}


Update readme and config

class Compile
  attr_accessor :token, :user,
                :root_resource_path, :resources,
                :compiler, :output_format
  attr_reader   :output_files, :log_files, :unique_id,
                :status, :error_type, :error_message,
                :bibtex_ran, :makeindex_ran

  POSSIBLE_COMPILER_OUTPUT_FORMATS = {
    :pdflatex => ['pdf'],
    :latex    => ['dvi', 'pdf', 'ps'],
    :xelatex  => ['pdf']
  }

  def compiler
    @compiler ||= 'pdflatex'
  end

  def output_format
    @output_format ||= 'pdf'
  end

  def initialize(attributes = {})
    self.root_resource_path = attributes[:root_resource_path] || "main.tex"
    self.token = attributes[:token]

    self.compiler = attributes[:compiler]
    self.output_format = attributes[:output_format]

    self.resources = []
    for resource in attributes[:resources].to_a
      self.resources << Resource.new(
        resource[:path],
        resource[:modified_date],
        resource[:content],
        resource[:url],
        self
      )
    end

    @output_files = []
    @log_files = []
    @status = :unprocessed
    @bibtex_ran = false
    @makeindex_ran = false
  end

  def compile
    @start_time = Time.now
    validate_compile
    write_resources_to_disk
    do_compile
    convert_to_output_format
    move_compiled_files_to_public_dir
    @status = :success
  rescue CLSI::CompileError => e
    @status = :failure
    @error_type = e.class.name.demodulize
    @error_message = e.message
  ensure
    move_log_files_to_public_dir
    write_response_to_public_dir
    remove_compile_directory unless PRESERVE_COMPILE_DIRECTORIES
    record_in_compile_log
  end

  def validate_compile
    if self.user.blank?
      self.user = User.find_by_token(self.token)
      raise CLSI::InvalidToken, 'user does not exist' if self.user.nil?
    end

    unless POSSIBLE_COMPILER_OUTPUT_FORMATS.has_key?(self.compiler.to_sym)
      raise CLSI::UnknownCompiler, "#{self.compiler} is not a valid compiler"
    end

    unless POSSIBLE_COMPILER_OUTPUT_FORMATS[self.compiler.to_sym].include?(self.output_format)
      raise CLSI::ImpossibleOutputFormat, "#{self.compiler} cannot produce #{self.output_format} output"
    end
  end

  def unique_id
    @unique_id ||= generate_unique_string
  end

  def compile_directory
    @compile_directory ||= File.join(LATEX_COMPILE_DIR, self.unique_id)
  end

  def to_xml
    xml = Builder::XmlMarkup.new
    xml.instruct!

    xml.compile do
      xml.compile_id(self.unique_id)

      if self.status == :failure
        xml.status('failure')
        xml.error do
          xml.type self.error_type
          xml.message self.error_message
        end
      else
        xml.status(self.status.to_s)
      end

      unless self.output_files.empty?
        xml.output do
          for file in self.output_files
            xml.file(:url => file.url, :type => file.type, :mimetype => file.mimetype)
          end
        end
      end

      unless self.log_files.empty?
        xml.logs do
          for file in self.log_files
            xml.file(:url => file.url, :type => file.type, :mimetype => file.mimetype)
          end
        end
      end
    end
  end

  def to_json
    hash = {
      'compile_id' => self.unique_id,
      'status'     => self.status.to_s
    }

    if self.status == :failure
      hash['error'] = {
        'type' => self.error_type,
        'message' => self.error_message
      }
    end

    unless self.output_files.empty?
      hash['output_files'] = self.output_files.collect{|of| {
        'url'      => of.url,
        'mimetype' => of.mimetype,
        'type'     => of.type
      }}
    end

    unless self.log_files.empty?
      hash['logs'] = self.log_files.collect{|lf| {
        'url'      => lf.url,
        'mimetype' => lf.mimetype,
        'type'     => lf.type
      }}
    end

    return ({'compile' => hash}).to_json
  end

private

  def write_resources_to_disk
    File.umask(0002)

    for resource in self.resources.to_a
      resource.write_to_disk
    end
  end

  def do_compile
    run_compiler

    aux_file_content = read_aux_files
    if aux_file_content.include? '\\citation' or aux_file_content.include? '\\bibdata' or aux_file_content.include? '\\bibstyle'
      modify_aux_files
      run_bibtex
      run_latex_again = true
      @bibtex_ran = true
    end

    if File.exist?(File.join(compile_directory, 'output.idx'))
      run_makeindex
      run_latex_again = true
      @makeindex_ran = true
    end

    if File.exist?(File.join(compile_directory, 'output.toc'))
      # We have a table of contents that needs to be included
      run_latex_again = true
    end

    if log_complains_about_references? or run_latex_again
      run_compiler
    end

    if log_complains_about_references?
      run_compiler
    end

    if log_complains_about_references?
      run_compiler
    end
  end

  def log_complains_about_references?
    log_content = read_log
    log_content.include?('There were undefined references') ||
    log_content.include?('There were undefined citations') ||
    log_content.include?('LaTeX Warning: Label(s) may have changed. Rerun to get cross-references right.') ||
    log_content.include?('LaTeX Warning: Citation') || # Natbib
    log_content.include?('No file output.toc') ||
    log_content.include?('Rerun LaTeX') # The longtables package
  end

  def run_bibtex
    bibtex_command = ['env', tex_env_variables, BIBTEX_COMMAND, "#{compile_directory_rel_to_chroot}/output"].flatten
    run_with_timeout(bibtex_command, BIBTEX_TIMEOUT)
  end

  def run_makeindex
    makeindex_command = [
      MAKEINDEX_COMMAND,
      '-o', "#{compile_directory_rel_to_chroot}/output.ind",
      "#{compile_directory_rel_to_chroot}/output.idx"
    ]
    run_with_timeout(makeindex_command, COMPILE_TIMEOUT)
  end

  def run_compiler
    run_with_timeout(compile_command, COMPILE_TIMEOUT)
  end

  def read_aux_files
    aux_file_paths = Dir.entries(self.compile_directory).reject{|e| not e.match(/\.aux$/)}
    aux_file_paths.collect!{|p| File.join(self.compile_directory, p)}
    return aux_file_paths.collect{|p| File.read(p)}.join("\n")
  end

  def modify_aux_files
    aux_file_names = Dir.entries(self.compile_directory).reject{|e| not e.match(/\.aux$/)}
    aux_file_paths = aux_file_names.collect{|n| File.join(self.compile_directory, n)}
    for aux_file in aux_file_paths
      content = File.read(aux_file)
      content.gsub!(/^\\@input\{(.*)\}$/, "\\@input{#{compile_directory_rel_to_chroot}/\\1}")
      File.open(aux_file, 'w') {|f|
        f.write(content)
      }
    end
  end

  def read_log
    log_file_path = File.join(self.compile_directory, 'output.log')
    return '' unless File.exist?(log_file_path)
    File.read(log_file_path)
  end

  def convert_to_output_format
    case self.compiler
    when 'pdflatex'
      input_format = 'pdf'
    when 'latex'
      input_format = 'dvi'
    when 'xelatex'
      input_format = 'pdf'
    end
    ensure_output_files_exist(input_format)
    conversion_method = "convert_#{input_format}_to_#{self.output_format}"
    if self.respond_to?(conversion_method, true)
      self.send(conversion_method)
    else
      raise CLSI::ImpossibleFormatConversion, "can't convert #{input_format} to #{self.output_format}"
    end
  end

  def move_compiled_files_to_public_dir
    FileUtils.mkdir_p(File.join(SERVER_PUBLIC_DIR, 'output', self.unique_id))

    for existing_file in find_output_files_of_type(self.output_format)
      existing_path = File.join(compile_directory, existing_file)
      relative_output_path = File.join(relative_output_dir, existing_file)
      output_path = File.join(SERVER_PUBLIC_DIR, relative_output_path)
      FileUtils.mv(existing_path, output_path)
      @output_files << OutputFile.new(:path => relative_output_path)
    end
  end

  def move_log_files_to_public_dir
    FileUtils.mkdir_p(output_dir)

    existing_log_path = File.join(compile_directory, 'output.log')
    relative_output_log_path = File.join(relative_output_dir, 'output.log')
    if File.exist?(existing_log_path)
      FileUtils.mv(existing_log_path, File.join(SERVER_PUBLIC_DIR, relative_output_log_path))
      @log_files << OutputFile.new(:path => relative_output_log_path)
    end
  end

  def remove_compile_directory
    FileUtils.rm_rf(self.compile_directory)
  end

  def write_response_to_public_dir
    FileUtils.mkdir_p(output_dir)
    File.open(File.join(output_dir, 'response.xml'), 'w') do |f|
      f.write(self.to_xml)
    end
    File.open(File.join(output_dir, 'response.json'), 'w') do |f|
      f.write(self.to_json)
    end
  end

  def record_in_compile_log
    CompileLog.create(
      :user => user,
      :time_taken => ((Time.now.to_f - @start_time.to_f) * 1000).to_i, # Time in milliseconds
      :bibtex_ran => @bibtex_ran,
      :makeindex_ran => @makeindex_ran
    )
  end

  def tex_env_variables
    root_and_relative_directories = [
      File.join(compile_directory_rel_to_chroot, File.dirname(self.root_resource_path)),
      compile_directory_rel_to_chroot
    ].join(":") + ":"
    return [
      "TEXMFOUTPUT=#{compile_directory_rel_to_chroot}",
      "TEXINPUTS=#{root_and_relative_directories}",
      "BIBINPUTS=#{root_and_relative_directories}",
      "BSTINPUTS=#{root_and_relative_directories}",
      "TEXFONTS=#{root_and_relative_directories}",
      "TFMFONTS=#{root_and_relative_directories}"
    ]
  end

  def output_dir
    File.join(SERVER_PUBLIC_DIR, relative_output_dir)
  end

  def relative_output_dir
    File.join('output', self.unique_id)
  end

  def compile_directory_rel_to_chroot
    @compile_directory_rel_to_chroot ||= File.join(LATEX_COMPILE_DIR_RELATIVE_TO_CHROOT, self.unique_id)
  end

  def compile_command
    case self.compiler
    when 'pdflatex'
      command = PDFLATEX_COMMAND
    when 'latex'
      command = LATEX_COMMAND
    when 'xelatex'
      command = XELATEX_COMMAND
    else
      raise NotImplemented # Previous checking means we should never get here!
    end
    return ["env"] + tex_env_variables + [command, "-interaction=batchmode",
            "-output-directory=#{compile_directory_rel_to_chroot}", "-no-shell-escape",
            "-jobname=output", self.root_resource_path]
  end

  # Returns a list of output files of the given type. Will raise a CLSI::NoOutputFile if no output
  # files of the given type exist.
  def find_output_files_of_type(type)
    file_name = "output.#{type}"
    output_path = File.join(compile_directory, file_name)
    raise CLSI::NoOutputProduced, 'no compiled documents were produced' unless File.exist?(output_path)
    return [file_name]
  end

  def ensure_output_files_exist(type)
    find_output_files_of_type(type)
  end

  def convert_pdf_to_pdf
    # Nothing to do!
  end

  def convert_dvi_to_dvi
    # Nothing to do!
  end

  def convert_dvi_to_pdf
    input = File.join(compile_directory_rel_to_chroot, 'output.dvi')
    output = File.join(compile_directory_rel_to_chroot, 'output.pdf')

    # Note: Adding &> /dev/null to this command makes run_with_timeout return straight away before
    # command is complete, and I have no idea why. Solution: Don't add it.
    dvipdf_command = "env TEXPICTS=#{compile_directory_rel_to_chroot} #{DVIPDF_COMMAND} \"#{input}\" \"#{output}\""
    run_with_timeout(dvipdf_command, DVIPDF_TIMEOUT)
  end

  def convert_dvi_to_ps
    input = File.join(compile_directory_rel_to_chroot, 'output.dvi')
    output = File.join(compile_directory_rel_to_chroot, 'output.ps')
    dvips_command = "env TEXPICTS=#{compile_directory_rel_to_chroot} #{DVIPS_COMMAND} -o \"#{output}\" \"#{input}\""
    run_with_timeout(dvips_command, DVIPS_TIMEOUT)
  end

  # Everything below here is copied from the mathwiki code. It was ugly when
  # I first wrote it and it hasn't improved with time.
  # Fixing it would be good.
  def run_with_timeout(command, timeout = 10)
    start_time = Time.now
    pid = fork {
      exec(*command)
    }
    while Time.now - start_time < timeout
      if Process.waitpid(pid, Process::WNOHANG)
        Rails.logger.info "(#{Time.now - start_time} seconds) #{command.to_a.join(' ')}"
        return pid
      end
      sleep 0.1 if (Time.now - start_time > 0.3) # No need to check too often if it's taking a while
    end

    # Process never finished
    kill_process(pid)
    raise CLSI::Timeout, "the compile took too long to run and was aborted"
  end

  def kill_process(pid)
    child_pids = %x[ps -e -o 'ppid pid' | awk '$1 == #{pid} { print $2 }'].split
    child_pids.collect{|cpid| kill_process(cpid.to_i)}
    Process.kill('INT', pid)
    Process.kill('HUP', pid)
    Process.kill('KILL', pid)
  end
end

class Compile
  attr_accessor :token, :user,
                :root_resource_path, :resources,
                :compiler, :output_format
  attr_reader   :output_files, :log_files, :unique_id,
                :status, :error_type, :error_message,
                :bibtex_ran, :makeindex_ran

  POSSIBLE_COMPILER_OUTPUT_FORMATS = {
    :pdflatex => ['pdf'],
    :latex    => ['dvi', 'pdf', 'ps'],
    :xelatex  => ['pdf']
  }

  def compiler
    @compiler ||= 'pdflatex'
  end

  def output_format
    @output_format ||= 'pdf'
  end

  def initialize(attributes = {})
    self.root_resource_path = attributes[:root_resource_path] || "main.tex"
    self.token = attributes[:token]

    self.compiler = attributes[:compiler]
    self.output_format = attributes[:output_format]

    self.resources = []
    for resource in attributes[:resources].to_a
      self.resources << Resource.new(
        resource[:path],
        resource[:modified_date],
        resource[:content],
        resource[:url],
        self
      )
    end

    @output_files = []
    @log_files = []
    @status = :unprocessed
    @bibtex_ran = false
    @makeindex_ran = false
  end

  def compile
    @start_time = Time.now
    validate_compile
    write_resources_to_disk
    do_compile
    convert_to_output_format
    move_compiled_files_to_public_dir
    @status = :success
  rescue CLSI::CompileError => e
    @status = :failure
    @error_type = e.class.name.demodulize
    @error_message = e.message
  ensure
    move_log_files_to_public_dir
    write_response_to_public_dir
    remove_compile_directory unless PRESERVE_COMPILE_DIRECTORIES
    record_in_compile_log
  end

  def validate_compile
    if self.user.blank?
      self.user = User.find_by_token(self.token)
      raise CLSI::InvalidToken, 'user does not exist' if self.user.nil?
    end

    unless POSSIBLE_COMPILER_OUTPUT_FORMATS.has_key?(self.compiler.to_sym)
      raise CLSI::UnknownCompiler, "#{self.compiler} is not a valid compiler"
    end

    unless POSSIBLE_COMPILER_OUTPUT_FORMATS[self.compiler.to_sym].include?(self.output_format)
      raise CLSI::ImpossibleOutputFormat, "#{self.compiler} cannot produce #{self.output_format} output"
    end
  end

  def unique_id
    @unique_id ||= generate_unique_string
  end

  def compile_directory
    @compile_directory ||= File.join(LATEX_COMPILE_DIR, self.unique_id)
  end

  def to_xml
    xml = Builder::XmlMarkup.new
    xml.instruct!

    xml.compile do
      xml.compile_id(self.unique_id)

      if self.status == :failure
        xml.status('failure')
        xml.error do
          xml.type self.error_type
          xml.message self.error_message
        end
      else
        xml.status(self.status.to_s)
      end

      unless self.output_files.empty?
        xml.output do
          for file in self.output_files
            xml.file(:url => file.url, :type => file.type, :mimetype => file.mimetype)
          end
        end
      end

      unless self.log_files.empty?
        xml.logs do
          for file in self.log_files
            xml.file(:url => file.url, :type => file.type, :mimetype => file.mimetype)
          end
        end
      end
    end
  end

  def to_json
    hash = {
      'compile_id' => self.unique_id,
      'status'     => self.status.to_s
    }

    if self.status == :failure
      hash['error'] = {
        'type' => self.error_type,
        'message' => self.error_message
      }
    end

    unless self.output_files.empty?
      hash['output_files'] = self.output_files.collect{|of| {
        'url'      => of.url,
        'mimetype' => of.mimetype,
        'type'     => of.type
      }}
    end

    unless self.log_files.empty?
      hash['logs'] = self.log_files.collect{|lf| {
        'url'      => lf.url,
        'mimetype' => lf.mimetype,
        'type'     => lf.type
      }}
    end

    return ({'compile' => hash}).to_json
  end

private

  def write_resources_to_disk
    File.umask(0002)

    for resource in self.resources.to_a
      resource.write_to_disk
    end
  end

  def do_compile
    run_compiler

    aux_file_content = read_aux_files
    if aux_file_content.include? '\\citation' or aux_file_content.include? '\\bibdata' or aux_file_content.include? '\\bibstyle'
      modify_aux_files
      run_bibtex
      run_latex_again = true
      @bibtex_ran = true
    end

    if File.exist?(File.join(compile_directory, 'output.idx'))
      run_makeindex
      run_latex_again = true
      @makeindex_ran = true
    end

    if File.exist?(File.join(compile_directory, 'output.toc'))
      # We have a table of contents that needs to be included
      run_latex_again = true
    end

    if log_complains_about_references? or run_latex_again
      run_compiler
    end

    if log_complains_about_references?
      run_compiler
    end

    if log_complains_about_references?
      run_compiler
    end
  end

  def log_complains_about_references?
    log_content = read_log
    log_content.include?('There were undefined references') ||
    log_content.include?('There were undefined citations') ||
    log_content.include?('LaTeX Warning: Label(s) may have changed. Rerun to get cross-references right.') ||
    log_content.include?('LaTeX Warning: Citation') || # Natbib
    log_content.include?('No file output.toc') ||
    log_content.include?('Rerun LaTeX') # The longtables package
  end

  def run_bibtex
    bibtex_command = ['env', tex_env_variables, BIBTEX_COMMAND, "#{compile_directory_rel_to_chroot}/output"].flatten
    run_with_timeout(bibtex_command, BIBTEX_TIMEOUT)
  end

  def run_makeindex
    makeindex_command = [
      MAKEINDEX_COMMAND,
      '-o', "#{compile_directory_rel_to_chroot}/output.ind",
      "#{compile_directory_rel_to_chroot}/output.idx"
    ]
    run_with_timeout(makeindex_command, COMPILE_TIMEOUT)
  end

  def run_compiler
    run_with_timeout(compile_command, COMPILE_TIMEOUT)
  end

  def read_aux_files
    aux_file_paths = Dir.entries(self.compile_directory).reject{|e| not e.match(/\.aux$/)}
    aux_file_paths.collect!{|p| File.join(self.compile_directory, p)}
    return aux_file_paths.collect{|p| File.read(p)}.join("\n")
  end

  def modify_aux_files
    aux_file_names = Dir.entries(self.compile_directory).reject{|e| not e.match(/\.aux$/)}
    aux_file_paths = aux_file_names.collect{|n| File.join(self.compile_directory, n)}
    for aux_file in aux_file_paths
      content = File.read(aux_file)
      content.gsub!(/^\\@input\{(.*)\}$/, "\\@input{#{compile_directory_rel_to_chroot}/\\1}")
      File.open(aux_file, 'w') {|f|
        f.write(content)
      }
    end
  end

  def read_log
    log_file_path = File.join(self.compile_directory, 'output.log')
    return '' unless File.exist?(log_file_path)
    File.read(log_file_path)
  end

  def convert_to_output_format
    case self.compiler
    when 'pdflatex'
      input_format = 'pdf'
    when 'latex'
      input_format = 'dvi'
    when 'xelatex'
      input_format = 'pdf'
    end
    ensure_output_files_exist(input_format)
    conversion_method = "convert_#{input_format}_to_#{self.output_format}"
    if self.respond_to?(conversion_method, true)
      self.send(conversion_method)
    else
      raise CLSI::ImpossibleFormatConversion, "can't convert #{input_format} to #{self.output_format}"
    end
  end

  def move_compiled_files_to_public_dir
    FileUtils.mkdir_p(File.join(SERVER_PUBLIC_DIR, 'output', self.unique_id))

    for existing_file in find_output_files_of_type(self.output_format)
      existing_path = File.join(compile_directory, existing_file)
      relative_output_path = File.join(relative_output_dir, existing_file)
      output_path = File.join(SERVER_PUBLIC_DIR, relative_output_path)
      FileUtils.mv(existing_path, output_path)
      @output_files << OutputFile.new(:path => relative_output_path)
    end
  end

  def move_log_files_to_public_dir
    FileUtils.mkdir_p(output_dir)

    existing_log_path = File.join(compile_directory, 'output.log')
    relative_output_log_path = File.join(relative_output_dir, 'output.log')
    if File.exist?(existing_log_path)
      FileUtils.mv(existing_log_path, File.join(SERVER_PUBLIC_DIR, relative_output_log_path))
      @log_files << OutputFile.new(:path => relative_output_log_path)
    end
  end

  def remove_compile_directory
    FileUtils.rm_rf(self.compile_directory)
  end

  def write_response_to_public_dir
    FileUtils.mkdir_p(output_dir)
    File.open(File.join(output_dir, 'response.xml'), 'w') do |f|
      f.write(self.to_xml)
    end
    File.open(File.join(output_dir, 'response.json'), 'w') do |f|
      f.write(self.to_json)
    end
  end

  def record_in_compile_log
    CompileLog.create(
      :user => user,
      :time_taken => ((Time.now.to_f - @start_time.to_f) * 1000).to_i, # Time in milliseconds
      :bibtex_ran => @bibtex_ran,
      :makeindex_ran => @makeindex_ran
    )
  end

  def tex_env_variables
    root_and_relative_directories = [
      File.join(compile_directory_rel_to_chroot, File.dirname(self.root_resource_path)),
      compile_directory_rel_to_chroot
    ].join(":") + ":"
    return [
      "TEXMFOUTPUT=#{compile_directory_rel_to_chroot}",
      "TEXINPUTS=#{root_and_relative_directories}",
      "BIBINPUTS=#{root_and_relative_directories}",
      "BSTINPUTS=#{root_and_relative_directories}",
      "TEXFONTS=#{root_and_relative_directories}",
      "TFMFONTS=#{root_and_relative_directories}"
    ]
  end

  def output_dir
    File.join(SERVER_PUBLIC_DIR, relative_output_dir)
  end

  def relative_output_dir
    File.join('output', self.unique_id)
  end

  def compile_directory_rel_to_chroot
    @compile_directory_rel_to_chroot ||= File.join(LATEX_COMPILE_DIR_RELATIVE_TO_CHROOT, self.unique_id)
  end

  def compile_command
    case self.compiler
    when 'pdflatex'
      command = PDFLATEX_COMMAND
    when 'latex'
      command = LATEX_COMMAND
    when 'xelatex'
      command = XELATEX_COMMAND
    else
      raise NotImplemented # Previous checking means we should never get here!
    end
    return ["env"] + tex_env_variables + [command, "-interaction=batchmode",
            "-output-directory=#{compile_directory_rel_to_chroot}", "-no-shell-escape",
            "-jobname=output", self.root_resource_path]
  end

  # Returns a list of output files of the given type. Will raise a CLSI::NoOutputFile if no output
  # files of the given type exist.
  def find_output_files_of_type(type)
    file_name = "output.#{type}"
    output_path = File.join(compile_directory, file_name)
    raise CLSI::NoOutputProduced, 'no compiled documents were produced' unless File.exist?(output_path)
    return [file_name]
  end

  def ensure_output_files_exist(type)
    find_output_files_of_type(type)
  end

  def convert_pdf_to_pdf
    # Nothing to do!
  end

  def convert_dvi_to_dvi
    # Nothing to do!
  end

  def convert_dvi_to_pdf
    input = File.join(compile_directory_rel_to_chroot, 'output.dvi')
    output = File.join(compile_directory_rel_to_chroot, 'output.pdf')

    # Note: Adding &> /dev/null to this command makes run_with_timeout return straight away before
    # command is complete, and I have no idea why. Solution: Don't add it.
    dvipdf_command = "env TEXPICTS=#{compile_directory_rel_to_chroot} #{DVIPDF_COMMAND} -o \"#{output}\" \"#{input}\""
    run_with_timeout(dvipdf_command, DVIPDF_TIMEOUT)
  end

  def convert_dvi_to_ps
    input = File.join(compile_directory_rel_to_chroot, 'output.dvi')
    output = File.join(compile_directory_rel_to_chroot, 'output.ps')
    dvips_command = "env TEXPICTS=#{compile_directory_rel_to_chroot} #{DVIPS_COMMAND} -o \"#{output}\" \"#{input}\""
    run_with_timeout(dvips_command, DVIPS_TIMEOUT)
  end

  # Everything below here is copied from the mathwiki code. It was ugly when
  # I first wrote it and it hasn't improved with time.
  # Fixing it would be good.
  def run_with_timeout(command, timeout = 10)
    start_time = Time.now
    pid = fork {
      exec(*command)
    }
    while Time.now - start_time < timeout
      if Process.waitpid(pid, Process::WNOHANG)
        Rails.logger.info "(#{Time.now - start_time} seconds) #{command.to_a.join(' ')}"
        return pid
      end
      sleep 0.1 if (Time.now - start_time > 0.3) # No need to check too often if it's taking a while
    end

    # Process never finished
    kill_process(pid)
    raise CLSI::Timeout, "the compile took too long to run and was aborted"
  end

  def kill_process(pid)
    child_pids = %x[ps -e -o 'ppid pid' | awk '$1 == #{pid} { print $2 }'].split
    child_pids.collect{|cpid| kill_process(cpid.to_i)}
    Process.kill('INT', pid)
    Process.kill('HUP', pid)
    Process.kill('KILL', pid)
  end
end


Class to Module + upgrades

#!/usr/bin/env ruby

##############################################################################################################
# ATENÇÃO: Este é o meu primeiro trabalho na linguagem Ruby. A lógica foi originalmente desenvolvida em PHP; #
# portanto, o estilo do código pode não agradar programadores Ruby experientes. Estou aberto a críticas      #
# construtivas e sugestões, para melhorar meu conhecimento na linguagem.                                     #
##############################################################################################################

# GExtenso class file
#
# author Fausto Gonçalves Cintra (goncin) <goncin@gmail.com>
# link http://goncin.wordpress.com
# link http://twitter.com/g0nc1n
# license http://creativecommons.org/licenses/LGPL/2.1/deed.pt
#

# GExtenso é uma classe que gera a representação por extenso de um número ou valor monetário.
#
# ATENÇÃO: A PÁGINA DE CÓDIGO DESTE ARQUIVO É UTF-8 (Unicode)!
#
# Sua implementação foi feita como prova de conceito, utilizando:
# * Métodos estáticos, implementando o padrão de projeto ("design pattern") SINGLETON;
# * Chamadas recursivas a métodos, minimizando repetições e mantendo o código enxuto; e
# * Tratamento de erros por intermédio de exceções.
#
# = EXEMPLOS DE USO =
#
# Para obter o extenso de um número, utilize GExtenso.numero.
#
# puts GExtenso.numero(832); # oitocentos e trinta e dois
# puts GExtenso.numero(832, GExtenso::GENERO_FEM) # oitocentas e trinta e duas
#
#
# Para obter o extenso de um valor monetário, utilize GExtenso.moeda.
#
# # IMPORTANTE: veja nota sobre o parâmetro 'valor' na documentação do método!
#
# puts GExtenso.moeda(15402) # cento e cinquenta e quatro reais e dois centavos
#
# puts GExtenso.moeda(47)   # quarenta e sete centavos
#
# puts GExtenso.moeda(357082, 2,
#   ['peseta', 'pesetas', GExtenso::GENERO_FEM],
#   ['cêntimo', 'cêntimos', GExtenso::GENERO_MASC])
#   # três mil, quinhentas e setenta pesetas e oitenta e dois cêntimos
#
# author Fausto Gonçalves Cintra (goncin) <goncin@gmail.com>
# version 0.1 2010-06-10

class GExtenso

  NUM_SING = 0
  NUM_PLURAL = 1
  POS_GENERO = 2
  GENERO_MASC = 0
  GENERO_FEM = 1

  VALOR_MAXIMO = 999999999

  # As unidades 1 e 2 variam em gênero, pelo que precisamos de dois conjuntos de strings (masculinas e femininas) para as unidades
  UNIDADES = {
    GENERO_MASC => {
      1 => 'um',
      2 => 'dois',
      3 => 'três',
      4 => 'quatro',
      5 => 'cinco',
      6 => 'seis',
      7 => 'sete',
      8 => 'oito',
      9 => 'nove'
    },
    GENERO_FEM => {
      1 => 'uma',
      2 => 'duas',
      3 => 'três',
      4 => 'quatro',
      5 => 'cinco',
      6 => 'seis',
      7 => 'sete',
      8 => 'oito',
      9 => 'nove'
    }
  }

  DE11A19 = {
    11 => 'onze',
    12 => 'doze',
    13 => 'treze',
    14 => 'quatorze',
    15 => 'quinze',
    16 => 'dezesseis',
    17 => 'dezessete',
    18 => 'dezoito',
    19 => 'dezenove'
  }

  DEZENAS = {
    10 => 'dez',
    20 => 'vinte',
    30 => 'trinta',
    40 => 'quarenta',
    50 => 'cinquenta',
    60 => 'sessenta',
    70 => 'setenta',
    80 => 'oitenta',
    90 => 'noventa'
  }

  CENTENA_EXATA = 'cem'

  # As centenas, com exceção de 'cento', também variam em gênero. Aqui também se faz
  # necessário dois conjuntos de strings (masculinas e femininas).

  CENTENAS = {
    GENERO_MASC => {
      100 => 'cento',
      200 => 'duzentos',
      300 => 'trezentos',
      400 => 'quatrocentos',
      500 => 'quinhentos',
      600 => 'seiscentos',
      700 => 'setecentos',
      800 => 'oitocentos',
      900 => 'novecentos'
    },
    GENERO_FEM => {
      100 => 'cento',
      200 => 'duzentas',
      300 => 'trezentas',
      400 => 'quatrocentas',
      500 => 'quinhentas',
      600 => 'seiscentas',
      700 => 'setecentas',
      800 => 'oitocentas',
      900 => 'novecentas'
    }
  }

  #'Mil' é invariável, seja em gênero, seja em número
  MILHAR = 'mil'

  MILHOES = {
    NUM_SING => 'milhão',
    NUM_PLURAL => 'milhões'
  }

   UNIDADES_ORDINAL = {
    GENERO_MASC => {
      1 => 'primeiro',
      2 => 'segundo',
      3 => 'terceiro',
      4 => 'quarto',
      5 => 'quinto',
      6 => 'sexto',
      7 => 'sétimo',
      8 => 'oitavo',
      9 => 'nono'},
    GENERO_FEM => {
      1 => 'primeira',
      2 => 'segunda',
      3 => 'terceira',
      4 => 'quarta',
      5 => 'quinta',
      6 => 'sexta',
      7 => 'sétima',
      8 => 'oitava',
      9 => 'nona'}}

  DEZENAS_ORDINAL = {
    GENERO_MASC => {
      10 => 'décimo',
      20 => 'vigésimo',
      30 => 'trigésimo',
      40 => 'quadragésimo',
      50 => 'quinquagésimo',
      60 => 'sexagésimo',
      70 => 'septuagésimo',
      80 => 'octogésimo',
      90 => 'nonagésimo'},
    GENERO_FEM => {
      10 => 'décima',
      20 => 'vigésima',
      30 => 'trigésima',
      40 => 'quadragésima',
      50 => 'quinquagésima',
      60 => 'sexagésima',
      70 => 'septuagésima',
      80 => 'octogésima',
      90 => 'nonagésima'}}

  CENTENAS_ORDINAL = {
    GENERO_MASC => {
      100 => 'centésimo',
      200 => 'ducentésimo',
      300 => 'trecentésimo',
      400 => 'quadringentésimo',
      500 => 'quingentésimo',
      600 => 'seiscentésimo',
      700 => 'septingentésimo',
      800 => 'octingentésimo',
      900 => 'noningentésimo'},
    GENERO_FEM => {
      100 => 'centésima',
      200 => 'ducentésima',
      300 => 'trecentésima',
      400 => 'quadringentésima',
      500 => 'quingentésima',
      600 => 'seiscentésima',
      700 => 'septingentésima',
      800 => 'octingentésima',
      900 => 'noningentésima'}}

    MILHAR_ORDINAL = {
      GENERO_MASC => {
        1000 => 'milésimo'},
      GENERO_FEM =>{
        1000 => 'milésima'}}

  def self.is_int(s)
    Integer(s) != nil rescue false
  end

  #######################################################################################################################################

  def self.numero (valor, genero = GENERO_MASC)

    # Gera a representação por extenso de um número inteiro, maior que zero e menor ou igual a VALOR_MAXIMO.
    #
    # PARÂMETROS:
    # valor (Integer) O valor numérico cujo extenso se deseja gerar
    #
    # genero (Integer) [Opcional; valor padrão: GExtenso::GENERO_MASC] O gênero gramatical (GExtenso::GENERO_MASC ou GExtenso::GENERO_FEM)
    # do extenso a ser gerado. Isso possibilita distinguir, por exemplo, entre 'duzentos e dois homens' e 'duzentas e duas mulheres'.
    #
    # VALOR DE RETORNO:
    # (String) O número por extenso

    # ----- VALIDAÇÃO DOS PARÂMETROS DE ENTRADA ----

    if !is_int(valor)
      raise "[Exceção em GExtenso.numero] Parâmetro 'valor' não é numérico (recebido: '#{valor}')"
    elsif valor <= 0
      raise "[Exceção em GExtenso.numero] Parâmetro 'valor' igual a ou menor que zero (recebido: '#{valor}')"
    elsif valor > VALOR_MAXIMO
      raise '[Exceção em GExtenso::numero] Parâmetro ''valor'' deve ser um inteiro entre 1 e ' + VALOR_MAXIMO.to_s + " (recebido: '#{valor}')"
    elsif genero != GENERO_MASC && genero != GENERO_FEM
      raise "Exceção em GExtenso: valor incorreto para o parâmetro 'genero' (recebido: '#{genero}')"

  # ------------------------------------------------

    elsif valor >= 1 && valor <= 9
      UNIDADES[genero][valor]

    elsif valor == 10
      DEZENAS[valor]

    elsif valor >= 11 && valor <= 19
      DE11A19[valor]

    elsif valor >= 20 && valor <= 99
      dezena = valor - (valor % 10)
      ret = DEZENAS[dezena]
      # Chamada recursiva à função para processar resto se este for maior que zero.
      # O conectivo 'e' é utilizado entre dezenas e unidades.
      resto = valor - dezena
      if resto > 0
        ret += ' e ' + self.numero(resto, genero)
      end
      ret

    elsif valor == 100
      CENTENA_EXATA

    elsif valor >= 101 && valor <= 999
      centena = valor - (valor % 100)
      ret = CENTENAS[genero][centena] # As centenas (exceto 'cento') variam em gênero
      # Chamada recursiva à função para processar resto se este for maior que zero.
      # O conectivo 'e' é utilizado entre centenas e dezenas.
      resto = valor - centena
      if resto > 0
        ret += ' e ' + self.numero(resto, genero)
      end
      ret

    elsif valor >= 1000 && valor <= 999999
      # A função 'floor' é utilizada para encontrar o inteiro da divisão de valor por 1000,
      # assim determinando a quantidade de milhares. O resultado é enviado a uma chamada recursiva
      # da função. A palavra 'mil' não se flexiona.
      milhar = (valor / 1000).floor
      ret = self.numero(milhar, GENERO_MASC) + ' ' + MILHAR # 'Mil' é do gênero masculino
      resto = valor % 1000
      # Chamada recursiva à função para processar resto se este for maior que zero.
      # O conectivo 'e' é utilizado entre milhares e números entre 1 e 99, bem como antes de centenas exatas.
      if resto > 0 && ((resto >= 1 && resto <= 99) || resto % 100 == 0)
        ret += ' e ' + self.numero(resto, genero)
      # Nos demais casos, após o milhar é utilizada a vírgula.
      elsif (resto > 0)
        ret += ', ' + self.numero(resto, genero)
      end
      ret

    elsif valor >= 100000 && valor <= VALOR_MAXIMO
      # A função 'floor' é utilizada para encontrar o inteiro da divisão de valor por 1000000,
      # assim determinando a quantidade de milhões. O resultado é enviado a uma chamada recursiva
      # da função. A palavra 'milhão' flexiona-se no plural.
      milhoes = (valor / 1000000).floor
      ret = self.numero(milhoes, GENERO_MASC) + ' ' # Milhão e milhões são do gênero masculino

      # Se a o número de milhões for maior que 1, deve-se utilizar a forma flexionada no plural
      ret += milhoes == 1 ? MILHOES[NUM_SING] : MILHOES[NUM_PLURAL]

      resto = valor % 1000000

      # Chamada recursiva à função para processar resto se este for maior que zero.
      # O conectivo 'e' é utilizado entre milhões e números entre 1 e 99, bem como antes de centenas exatas.
      if resto && ((resto >= 1 && resto <= 99) || resto % 100 == 0)
        ret += ' e ' + ret.numero(resto, genero)
      # Nos demais casos, após o milhão é utilizada a vírgula.
      elsif resto > 0
        ret += ', ' + self.numero(resto, genero)
      end
      ret

    end

  end

  #######################################################################################################################################

  def self.moeda(
    valor,
    casas_decimais = 2,
    info_unidade = ['real', 'reais', GENERO_MASC],
    info_fracao = ['centavo', 'centavos', GENERO_MASC]
  )

   # Gera a representação por extenso de um valor monetário, maior que zero e menor ou igual a GExtenso::VALOR_MAXIMO.
   #
   #
   # PARÂMETROS:
   # valor (Integer) O valor monetário cujo extenso se deseja gerar.
   #   ATENÇÃO: PARA EVITAR OS CONHECIDOS PROBLEMAS DE ARREDONDAMENTO COM NÚMEROS DE PONTO FLUTUANTE, O VALOR DEVE SER PASSADO
   #   JÁ DEVIDAMENTE MULTIPLICADO POR 10 ELEVADO A $casasDecimais (o que equivale, normalmente, a passar o valor com centavos
   #   multiplicado por 100)
   #
   # casas_decimais (Integer) [Opcional; valor padrão: 2] Número de casas decimais a serem consideradas como parte fracionária (centavos)
   #
   # info_unidade (Array) [Opcional; valor padrão: ['real', 'reais', GExtenso::GENERO_MASC]] Fornece informações sobre a moeda a ser
   #   utilizada. O primeiro valor da matriz corresponde ao nome da moeda no singular, o segundo ao nome da moeda no plural e o terceiro
   #   ao gênero gramatical do nome da moeda (GExtenso::GENERO_MASC ou GExtenso::GENERO_FEM)
   #
   # info_fracao (Array) [Opcional; valor padrão: ['centavo', 'centavos', GExtenso::GENERO_MASC]] Provê informações sobre a parte fracionária
   #   da moeda. O primeiro valor da matriz corresponde ao nome da parte fracionária no singular, o segundo ao nome da parte fracionária no plural
   #   e o terceiro ao gênero gramatical da parte fracionária (GExtenso::GENERO_MASC ou GExtenso::GENERO_FEM)
   #
   # VALOR DE RETORNO:
   # (String) O valor monetário por extenso

    # ----- VALIDAÇÃO DOS PARÂMETROS DE ENTRADA ----

    if ! self.is_int(valor)
      raise "[Exceção em GExtenso.moeda] Parâmetro 'valor' não é numérico (recebido: '#{valor}')"

    elsif valor <= 0
      raise "[Exceção em GExtenso.moeda] Parâmetro valor igual a ou menor que zero (recebido: '#{valor}')"

    elsif ! self.is_int(casas_decimais) || casas_decimais < 0
      raise "[Exceção em GExtenso.moeda] Parâmetro 'casas_decimais' não é numérico ou é menor que zero (recebido: '#{casas_decimais}')"

    elsif info_unidade.class != Array || info_unidade.length < 3
      temp = info_unidade.class == Array ? '[' + info_unidade.join(', ') + ']' : "'#{info_unidade}'"
      raise "[Exceção em GExtenso.moeda] Parâmetro 'info_unidade' não é uma matriz com 3 (três) elementos (recebido: #{temp})"

    elsif info_unidade[POS_GENERO] != GENERO_MASC && info_unidade[POS_GENERO] != GENERO_FEM
      raise "Exceção em GExtenso: valor incorreto para o parâmetro 'info_unidade[POS_GENERO]' (recebido: '#{info_unidade[POS_GENERO]}')"

    elsif info_fracao.class != Array || info_fracao.length < 3
      temp = info_fracao.class == Array ? '[' + info_fracao.join(', ') + ']' : "'#{info_fracao}'"
      raise "[Exceção em GExtenso.moeda] Parâmetro 'info_fracao' não é uma matriz com 3 (três) elementos (recebido: #{temp})"

    elsif info_fracao[POS_GENERO] != GENERO_MASC && info_fracao[POS_GENERO] != GENERO_FEM
      raise "[Exceção em GExtenso.moeda] valor incorreto para o parâmetro 'info_fracao[POS_GENERO]' (recebido: '#{info_fracao[POS_GENERO]}')."

    end

    # -----------------------------------------------

    ret = ''

    # A parte inteira do valor monetário corresponde ao valor passado dividido por 10 elevado a casas_decimais, desprezado o resto.
    # Assim, com o padrão de 2 casas_decimais, o valor será dividido por 100 (10^2), e o resto é descartado utilizando-se floor().
    parte_inteira = valor.floor / (10**casas_decimais)

    # A parte fracionária ('centavos'), por seu turno, corresponderá ao resto da divisão do valor por 10 elevado a casas_decimais.
    # No cenário comum em que trabalhamos com 2 casas_decimais, será o resto da divisão do valor por 100 (10^2).
    fracao = valor % (10**casas_decimais)

    # O extenso para a parte_inteira somente será gerado se esta for maior que zero. Para tanto, utilizamos
    # os préstimos do método GExtenso::numero().
    if parte_inteira > 0
      ret = self.numero(parte_inteira, info_unidade[POS_GENERO]) + ' '
      ret += parte_inteira == 1 ? info_unidade[NUM_SING] : info_unidade[NUM_PLURAL]
    end

    # De forma semelhante, o extenso da fracao somente será gerado se esta for maior que zero. */
    if fracao > 0
      # Se a parte_inteira for maior que zero, o extenso para ela já terá sido gerado. Antes de juntar os
      # centavos, precisamos colocar o conectivo 'e'.
      if parte_inteira > 0
        ret += ' e '
      end
      ret += self.numero(fracao, info_fracao[POS_GENERO]) + ' '
      ret += parte_inteira == 1 ? info_fracao[NUM_SING] : info_fracao[NUM_PLURAL]
    end

    ret

  end

  ######################################################################################################################################################
  def self.ordinal (valor, genero = GENERO_MASC)

    # Gera a representação ordinal de um número inteiro de 1 à 1000

    # PARÂMETROS:
    # valor (Integer) O valor numérico cujo extenso se deseja gerar
    #
    # genero (Integer) [Opcional; valor padrão: GExtenso::GENERO_MASC] O gênero gramatical (GExtenso::GENERO_MASC ou GExtenso::GENERO_FEM)
    # do extenso a ser gerado. Isso possibilita distinguir, por exemplo, entre 'duzentos e dois homens' e 'duzentas e duas mulheres'.
    #
    # VALOR DE RETORNO:
    # (String) O número por extenso

    # ----- VALIDAÇÃO DOS PARÂMETROS DE ENTRADA ----

    if !is_int(valor)
      raise "[Exceção em GExtenso.numero] Parâmetro 'valor' não é numérico (recebido: '#{valor}')"
    elsif valor <= 0
      raise "[Exceção em GExtenso.numero] Parâmetro 'valor' igual a ou menor que zero (recebido: '#{valor}')"
    elsif valor > VALOR_MAXIMO
      raise '[Exceção em GExtenso::numero] Parâmetro ''valor'' deve ser um inteiro entre 1 e ' + VALOR_MAXIMO.to_s + " (recebido: '#{valor}')"
    elsif genero != GENERO_MASC && genero != GENERO_FEM
      raise "Exceção em GExtenso: valor incorreto para o parâmetro 'genero' (recebido: '#{genero}')"
  # ------------------------------------------------
    elsif valor >= 1 && valor <= 9
      return UNIDADES_ORDINAL[genero][valor]
    elsif valor >= 10 && valor <= 99
      dezena = valor - (valor % 10)
      resto = valor - dezena
      ret = DEZENAS_ORDINAL[genero][dezena]+" "
      if resto > 0 then ret+= self.ordinal(resto,genero); end
      return ret
    elsif valor >= 100 && valor <= 999
      centena = valor - (valor % 100)
      resto = valor - centena
      ret = CENTENAS_ORDINAL[genero][centena]+" "
      if resto > 0 then ret += self.ordinal(resto, genero); end
      return ret
    elsif valor == 1000
      return MILHAR_ORDINAL[genero][valor]+" "
    end
  end

end

#!/usr/bin/env ruby

##############################################################################################################
# ATENÇÃO: Este é o meu primeiro trabalho na linguagem Ruby. A lógica foi originalmente desenvolvida em PHP; #
# portanto, o estilo do código pode não agradar programadores Ruby experientes. Estou aberto a críticas      #
# construtivas e sugestões, para melhorar meu conhecimento na linguagem.                                     #
##############################################################################################################

# GExtenso class file
#
# author Fausto Gonçalves Cintra (goncin) <goncin@gmail.com>
# assitente Leonardo Ostan (lostan) <lostan@gmail.com>
# link http://goncin.wordpress.com
# link http://twitter.com/g0nc1n
# license http://creativecommons.org/licenses/LGPL/2.1/deed.pt
#

# GExtenso é uma classe que gera a representação por extenso de um número ou valor monetário.
#
# ATENÇÃO: A PÁGINA DE CÓDIGO DESTE ARQUIVO É UTF-8 (Unicode)!
#
# Sua implementação foi feita como prova de conceito, utilizando:
# * Métodos estáticos, implementando o padrão de projeto ("design pattern") SINGLETON;
# * Chamadas recursivas a métodos, minimizando repetições e mantendo o código enxuto; e
# * Tratamento de erros por intermédio de exceções.
#
# = EXEMPLOS DE USO =
#
# Para obter o extenso de um número, utilize GExtenso.numero.
#
# puts GExtenso.numero(832); # oitocentos e trinta e dois
# puts GExtenso.numero(832, GExtenso::GENERO_FEM) # oitocentas e trinta e duas
#
#
# Para obter o extenso de um valor monetário, utilize GExtenso.moeda.
#
# # IMPORTANTE: veja nota sobre o parâmetro 'valor' na documentação do método!
#
# puts GExtenso.moeda(15402) # cento e cinquenta e quatro reais e dois centavos
#
# puts GExtenso.moeda(47)   # quarenta e sete centavos
#
# puts GExtenso.moeda(357082, 2,
#   ['peseta', 'pesetas', GExtenso::GENERO_FEM],
#   ['cêntimo', 'cêntimos', GExtenso::GENERO_MASC])
#   # três mil, quinhentas e setenta pesetas e oitenta e dois cêntimos
#
# author Fausto Gonçalves Cintra (goncin) <goncin@gmail.com>
# version 0.1 2010-06-10

require 'date'

module Extenso

  NUM_SING = 0
  NUM_PLURAL = 1
  POS_GENERO = 2
  GENERO_MASC = 0
  GENERO_FEM = 1

  VALOR_MAXIMO = 999999999

  # As unidades 1 e 2 variam em gênero, pelo que precisamos de dois conjuntos de strings (masculinas e femininas) para as unidades
  UNIDADES = {
    GENERO_MASC => {
      1 => 'um',
      2 => 'dois',
      3 => 'três',
      4 => 'quatro',
      5 => 'cinco',
      6 => 'seis',
      7 => 'sete',
      8 => 'oito',
      9 => 'nove'
    },
    GENERO_FEM => {
      1 => 'uma',
      2 => 'duas',
      3 => 'três',
      4 => 'quatro',
      5 => 'cinco',
      6 => 'seis',
      7 => 'sete',
      8 => 'oito',
      9 => 'nove'
    }
  }

  DE11A19 = {
    11 => 'onze',
    12 => 'doze',
    13 => 'treze',
    14 => 'quatorze',
    15 => 'quinze',
    16 => 'dezesseis',
    17 => 'dezessete',
    18 => 'dezoito',
    19 => 'dezenove'
  }

  DEZENAS = {
    10 => 'dez',
    20 => 'vinte',
    30 => 'trinta',
    40 => 'quarenta',
    50 => 'cinquenta',
    60 => 'sessenta',
    70 => 'setenta',
    80 => 'oitenta',
    90 => 'noventa'
  }

  CENTENA_EXATA = 'cem'

  # As centenas, com exceção de 'cento', também variam em gênero. Aqui também se faz
  # necessário dois conjuntos de strings (masculinas e femininas).

  CENTENAS = {
    GENERO_MASC => {
      100 => 'cento',
      200 => 'duzentos',
      300 => 'trezentos',
      400 => 'quatrocentos',
      500 => 'quinhentos',
      600 => 'seiscentos',
      700 => 'setecentos',
      800 => 'oitocentos',
      900 => 'novecentos'
    },
    GENERO_FEM => {
      100 => 'cento',
      200 => 'duzentas',
      300 => 'trezentas',
      400 => 'quatrocentas',
      500 => 'quinhentas',
      600 => 'seiscentas',
      700 => 'setecentas',
      800 => 'oitocentas',
      900 => 'novecentas'
    }
  }

  #'Mil' é invariável, seja em gênero, seja em número
  MILHAR = 'mil'

  MILHOES = {
    NUM_SING => 'milhão',
    NUM_PLURAL => 'milhões'
  }

  UNIDADES_ORDINAL = {
    GENERO_MASC => {
      1 => 'primeiro',
      2 => 'segundo',
      3 => 'terceiro',
      4 => 'quarto',
      5 => 'quinto',
      6 => 'sexto',
      7 => 'sétimo',
      8 => 'oitavo',
      9 => 'nono'},
    GENERO_FEM => {
      1 => 'primeira',
      2 => 'segunda',
      3 => 'terceira',
      4 => 'quarta',
      5 => 'quinta',
      6 => 'sexta',
      7 => 'sétima',
      8 => 'oitava',
      9 => 'nona'}}

  DEZENAS_ORDINAL = {
    GENERO_MASC => {
      10 => 'décimo',
      20 => 'vigésimo',
      30 => 'trigésimo',
      40 => 'quadragésimo',
      50 => 'quinquagésimo',
      60 => 'sexagésimo',
      70 => 'septuagésimo',
      80 => 'octogésimo',
      90 => 'nonagésimo'},
    GENERO_FEM => {
      10 => 'décima',
      20 => 'vigésima',
      30 => 'trigésima',
      40 => 'quadragésima',
      50 => 'quinquagésima',
      60 => 'sexagésima',
      70 => 'septuagésima',
      80 => 'octogésima',
      90 => 'nonagésima'}}

  CENTENAS_ORDINAL = {
    GENERO_MASC => {
      100 => 'centésimo',
      200 => 'ducentésimo',
      300 => 'trecentésimo',
      400 => 'quadringentésimo',
      500 => 'quingentésimo',
      600 => 'seiscentésimo',
      700 => 'septingentésimo',
      800 => 'octingentésimo',
      900 => 'noningentésimo'},
    GENERO_FEM => {
      100 => 'centésima',
      200 => 'ducentésima',
      300 => 'trecentésima',
      400 => 'quadringentésima',
      500 => 'quingentésima',
      600 => 'seiscentésima',
      700 => 'septingentésima',
      800 => 'octingentésima',
      900 => 'noningentésima'}}

    MILHAR_ORDINAL = {
      GENERO_MASC => {
        1000 => 'milésimo'},
      GENERO_FEM =>{
        1000 => 'milésima'}}

    MESES = {
      1 => 'janeiro',
      2 => 'fevereiro',
      3 => 'março',
      4 => 'abril',
      5 => 'maio',
      6 => 'junho',
      7 => 'julho',
      8 => 'agosto',
      9 => 'setembro',
      10 =>'outubro',
      11 =>'novembro',
      12 =>'dezembro'}

  def Extenso.is_int(s)
    Integer(s) != nil rescue false
  end

  #######################################################################################################################################

  def Extenso.numero (valor, genero = GENERO_MASC)

    # Gera a representação por extenso de um número inteiro, maior que zero e menor ou igual a VALOR_MAXIMO.
    #
    # PARÂMETROS:
    # valor (Integer) O valor numérico cujo extenso se deseja gerar
    #
    # genero (Integer) [Opcional; valor padrão: GExtenso::GENERO_MASC] O gênero gramatical (GExtenso::GENERO_MASC ou GExtenso::GENERO_FEM)
    # do extenso a ser gerado. Isso possibilita distinguir, por exemplo, entre 'duzentos e dois homens' e 'duzentas e duas mulheres'.
    #
    # VALOR DE RETORNO:
    # (String) O número por extenso incluindo ponto flutuante caso exista

    # ----- VALIDAÇÃO DOS PARÂMETROS DE ENTRADA ----
    if valor < 0
      raise "[Exceção em GExtenso.numero] Parâmetro 'valor' igual a ou menor que zero (recebido: '#{valor}')"
    elsif valor > VALOR_MAXIMO
      raise '[Exceção em GExtenso::numero] Parâmetro ''valor'' deve ser um inteiro entre 1 e ' + VALOR_MAXIMO.to_s + " (recebido: '#{valor}')"
    elsif genero != GENERO_MASC && genero != GENERO_FEM
      raise "Exceção em GExtenso: valor incorreto para o parâmetro 'genero' (recebido: '#{genero}')"
    end

    if (valor - Integer(valor)) == 0
      return Extenso.numero_inteiro(Integer(valor))
    else
      flutuante = valor - Integer(valor)
      flutuante = flutuante.round(3)
      zeros = (1..4).detect{|x| 1.0*flutuante*(10**x) >= 1} - 1
      zeros_str = ""
      (1..zeros).each {|x| zeros_str << "zero "}
      casas = (1..4).detect{|x| 1.0*flutuante*(10**x)%10 == 0} - 1
      return Extenso.numero_inteiro(Integer(valor)) + " ponto " + zeros_str + Extenso.numero_inteiro((1.0*(10**casas)*flutuante).to_i)
    end
  end

  def Extenso.numero_inteiro(valor, genero = GENERO_MASC)
    if valor == 0
      return "zero"
    elsif valor >= 1 && valor <= 9
      UNIDADES[genero][valor]

    elsif valor == 10
      DEZENAS[valor]

    elsif valor >= 11 && valor <= 19
      DE11A19[valor]

    elsif valor >= 20 && valor <= 99
      dezena = valor - (valor % 10)
      ret = DEZENAS[dezena]
      # Chamada recursiva à função para processar resto se este for maior que zero.
      # O conectivo 'e' é utilizado entre dezenas e unidades.
      resto = valor - dezena
      if resto > 0
        ret += ' e ' + Extenso.numero(resto, genero)
      end
      ret

    elsif valor == 100
      CENTENA_EXATA

    elsif valor >= 101 && valor <= 999
      centena = valor - (valor % 100)
      ret = CENTENAS[genero][centena] # As centenas (exceto 'cento') variam em gênero
      # Chamada recursiva à função para processar resto se este for maior que zero.
      # O conectivo 'e' é utilizado entre centenas e dezenas.
      resto = valor - centena
      if resto > 0
        ret += ' e ' + Extenso.numero(resto, genero)
      end
      ret

    elsif valor >= 1000 && valor <= 999999
      # A função 'floor' é utilizada para encontrar o inteiro da divisão de valor por 1000,
      # assim determinando a quantidade de milhares. O resultado é enviado a uma chamada recursiva
      # da função. A palavra 'mil' não se flexiona.
      milhar = (valor / 1000).floor
      ret = Extenso.numero(milhar, GENERO_MASC) + ' ' + MILHAR # 'Mil' é do gênero masculino
      resto = valor % 1000
      # Chamada recursiva à função para processar resto se este for maior que zero.
      # O conectivo 'e' é utilizado entre milhares e números entre 1 e 99, bem como antes de centenas exatas.
      if resto > 0 && ((resto >= 1 && resto <= 99) || resto % 100 == 0)
        ret += ' e ' + Extenso.numero(resto, genero)
      # Nos demais casos, após o milhar é utilizada a vírgula.
      elsif (resto > 0)
        ret += ', ' + Extenso.numero(resto, genero)
      end
      ret

    elsif valor >= 100000 && valor <= VALOR_MAXIMO
      # A função 'floor' é utilizada para encontrar o inteiro da divisão de valor por 1000000,
      # assim determinando a quantidade de milhões. O resultado é enviado a uma chamada recursiva
      # da função. A palavra 'milhão' flexiona-se no plural.
      milhoes = (valor / 1000000).floor
      ret = Extenso.numero(milhoes, GENERO_MASC) + ' ' # Milhão e milhões são do gênero masculino

      # Se a o número de milhões for maior que 1, deve-se utilizar a forma flexionada no plural
      ret += milhoes == 1 ? MILHOES[NUM_SING] : MILHOES[NUM_PLURAL]

      resto = valor % 1000000

      # Chamada recursiva à função para processar resto se este for maior que zero.
      # O conectivo 'e' é utilizado entre milhões e números entre 1 e 99, bem como antes de centenas exatas.
      if resto && ((resto >= 1 && resto <= 99) || resto % 100 == 0)
        ret += ' e ' + Extenso.numero(resto, genero)
      # Nos demais casos, após o milhão é utilizada a vírgula.
      elsif resto > 0
        ret += ', ' + Extenso.numero(resto, genero)
      end
      ret

    end

  end

  #######################################################################################################################################

  def Extenso.moeda(
    valor,
    casas_decimais = 2,
    info_unidade = ['real', 'reais', GENERO_MASC],
    info_fracao = ['centavo', 'centavos', GENERO_MASC]
  )

   # Gera a representação por extenso de um valor monetário, maior que zero e menor ou igual a GExtenso::VALOR_MAXIMO.
   #
   #
   # PARÂMETROS:
   # valor (Integer) O valor monetário cujo extenso se deseja gerar.
   #   ATENÇÃO: PARA EVITAR OS CONHECIDOS PROBLEMAS DE ARREDONDAMENTO COM NÚMEROS DE PONTO FLUTUANTE, O VALOR DEVE SER PASSADO
   #   JÁ DEVIDAMENTE MULTIPLICADO POR 10 ELEVADO A $casasDecimais (o que equivale, normalmente, a passar o valor com centavos
   #   multiplicado por 100)
   #
   # casas_decimais (Integer) [Opcional; valor padrão: 2] Número de casas decimais a serem consideradas como parte fracionária (centavos)
   #
   # info_unidade (Array) [Opcional; valor padrão: ['real', 'reais', GExtenso::GENERO_MASC]] Fornece informações sobre a moeda a ser
   #   utilizada. O primeiro valor da matriz corresponde ao nome da moeda no singular, o segundo ao nome da moeda no plural e o terceiro
   #   ao gênero gramatical do nome da moeda (GExtenso::GENERO_MASC ou GExtenso::GENERO_FEM)
   #
   # info_fracao (Array) [Opcional; valor padrão: ['centavo', 'centavos', GExtenso::GENERO_MASC]] Provê informações sobre a parte fracionária
   #   da moeda. O primeiro valor da matriz corresponde ao nome da parte fracionária no singular, o segundo ao nome da parte fracionária no plural
   #   e o terceiro ao gênero gramatical da parte fracionária (GExtenso::GENERO_MASC ou GExtenso::GENERO_FEM)
   #
   # VALOR DE RETORNO:
   # (String) O valor monetário por extenso

    # ----- VALIDAÇÃO DOS PARÂMETROS DE ENTRADA ----
    if ! Extenso.is_int(valor)
      raise "[Exceção em GExtenso.moeda] Parâmetro 'valor' não é numérico (recebido: '#{valor}')"

    elsif valor < 0
      raise "[Exceção em GExtenso.moeda] Parâmetro valor igual a ou menor que zero (recebido: '#{valor}')"

    elsif ! Extenso.is_int(casas_decimais) || casas_decimais < 0
      raise "[Exceção em GExtenso.moeda] Parâmetro 'casas_decimais' não é numérico ou é menor que zero (recebido: '#{casas_decimais}')"

    elsif info_unidade.class != Array || info_unidade.length < 3
      temp = info_unidade.class == Array ? '[' + info_unidade.join(', ') + ']' : "'#{info_unidade}'"
      raise "[Exceção em GExtenso.moeda] Parâmetro 'info_unidade' não é uma matriz com 3 (três) elementos (recebido: #{temp})"

    elsif info_unidade[POS_GENERO] != GENERO_MASC && info_unidade[POS_GENERO] != GENERO_FEM
      raise "Exceção em GExtenso: valor incorreto para o parâmetro 'info_unidade[POS_GENERO]' (recebido: '#{info_unidade[POS_GENERO]}')"

    elsif info_fracao.class != Array || info_fracao.length < 3
      temp = info_fracao.class == Array ? '[' + info_fracao.join(', ') + ']' : "'#{info_fracao}'"
      raise "[Exceção em GExtenso.moeda] Parâmetro 'info_fracao' não é uma matriz com 3 (três) elementos (recebido: #{temp})"

    elsif info_fracao[POS_GENERO] != GENERO_MASC && info_fracao[POS_GENERO] != GENERO_FEM
      raise "[Exceção em GExtenso.moeda] valor incorreto para o parâmetro 'info_fracao[POS_GENERO]' (recebido: '#{info_fracao[POS_GENERO]}')."
    elsif valor == 0
      return "zero reais"
    end

    # -----------------------------------------------

    ret = ''

    # A parte inteira do valor monetário corresponde ao valor passado dividido por 10 elevado a casas_decimais, desprezado o resto.
    # Assim, com o padrão de 2 casas_decimais, o valor será dividido por 100 (10^2), e o resto é descartado utilizando-se floor().
    parte_inteira = valor.floor / (10**casas_decimais)

    # A parte fracionária ('centavos'), por seu turno, corresponderá ao resto da divisão do valor por 10 elevado a casas_decimais.
    # No cenário comum em que trabalhamos com 2 casas_decimais, será o resto da divisão do valor por 100 (10^2).
    fracao = valor % (10**casas_decimais)

    # O extenso para a parte_inteira somente será gerado se esta for maior que zero. Para tanto, utilizamos
    # os préstimos do método GExtenso::numero().
    if parte_inteira > 0
      ret = Extenso.numero(parte_inteira, info_unidade[POS_GENERO]) + ' '
      ret += parte_inteira == 1 ? info_unidade[NUM_SING] : info_unidade[NUM_PLURAL]
    end

    # De forma semelhante, o extenso da fracao somente será gerado se esta for maior que zero. */
    if fracao > 0
      # Se a parte_inteira for maior que zero, o extenso para ela já terá sido gerado. Antes de juntar os
      # centavos, precisamos colocar o conectivo 'e'.
      if parte_inteira > 0
        ret += ' e '
      end
      ret += Extenso.numero(fracao, info_fracao[POS_GENERO]) + ' '
      ret += parte_inteira == 1 ? info_fracao[NUM_SING] : info_fracao[NUM_PLURAL]
    end

    ret

  end
######################################################################################################################################################
  def Extenso.data(data)
    # Escreve uma data por extenso
    # A variavel data é um objeto da classe Date
    # Uso esta classe porque ela dá suporte à operações elementares com datas e mantém a consistência nos sistemas que estou programando
    # VALOR DE RETORNO:
    # (String) A data escrita por extenso
    # Exemplo: GExtenso.data(Date.parse "23/12/1983") ==> "vinte e três de dezembro de mil novecentos e oitenta e três"
    %Q{#{Extenso.numero(data.day)} de #{MESES[data.month]} de #{Extenso.numero(data.year)}}
  end

######################################################################################################################################################
  def Extenso.ordinal (valor, genero = GENERO_MASC)

    # Gera a representação ordinal de um número inteiro de 1 à 1000

    # PARÂMETROS:
    # valor (Integer) O valor numérico cujo extenso se deseja gerar
    #
    # genero (Integer) [Opcional; valor padrão: GExtenso::GENERO_MASC] O gênero gramatical (GExtenso::GENERO_MASC ou GExtenso::GENERO_FEM)
    # do extenso a ser gerado. Isso possibilita distinguir, por exemplo, entre 'duzentos e dois homens' e 'duzentas e duas mulheres'.
    #
    # VALOR DE RETORNO:
    # (String) O número por extenso

    # ----- VALIDAÇÃO DOS PARÂMETROS DE ENTRADA ----

    if !is_int(valor)
      raise "[Exceção em GExtenso.numero] Parâmetro 'valor' não é numérico (recebido: '#{valor}')"
    elsif valor <= 0
      raise "[Exceção em GExtenso.numero] Parâmetro 'valor' igual a ou menor que zero (recebido: '#{valor}')"
    elsif valor > VALOR_MAXIMO
      raise '[Exceção em GExtenso::numero] Parâmetro ''valor'' deve ser um inteiro entre 1 e ' + VALOR_MAXIMO.to_s + " (recebido: '#{valor}')"
    elsif genero != GENERO_MASC && genero != GENERO_FEM
      raise "Exceção em GExtenso: valor incorreto para o parâmetro 'genero' (recebido: '#{genero}')"
  # ------------------------------------------------
    elsif valor >= 1 && valor <= 9
      return UNIDADES_ORDINAL[genero][valor]
    elsif valor >= 10 && valor <= 99
      dezena = valor - (valor % 10)
      resto = valor - dezena
      ret = DEZENAS_ORDINAL[genero][dezena]+" "
      if resto > 0 then ret+= Extenso.ordinal(resto,genero); end
      return ret
    elsif valor >= 100 && valor <= 999
      centena = valor - (valor % 100)
      resto = valor - centena
      ret = CENTENAS_ORDINAL[genero][centena]+" "
      if resto > 0 then ret += Extenso.ordinal(resto, genero); end
      return ret
    elsif valor == 1000
      return MILHAR_ORDINAL[genero][valor]+" "
    end
  end

  def Extenso.moeda_real(numero)
    unit = "R$"
    separator = ","
    delimiter = "."
    mystring = sprintf("%s %.2f",unit, numero)
    mystring = mystring.gsub(".",separator)
    pos = mystring.match(separator).begin(0) - 3
    while !(/[0-9]/.match(mystring[pos-1])== nil) do
      mystring.insert(pos,delimiter)
      pos-=3
    end
    return mystring
  end

  def Extenso.to_cash(numero)
    unit = "R$"
    separator = ","
    delimiter = "."
    mystring = sprintf("%s %.2f",unit, numero)
    mystring = mystring.gsub(".",separator)
    pos = mystring.match(separator).begin(0) - 3
    while !(/[0-9]/.match(mystring[pos-1])== nil) do
      mystring.insert(pos,delimiter)
      pos-=3
    end
    return mystring << " (" << Extenso.moeda((numero*100).to_i) << ")"
  end

end


Small website improvements

#content h1 { margin: 0 0 .75em 0; font-size: 200%; }
#content h2 {
  margin: 1.5em 0 .75em 0; border-bottom: 2pt dotted silver;
  font-size: 150%;
}
#content h3 { margin: 1.5em 0 .5em 0; font-size: 125%; }
#content li { margin: .5em 0; }
#content dt { font-weight: bold; }
#content dd { margin-top: 1em; margin-bottom: 1em; }
#content aside.info { margin: 1em 0 1em 2em; color: gray; }
#content pre {
  overflow: auto;
  padding: .5em 1em; border-left: 5px solid silver;
  background-color: #f0f0f0;
}
#content table { border-spacing: 0; }
#content a, #content a:visited { color: #3d586c; }

#content .center { text-align: center; }

/* Home */

#content #sidebar {
  float: right; width: 17em;
  font-size: 80%; text-align: center;
}
#content #sidebar a {
  font-weight: bold; text-decoration: none;
  color: #006000;
}
#content #sidebar a:hover { text-decoration: underline; }
#content #sidebar a.try {
  display: block;
  padding: .75em; border-radius: .6em; -moz-border-radius: .6em;
  font-size: 140%;
  color: #e0ffe0; background-color: #499149;
}
#content #sidebar a.try:hover {
  text-decoration: none; background-color: #006000;
}
#content #sidebar .npm {
  padding: .75em; border: 1px solid #499149; border-radius: .7em; -moz-border-radius: .7em;
  font-family: "Lucida Console", fixed, monospace; font-size: 120%;
  color: #2c572c; background-color: #e0ffe0;
}
#content #sidebar .label { margin-left: 2.6em; text-align: left; color: #606060; }
#content #sidebar #download {
  list-style: square;
  padding-left: 2em;
  color: gray;
  text-align: left;
  font-size: 120%;
}
#content #sidebar a.twitter { display:block; margin-top: 5em; }
#content #sidebar .separator { color: gray; margin: 1.5em 0 1em 0; }

#content #left-column { margin-right: 17em; }

/* Online Version */

#content .message {
  border-radius: .5em; -moz-border-radius: .5em; padding: .5em 1em;
}
#content .message.info { background-color: #c0ffc0; }
#content .message.info a.download { display: block; float: right; }
#content .message.info .size-and-time { visibility: hidden; float: right; font-size: 70%; margin: .3em 0; color: #80c080; }
#content .message.info:hover .size-and-time { visibility: visible; }
#content .message.error { background-color: orange; }
#content .message.progress {
  padding-left: 40px;
  /* Spinner image generated by http://www.loadinfo.net/. */
  background: #ffff80 url("../img/spinner-16x16-progress.gif") 14px center no-repeat;
}
#content .message.disabled { color: gray; background-color: #f0f0f0; }

#content table.form { width: 100%; }
#content table.form td, table.form th { padding: .5em 1em; }
#content table.form td:first-child, table.form th:first-child { padding-left:  0; }
#content table.form td:last-child,  table.form th:last-child  { padding-right: 0; }
#content table.form th { text-align: left; font-weight: normal; }

#content h2.suggestion { border: none; }
#content h2.suggestion.top { margin-top: 0; }
#content h2.suggestion .step-number {
  display: block; float: left;
  width: 1.5em;
  border-radius: .4em; -moz-border-radius: .4em;
  text-align: center;
  color: white; background-color: black;
}
#content h2.suggestion .step-title { margin-left: 2.5em; }

#content textarea.code { width: 100%; height: 20em; font-family: "Lucida Console", fixed, monospace; }

#content .textarea-wrapper { padding-right: 6px; }

#content .tooltip { position: absolute; display: none; }
#content .tooltip .content {
  padding: .5em 1em;
  box-shadow: .25em .25em .5em rgba(0, 0, 0, 0.25); -webkit-box-shadow: .25em .25em .5em rgba(0, 0, 0, 0.25); -moz-box-shadow: .25em .25em .5em rgba(0, 0, 0, 0.25);
  border-radius: .5em; -moz-border-radius: .5em;
  padding: .5em 1em;
  color: white; background-color: black;
  font-size: 80%;
}
#content .tooltip .arrow {
  height: 6px;
  background: url("../img/tooltip-arrow.png") top center no-repeat;
}

#content #columns { width: 100%; height: 100%; border-spacing: 1em; }
#content #columns td { width: 50%; }

#content table.column { width: 100%; height: 100%; }
#content table.column td { vertical-align: top; }
/* Browsers will enlarge the |.content-height| cells to fit the contents. */
#content table.column td.content-height { height: 1px; }

#content #output-header {
  margin: 1.25em 0 0 0; border: none; padding: .25em 1.2em .25em 1.2em;
  font-size: 80%;
  color: white; background-color: silver;
}
#content #output {
  overflow: auto;
  max-height: 20em;
  margin: 0; padding: .5em 1em; border: 2px solid silver; border-top: none;
  background-color: #f0f0f0;
}
#content #output.disabled { color: gray; }

#content #settings { padding: .5em 0; }
#content #settings label { padding-right: 1em; }
#content #settings label[for=option-optimize] { padding-left: 2em; }
#content #parser-var { width: 15em; }
#content #options { padding-top: 1em; }
#content #parser-download {
  float: right;
  width: 9em;
  margin-top: 2em;
  padding: .5em; border-radius: .4em; -moz-border-radius: .4em;
  text-align: center; text-decoration: none;
  color: #e0ffe0; background-color: #499149;
}
#content #parser-download:hover { background-color: #006000; }
#content #parser-download.disabled { color: #e0e0e0; background-color: gray; }

$(document).ready(function() {
  var KB      = 1024;
  var MS_IN_S = 1000;

  var parser;

  var buildAndParseTimer = null;
  var parseTimer         = null;

  var oldGrammar        = null;
  var oldParserVar      = null;
  var oldOptionCache    = null;
  var oldOptionOptimize = null;
  var oldInput          = null;

  function buildSizeAndTimeInfoHtml(title, size, time) {
    return $("<span/>", {
      "class": "size-and-time",
      title:   title,
      html:    (size / KB).toPrecision(2) + " kB, "
                 + time + " ms, "
                 + ((size / KB) / (time / MS_IN_S)).toPrecision(2) + " kB/s"
    });
  }

  function buildErrorMessage(e) {
    return e.line !== undefined && e.column !== undefined
      ? "Line " + e.line + ", column " + e.column + ": " + e.message
      : e.message;
  }

  function build() {
    oldGrammar        = $("#grammar").val();
    oldParserVar      = $("#parser-var").val();
    oldOptionCache    = $("#option-cache").is(":checked");
    oldOptionOptimize = $("#option-optimize").val();

    $('#build-message').attr("class", "message progress").text("Building the parser...");
    $("#input").attr("disabled", "disabled");
    $("#parse-message").attr("class", "message disabled").text("Parser not available.");
    $("#output").addClass("disabled").text("Output not available.");
    $("#parser-var").attr("disabled", "disabled");
    $("#option-cache").attr("disabled", "disabled");
    $("#option-optimize").attr("disabled", "disabled");
    $("#parser-download").addClass("disabled");

    try {
      var timeBefore = (new Date).getTime();
      var parserSource = PEG.buildParser($("#grammar").val(), {
        cache:    $("#option-cache").is(":checked"),
        optimize: $("#option-optimize").val(),
        output:   "source"
      });
      var timeAfter = (new Date).getTime();

      parser = eval(parserSource);

      $("#build-message")
        .attr("class", "message info")
        .html("Parser built successfully.")
        .append(buildSizeAndTimeInfoHtml(
          "Parser build time and speed",
          $("#grammar").val().length,
          timeAfter - timeBefore
        ));
      var parserUrl = "data:text/plain;charset=utf-8;base64,"
        + Base64.encode($("#parser-var").val() + " = " + parserSource + ";\n");
      $("#input").removeAttr("disabled");
      $("#parser-var").removeAttr("disabled");
      $("#option-cache").removeAttr("disabled");
      $("#option-optimize").removeAttr("disabled");
      $("#parser-download").removeClass("disabled").attr("href", parserUrl);

      var result = true;
    } catch (e) {
      $("#build-message").attr("class", "message error").text(buildErrorMessage(e));
      var parserUrl = "data:text/plain;charset=utf-8;base64,"
        + Base64.encode("Parser not available.");
      $("#parser-download").attr("href", parserUrl);

      var result = false;
    }

    doLayout();
    return result;
  }

  function parse() {
    oldInput = $("#input").val();

    $("#input").removeAttr("disabled");
    $("#parse-message").attr("class", "message progress").text("Parsing the input...");
    $("#output").addClass("disabled").text("Output not available.");

    try {
      var timeBefore = (new Date).getTime();
      var output = parser.parse($("#input").val());
      var timeAfter = (new Date).getTime();

      $("#parse-message")
        .attr("class", "message info")
        .text("Input parsed successfully.")
        .append(buildSizeAndTimeInfoHtml(
          "Parsing time and speed",
          $("#input").val().length,
          timeAfter - timeBefore
        ));
      $("#output").removeClass("disabled").text(jsDump.parse(output));

      var result = true;
    } catch (e) {
      $("#parse-message").attr("class", "message error").text(buildErrorMessage(e));

      var result = false;
    }

    doLayout();
    return result;
  }

  function buildAndParse() {
    build() && parse();
  }

  function scheduleBuildAndParse() {
    var nothingChanged = $("#grammar").val() === oldGrammar
      && $("#parser-var").val() === oldParserVar
      && $("#option-cache").is(":checked") === oldOptionCache
      && $("#option-optimize").val() === oldOptionOptimize;
    if (nothingChanged) { return; }

    if (buildAndParseTimer !== null) {
      clearTimeout(buildAndParseTimer);
      buildAndParseTimer = null;
    }
    if (parseTimer !== null) {
      clearTimeout(parseTimer);
      parseTimer = null;
    }

    buildAndParseTimer = setTimeout(function() {
      buildAndParse();
      buildAndParseTimer = null;
    }, 500);
  }

  function scheduleParse() {
    if ($("#input").val() === oldInput) { return; }
    if (buildAndParseTimer !== null) { return; }

    if (parseTimer !== null) {
      clearTimeout(parseTimer);
      parseTimer = null;
    }

    parseTimer = setTimeout(function() {
      parse();
      parseTimer = null;
    }, 500);
  }

  function doLayout() {
    /*
     * This forces layout of the page so that the |#columns| table gets a chance
     * make itself smaller when the browser window shrinks.
     */
    if ($.browser.msie || $.browser.opera) {
      $("#left-column").height("0px");
      $("#right-column").height("0px");
    }
    $("#grammar").height("0px");
    $("#input").height("0px");

    if ($.browser.msie || $.browser.opera) {
      $("#left-column").height(($("#left-column").parent().innerHeight() - 2) + "px");
      $("#right-column").height(($("#right-column").parent().innerHeight() - 2) + "px");
    }

    $("#grammar").height(($("#grammar").parent().parent().innerHeight() - 14) + "px");
    $("#input").height(($("#input").parent().parent().innerHeight() - 14) + "px");
  }

  $("#grammar, #parser-var, #option-cache, #option-optimize")
    .change(scheduleBuildAndParse)
    .mousedown(scheduleBuildAndParse)
    .mouseup(scheduleBuildAndParse)
    .click(scheduleBuildAndParse)
    .keydown(scheduleBuildAndParse)
    .keyup(scheduleBuildAndParse)
    .keypress(scheduleBuildAndParse);

  $("#input")
    .change(scheduleParse)
    .mousedown(scheduleParse)
    .mouseup(scheduleParse)
    .click(scheduleParse)
    .keydown(scheduleParse)
    .keyup(scheduleParse)
    .keypress(scheduleParse);

  doLayout();
  $(window).resize(doLayout);

  $("#loader").hide();
  $("#content").show();

  $("#grammar, #parser-var, #option-cache, #option-optimize").removeAttr("disabled");

  $("#grammar, #input").focus(function() {
    var textarea = $(this);

    setTimeout(function() {
      textarea.unbind("focus");

      var tooltip = textarea.next();
      var position = textarea.position();

      tooltip.css({
        top:  (position.top - tooltip.outerHeight() - 5) + "px",
        left: (position.left + textarea.outerWidth() - tooltip.outerWidth()) + "px"
      }).fadeTo(400, 0.8).delay(3000).fadeOut();
    }, 1000);
  });

  $("#grammar").focus();

  buildAndParse();
});

#content h1 { margin: 0 0 .75em 0; font-size: 200%; }
#content h2 {
  margin: 1.5em 0 .75em 0; border-bottom: 2pt dotted silver;
  font-size: 150%;
}
#content h3 { margin: 1.5em 0 .5em 0; font-size: 125%; }
#content li { margin: .5em 0; }
#content dt { font-weight: bold; }
#content dd { margin-top: 1em; margin-bottom: 1em; }
#content aside.info { margin: 1em 0 1em 2em; color: gray; }
#content pre {
  overflow: auto;
  padding: .5em 1em; border-left: 5px solid silver;
  background-color: #f0f0f0;
}
#content table { border-spacing: 0; }
#content a, #content a:visited { color: #3d586c; }

#content .center { text-align: center; }

/* Home */

#content #sidebar {
  float: right; width: 17em;
  font-size: 80%; text-align: center;
}
#content #sidebar a {
  font-weight: bold; text-decoration: none;
  color: #006000;
}
#content #sidebar a:hover { text-decoration: underline; }
#content #sidebar a.try {
  display: block;
  padding: .75em; border-radius: .6em; -moz-border-radius: .6em;
  font-size: 140%;
  color: #e0ffe0; background-color: #499149;
}
#content #sidebar a.try:hover {
  text-decoration: none; background-color: #006000;
}
#content #sidebar .npm {
  padding: .75em; border: 1px solid #499149; border-radius: .7em; -moz-border-radius: .7em;
  font-family: "Lucida Console", fixed, monospace; font-size: 120%;
  color: #2c572c; background-color: #e0ffe0;
}
#content #sidebar .label { margin-left: 2.6em; text-align: left; color: #606060; }
#content #sidebar #download {
  list-style: square;
  padding-left: 2em;
  color: gray;
  text-align: left;
  font-size: 120%;
}
#content #sidebar a.twitter { display:block; margin-top: 5em; }
#content #sidebar .separator { color: gray; margin: 1.5em 0 1em 0; }

#content #left-column { margin-right: 17em; }

/* Online Version */

#content .message {
  border-radius: .5em; -moz-border-radius: .5em; padding: .5em 1em;
}
#content .message.info { background-color: #c0ffc0; }
#content .message.info a.download { display: block; float: right; }
#content .message.info .size-and-time { visibility: hidden; float: right; font-size: 70%; margin: .3em 0; color: #80c080; }
#content .message.info:hover .size-and-time { visibility: visible; }
#content .message.error { background-color: orange; }
#content .message.progress {
  padding-left: 40px;
  /* Spinner image generated by http://www.loadinfo.net/. */
  background: #ffff80 url("../img/spinner-16x16-progress.gif") 14px center no-repeat;
}
#content .message.disabled { color: gray; background-color: #f0f0f0; }

#content table.form { width: 100%; }
#content table.form td, table.form th { padding: .5em 1em; }
#content table.form td:first-child, table.form th:first-child { padding-left:  0; }
#content table.form td:last-child,  table.form th:last-child  { padding-right: 0; }
#content table.form th { text-align: left; font-weight: normal; }

#content h2.suggestion { border: none; }
#content h2.suggestion.top { margin-top: 0; }
#content h2.suggestion .step-number {
  display: block; float: left;
  width: 1.5em;
  border-radius: .4em; -moz-border-radius: .4em;
  text-align: center;
  color: white; background-color: black;
}
#content h2.suggestion .step-title { margin-left: 2.5em; }

#content textarea.code { width: 100%; height: 20em; font-family: "Lucida Console", fixed, monospace; }

#content .textarea-wrapper { padding-right: 6px; }

#content #columns { width: 100%; height: 100%; border-spacing: 1em; }
#content #columns td { width: 50%; }

#content table.column { width: 100%; height: 100%; }
#content table.column td { vertical-align: top; }
/* Browsers will enlarge the |.content-height| cells to fit the contents. */
#content table.column td.content-height { height: 1px; }

#content #output-header {
  margin: 1.25em 0 0 0; border: none; padding: .25em 1.2em .25em 1.2em;
  font-size: 80%;
  color: white; background-color: silver;
}
#content #output {
  overflow: auto;
  max-height: 20em;
  margin: 0; padding: .5em 1em; border: 2px solid silver; border-top: none;
  background-color: #f0f0f0;
}
#content #output.disabled { color: gray; }

#content #settings { padding: .5em 0; }
#content #settings label { padding-right: 1em; }
#content #parser-var { width: 15em; }
#content #options { padding-top: 1em; }
#content #parser-download {
  float: right;
  width: 9em;
  margin-top: 2em;
  padding: .5em; border-radius: .4em; -moz-border-radius: .4em;
  text-align: center; text-decoration: none;
  color: #e0ffe0; background-color: #499149;
}
#content #parser-download:hover { background-color: #006000; }
#content #parser-download.disabled { color: #e0e0e0; background-color: gray; }

$(document).ready(function() {
  var KB      = 1024;
  var MS_IN_S = 1000;

  var parser;

  var buildAndParseTimer = null;
  var parseTimer         = null;

  var oldGrammar                  = null;
  var oldParserVar                = null;
  var oldOptionCache              = null;
  var oldOptionTrackLineAndColumn = null;
  var oldInput                    = null;

  function buildSizeAndTimeInfoHtml(title, size, time) {
    return $("<span/>", {
      "class": "size-and-time",
      title:   title,
      html:    (size / KB).toPrecision(2) + " kB, "
                 + time + " ms, "
                 + ((size / KB) / (time / MS_IN_S)).toPrecision(2) + " kB/s"
    });
  }

  function buildErrorMessage(e) {
    return e.line !== undefined && e.column !== undefined
      ? "Line " + e.line + ", column " + e.column + ": " + e.message
      : e.message;
  }

  function build() {
    oldGrammar                  = $("#grammar").val();
    oldParserVar                = $("#parser-var").val();
    oldOptionCache              = $("#option-cache").is(":checked"),
    oldOptionTrackLineAndColumn = $("#option-track-line-and-column").is(":checked")

    $('#build-message').attr("class", "message progress").text("Building the parser...");
    $("#input").attr("disabled", "disabled");
    $("#parse-message").attr("class", "message disabled").text("Parser not available.");
    $("#output").addClass("disabled").text("Output not available.");
    $("#parser-var").attr("disabled", "disabled");
    $("#option-cache").attr("disabled", "disabled");
    $("#option-track-line-and-column").attr("disabled", "disabled");
    $("#parser-download").addClass("disabled");

    try {
      var timeBefore = (new Date).getTime();
      parser = PEG.buildParser($("#grammar").val(), {
        cache:              $("#option-cache").is(":checked"),
        trackLineAndColumn: $("#option-track-line-and-column").is(":checked")
      });
      var timeAfter = (new Date).getTime();

      $("#build-message")
        .attr("class", "message info")
        .html("Parser built successfully.")
        .append(buildSizeAndTimeInfoHtml(
          "Parser build time and speed",
          $("#grammar").val().length,
          timeAfter - timeBefore
        ));
      var parserUrl = "data:text/plain;charset=utf-8;base64,"
        + Base64.encode($("#parser-var").val() + " = " + parser.toSource() + ";\n");
      $("#input").removeAttr("disabled");
      $("#parser-var").removeAttr("disabled");
      $("#option-cache").removeAttr("disabled");
      $("#option-track-line-and-column").removeAttr("disabled");
      $("#parser-download").removeClass("disabled").attr("href", parserUrl);

      var result = true;
    } catch (e) {
      $("#build-message").attr("class", "message error").text(buildErrorMessage(e));
      var parserUrl = "data:text/plain;charset=utf-8;base64,"
        + Base64.encode("Parser not available.");
      $("#parser-download").attr("href", parserUrl);

      var result = false;
    }

    doLayout();
    return result;
  }

  function parse() {
    oldInput = $("#input").val();

    $("#input").removeAttr("disabled");
    $("#parse-message").attr("class", "message progress").text("Parsing the input...");
    $("#output").addClass("disabled").text("Output not available.");

    try {
      var timeBefore = (new Date).getTime();
      var output = parser.parse($("#input").val());
      var timeAfter = (new Date).getTime();

      $("#parse-message")
        .attr("class", "message info")
        .text("Input parsed successfully.")
        .append(buildSizeAndTimeInfoHtml(
          "Parsing time and speed",
          $("#input").val().length,
          timeAfter - timeBefore
        ));
      $("#output").removeClass("disabled").text(jsDump.parse(output));

      var result = true;
    } catch (e) {
      $("#parse-message").attr("class", "message error").text(buildErrorMessage(e));

      var result = false;
    }

    doLayout();
    return result;
  }

  function buildAndParse() {
    build() && parse();
  }

  function scheduleBuildAndParse() {
    var nothingChanged = $("#grammar").val() === oldGrammar
      && $("#parser-var").val() === oldParserVar
      && $("#option-cache").is(":checked") === oldOptionCache
      && $("#option-track-line-and-column").is(":checked") === oldOptionTrackLineAndColumn;
    if (nothingChanged) { return; }

    if (buildAndParseTimer !== null) {
      clearTimeout(buildAndParseTimer);
      buildAndParseTimer = null;
    }
    if (parseTimer !== null) {
      clearTimeout(parseTimer);
      parseTimer = null;
    }

    buildAndParseTimer = setTimeout(function() {
      buildAndParse();
      buildAndParseTimer = null;
    }, 500);
  }

  function scheduleParse() {
    if ($("#input").val() === oldInput) { return; }
    if (buildAndParseTimer !== null) { return; }

    if (parseTimer !== null) {
      clearTimeout(parseTimer);
      parseTimer = null;
    }

    parseTimer = setTimeout(function() {
      parse();
      parseTimer = null;
    }, 500);
  }

  function doLayout() {
    /*
     * This forces layout of the page so that the |#columns| table gets a chance
     * make itself smaller when the browser window shrinks.
     */
    if ($.browser.msie || $.browser.opera) {
      $("#left-column").height("0px");
      $("#right-column").height("0px");
    }
    $("#grammar").height("0px");
    $("#input").height("0px");

    if ($.browser.msie || $.browser.opera) {
      $("#left-column").height(($("#left-column").parent().innerHeight() - 2) + "px");
      $("#right-column").height(($("#right-column").parent().innerHeight() - 2) + "px");
    }

    $("#grammar").height(($("#grammar").parent().parent().innerHeight() - 14) + "px");
    $("#input").height(($("#input").parent().parent().innerHeight() - 14) + "px");
  }

  $("#grammar, #parser-var, #option-cache, #option-track-line-and-column")
    .change(scheduleBuildAndParse)
    .mousedown(scheduleBuildAndParse)
    .mouseup(scheduleBuildAndParse)
    .click(scheduleBuildAndParse)
    .keydown(scheduleBuildAndParse)
    .keyup(scheduleBuildAndParse)
    .keypress(scheduleBuildAndParse);

  $("#input")
    .change(scheduleParse)
    .mousedown(scheduleParse)
    .mouseup(scheduleParse)
    .click(scheduleParse)
    .keydown(scheduleParse)
    .keyup(scheduleParse)
    .keypress(scheduleParse);

  doLayout();
  $(window).resize(doLayout);

  $("#loader").hide();
  $("#content").show();

  $("#grammar, #parser-var, #option-cache, #option-track-line-and-column").removeAttr("disabled");

  $("#grammar").focus();

  buildAndParse();
});