jueves, 16 de mayo de 2013

Convertir números a letras usando PHP 5 / Poo y aplicando MVC

Es la misma función que hice para convertir números a letras (tanto scriptera como la versión para CodeIgniter), pero ahora hecha con programación orientada a objetos

Como un valor adicional, vamos a aplicar el patrón de diseño MVC (modelo, vista, controlador)

Para este ejemplo voy a suponer que ya conoces las bases de POO (programación orientada a objetos), de lo contrario sugiero primero buscar documentación en la web

Puedes ejecutar el demo desde aquí Convertir números a letras PHP 5 Poo/MVC

El código fuente lo puedes descargar desde aquí poo_numtoletras.zip

Primero, debemos crear una estructura de archivos que contenga:

  • un archivo index.php
  • una carpeta controller
    • un archivo c_numeros.php
  • una carpeta model
    • un archivo m_numeros.php
  • una carpeta view
    • un archivo v_numeros.php

Segundo, editamos el archivo ruteador index.php y colocamos este código:

<?php

/**
 * Ruteador para aplicar el modelo MVC simple
 * 
 * @author Ultiminio Ramos Galán <contacto@ultiminioramos.com>
 * @date 2013-05-16
 */

# cargar el controlador
include 'controller/c_numeros.php';

$instancia = new C_numeros();

$instancia->index();

/* EOF */

Tercero, editamos el archivo del controlador c_numeros.php y colocamos este código:

<?php
/**
 * Clase controladora
 * 
 * @author Ultiminio Ramos Galán <contacto@ultiminioramos.com>
 * @date 2013-05-16
 */

class C_numeros
{

    protected $_letras = '';
    protected $_cantidad;

    public function __construct()
    {
        
    }

    public function index()
    {
        if (isset($_POST['cantidad'])) {
            # preparar la variable
            $this->_prepararDatos();

            # cargar el modelo
            $modeloNum = $this->_cargarModelo('m_numeros');

            # ejecutar la conversion a letras
            try {
                $this->_letras = $modeloNum->numeroLetras($this->_cantidad);
            } catch (Exception $e) {
                $letras = 'Ocurrió un error.';
            }
        }

        # cargar la vista
        $view_data['cantidad'] = $this->_cantidad;
        $view_data['letras'] = $this->_letras;

        $this->_cargarVista('v_numeros', $view_data);
    }

    /**
     * En este método puedes validar el dato de entrada y 
     * agregar funcionalidad adicional
     * 
     * @return void
     */
    protected function _prepararDatos()
    {
        $this->_cantidad = trim($_POST['cantidad']);

        return;
    }

    /**
     * Instancia la clase del modelo y regresa el objeto
     * 
     * @param string $modelo
     * @return object
     */
    protected function _cargarModelo($modelo)
    {
        include "model/{$modelo}.php";
        
        return new M_numeros();
    }

    /**
     * Cargar la vista
     * 
     * @param string $vista
     * @param array $datos_vista
     */
    protected function _cargarVista($vista, $datos_vista)
    {
        include "view/{$vista}.php";
    }

}

Cuarto, editamos el archivo de la vista v_numeros.php y colocamos este código:

<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>Convertir números a letras con PHP 5/Poo </title>
    </head>
    <body>
        <form name="forma1" action="" method="post">
            <div style="width: 100%; display: block;">
                <input id="cantidad" type="text" name="cantidad" value="<?php echo $datos_vista['cantidad']; ?>" size="50" maxlength="21" /> 
                <input id="boton1" type="submit" name="boton1" value="Convertir..." />
            </div>
            <div style="width: 100%; display: block;">
                <textarea id="cantidad_letras" cols="100" rows="5"><?php echo $datos_vista['letras']; ?></textarea>
            </div>
        </form>

        <script type="text/javascript" charset="utf-8">
        </script>
    </body>
</html>

Quinto, editamos el archivo del modelo m_numeros.php y colocamos este código:

<?php
/**
 * Clase del proceso de negocio (modelo)
 * 
 * @author Ultiminio Ramos Galán <contacto@ultiminioramos.com>
 * @date 2013-05-16
 */

class M_numeros
{

    /**
     * Convierte un número en una cadena de letras, para el idioma
     * castellano, pero puede funcionar para español de mexico, de  
     * españa, colombia, argentina, etc.
     * 
     * Máxima cifra soportada: 18 dígitos con 2 decimales
     * 999,999,999,999,999,999.99
     * NOVECIENTOS NOVENTA Y NUEVE MIL NOVECIENTOS NOVENTA Y NUEVE BILLONES
     * NOVECIENTOS NOVENTA Y NUEVE MIL NOVECIENTOS NOVENTA Y NUEVE MILLONES
     * NOVECIENTOS NOVENTA Y NUEVE MIL NOVECIENTOS NOVENTA Y NUEVE PESOS 99/100 M.N.
     * 
     * @author Ultiminio Ramos Galán <contacto@ultiminioramos.com>
     * @param string $numero La cantidad numérica a convertir 
     * @param string $moneda La moneda local de tu país
     * @param string $subfijo Una cadena adicional para el subfijo
     * 
     * @return string La cantidad convertida a letras
     */
    public function numeroLetras($numero, $moneda = 'PESO', $subfijo = 'M.N.')
    {
        $xarray = array(
            0 => 'Cero'
            , 1 => 'UN', 'DOS', 'TRES', 'CUATRO', 'CINCO', 'SEIS', 'SIETE', 'OCHO', 'NUEVE'
            , 'DIEZ', 'ONCE', 'DOCE', 'TRECE', 'CATORCE', 'QUINCE', 'DIECISEIS', 'DIECISIETE', 'DIECIOCHO', 'DIECINUEVE'
            , 'VEINTI', 30 => 'TREINTA', 40 => 'CUARENTA', 50 => 'CINCUENTA'
            , 60 => 'SESENTA', 70 => 'SETENTA', 80 => 'OCHENTA', 90 => 'NOVENTA'
            , 100 => 'CIENTO', 200 => 'DOSCIENTOS', 300 => 'TRESCIENTOS', 400 => 'CUATROCIENTOS', 500 => 'QUINIENTOS'
            , 600 => 'SEISCIENTOS', 700 => 'SETECIENTOS', 800 => 'OCHOCIENTOS', 900 => 'NOVECIENTOS'
        );

        $numero = trim($numero);
        $xpos_punto = strpos($numero, '.');
        $xaux_int = $numero;
        $xdecimales = '00';
        if (!($xpos_punto === false)) {
            if ($xpos_punto == 0) {
                $numero = '0' . $numero;
                $xpos_punto = strpos($numero, '.');
            }
            $xaux_int = substr($numero, 0, $xpos_punto); // obtengo el entero de la cifra a covertir
            $xdecimales = substr($numero . '00', $xpos_punto + 1, 2); // obtengo los valores decimales
        }

        $XAUX = str_pad($xaux_int, 18, ' ', STR_PAD_LEFT); // ajusto la longitud de la cifra, para que sea divisible por centenas de miles (grupos de 6)
        $xcadena = '';
        for ($xz = 0; $xz < 3; $xz++) {
            $xaux = substr($XAUX, $xz * 6, 6);
            $xi = 0;
            $xlimite = 6; // inicializo el contador de centenas xi y establezco el límite a 6 dígitos en la parte entera
            $xexit = true; // bandera para controlar el ciclo del While
            while ($xexit) {
                if ($xi == $xlimite) { // si ya llegó al límite máximo de enteros
                    break; // termina el ciclo
                }

                $x3digitos = ($xlimite - $xi) * -1; // comienzo con los tres primeros digitos de la cifra, comenzando por la izquierda
                $xaux = substr($xaux, $x3digitos, abs($x3digitos)); // obtengo la centena (los tres dígitos)
                for ($xy = 1; $xy < 4; $xy++) { // ciclo para revisar centenas, decenas y unidades, en ese orden
                    switch ($xy) {
                        case 1: // checa las centenas
                            $key = (int) substr($xaux, 0, 3);
                            if (100 > $key) { // si el grupo de tres dígitos es menor a una centena ( < 99) no hace nada y pasa a revisar las decenas
                                /* do nothing */
                            } else {
                                if (TRUE === array_key_exists($key, $xarray)) {  // busco si la centena es número redondo (100, 200, 300, 400, etc..)
                                    $xseek = $xarray[$key];
                                    $xsub = $this->_subfijo($xaux); // devuelve el subfijo correspondiente (Millón, Millones, Mil o nada)
                                    if (100 == $key) {
                                        $xcadena = ' ' . $xcadena . ' CIEN ' . $xsub;
                                    } else {
                                        $xcadena = ' ' . $xcadena . ' ' . $xseek . ' ' . $xsub;
                                    }
                                    $xy = 3; // la centena fue redonda, entonces termino el ciclo del for y ya no reviso decenas ni unidades
                                } else { // entra aquí si la centena no fue numero redondo (101, 253, 120, 980, etc.)
                                    $key = (int) substr($xaux, 0, 1) * 100;
                                    $xseek = $xarray[$key]; // toma el primer caracter de la centena y lo multiplica por cien y lo busca en el arreglo (para que busque 100,200,300, etc)
                                    $xcadena = ' ' . $xcadena . ' ' . $xseek;
                                } // ENDIF ($xseek)
                            } // ENDIF (substr($xaux, 0, 3) < 100)
                            break;
                        case 2: // checa las decenas (con la misma lógica que las centenas)
                            $key = (int) substr($xaux, 1, 2);
                            if (10 > $key) {
                                /* do nothing */
                            } else {
                                if (TRUE === array_key_exists($key, $xarray)) {
                                    $xseek = $xarray[$key];
                                    $xsub = $this->_subfijo($xaux);
                                    if (20 == $key) {
                                        $xcadena = ' ' . $xcadena . ' VEINTE ' . $xsub;
                                    } else {
                                        $xcadena = ' ' . $xcadena . ' ' . $xseek . ' ' . $xsub;
                                    }
                                    $xy = 3;
                                } else {
                                    $key = (int) substr($xaux, 1, 1) * 10;
                                    $xseek = $xarray[$key];
                                    if (20 == $key)
                                        $xcadena = ' ' . $xcadena . ' ' . $xseek;
                                    else
                                        $xcadena = ' ' . $xcadena . ' ' . $xseek . ' Y ';
                                } // ENDIF ($xseek)
                            } // ENDIF (substr($xaux, 1, 2) < 10)
                            break;
                        case 3: // checa las unidades
                            $key = (int) substr($xaux, 2, 1);
                            if (1 > $key) { // si la unidad es cero, ya no hace nada
                                /* do nothing */
                            } else {
                                $xseek = $xarray[$key]; // obtengo directamente el valor de la unidad (del uno al nueve)
                                $xsub = $this->_subfijo($xaux);
                                $xcadena = ' ' . $xcadena . ' ' . $xseek . ' ' . $xsub;
                            } // ENDIF (substr($xaux, 2, 1) < 1)
                            break;
                    } // END SWITCH
                } // END FOR
                $xi = $xi + 3;
            } // ENDDO
            # si la cadena obtenida termina en MILLON o BILLON, entonces le agrega al final la conjuncion DE
            if ('ILLON' == substr(trim($xcadena), -5, 5)) {
                $xcadena.= ' DE';
            }

            # si la cadena obtenida en MILLONES o BILLONES, entonces le agrega al final la conjuncion DE
            if ('ILLONES' == substr(trim($xcadena), -7, 7)) {
                $xcadena.= ' DE';
            }

            # depurar leyendas finales
            if ('' != trim($xaux)) {
                switch ($xz) {
                    case 0:
                        if ('1' == trim(substr($XAUX, $xz * 6, 6))) {
                            $xcadena.= 'UN BILLON ';
                        } else {
                            $xcadena.= ' BILLONES ';
                        }
                        break;
                    case 1:
                        if ('1' == trim(substr($XAUX, $xz * 6, 6))) {
                            $xcadena.= 'UN MILLON ';
                        } else {
                            $xcadena.= ' MILLONES ';
                        }
                        break;
                    case 2:
                        if (1 > $numero) {
                            $xcadena = "CERO {$moneda}S {$xdecimales}/100 {$subfijo}";
                        }
                        if ($numero >= 1 && $numero < 2) {
                            $xcadena = "UN {$moneda} {$xdecimales}/100 {$subfijo}";
                        }
                        if ($numero >= 2) {
                            $xcadena.= " {$moneda}S {$xdecimales}/100 {$subfijo}"; //
                        }
                        break;
                } // endswitch ($xz)
            } // ENDIF (trim($xaux) != "")

            $xcadena = str_replace('VEINTI ', 'VEINTI', $xcadena); // quito el espacio para el VEINTI, para que quede: VEINTICUATRO, VEINTIUN, VEINTIDOS, etc
            $xcadena = str_replace('  ', ' ', $xcadena); // quito espacios dobles
            $xcadena = str_replace('UN UN', 'UN', $xcadena); // quito la duplicidad
            $xcadena = str_replace('  ', ' ', $xcadena); // quito espacios dobles
            $xcadena = str_replace('BILLON DE MILLONES', 'BILLON DE', $xcadena); // corrigo la leyenda
            $xcadena = str_replace('BILLONES DE MILLONES', 'BILLONES DE', $xcadena); // corrigo la leyenda
            $xcadena = str_replace('DE UN', 'UN', $xcadena); // corrigo la leyenda
        } // ENDFOR ($xz)
        return trim($xcadena);
    }

    /**
     * Esta función regresa un subfijo para la cifra
     * 
     * @author Ultiminio Ramos Galán <contacto@ultiminioramos.com>
     * @param string $cifras La cifra a medir su longitud
     */
    private function _subfijo($cifras)
    {
        $cifras = trim($cifras);
        $strlen = strlen($cifras);
        $_sub = '';
        if (4 <= $strlen && 6 >= $strlen) {
            $_sub = 'MIL';
        }

        return $_sub;
    }

}

/*  EOF  */

El código fuente lo puedes descargar desde aquí poo_numtoletras.zip

Espero que les haya sido de utilidad.

No hay comentarios:

Publicar un comentario

Datos personales

Mi foto
Podrás encontrar códigos recursos y artículos sobre PHP, JavaScript, jQuery, MooTools, Ajax, CSS, HTML, UML, RUP, AUP, XP (eXtreme Programming), Six-Sigma, CMMI, FrameWorks, Zend Framework, Magento, CodeIgniter, CakePHP, Joomla 1.5, Doctrine, Active Record, ORM, POO, MVC, MySql, PostgreSql. Este espacio está destinado a ayudar y compartir un poco de lo mucho que he recibido de la comunidad en la Red.