CultureInfo. Personalizar formatos

Recientemente publiqué un post en el que había creado una clase que facilitaba la labor de crear ficheros delimitados. Como suele ocurrir casi siempre, baso los artículos en vivencias de mi vida laboral. Es por esto que, tal como ha sucedido en el presente, en más de una ocasión, tras resolver un problema, surge otro nuevo.

El caso es que, hasta el momento, cuando había tenido que crear un fichero de este tipo para enviar al exterior, siempre estaba hablando con empresas españolas. En este caso, es distinto, puesto que el fichero irá a parar a una multinacional que, lógicamente, impone sus normas. En principio, estas normas no suelen dar muchos problemas, pero sí que representan algún trastorno en lo referente a los formatos numéricos y de fechas.

Reconozco que hasta el momento nunca me había preocupado mucho de estos temas de crear aplicaciones multilingües ni tal siquiera de cambiar determinados formatos, sobre todo numéricos, pero como ha llegado el momento de hacerlo, pues he empezado a interesarme, de momento, por alguna herramienta que nos ofrece el Framework. En esta ocasión no he pasado de la clase CultureInfo, aunque no descarto seguir con las interfaces IFormatProvider e ICustomFormater, que me han dado muy buenas vibraciones.

Bueno, vamos al grano, que siempre me disipo intentando entrar en situación. El caso es que necesito tener un modo más o menos centralizado en el que pueda definir cómo quedarán los formatos de fecha y numéricos, así que lo mejor que he sabido ver es crear una clase CulturaPropia, en la que pueda personalizar los formatos que necesito. Esta clase heredará de otra del Framework, la clase CultureInfo.

using System;
using System.Globalization;

namespace JnSoftware.Samples
{
    class CulturaPropia : CultureInfo
    {
        public CulturaPropia()
            : base("es-ES")
        {
            setFormatoNumerico();
            setFormatoMoneda();
            setFormatoFecha();
        }

        /// <summary>
        /// Establece formatos numéricos
        /// </summary>
        private void setFormatoNumerico()
        {
            NumberFormatInfo n = this.NumberFormat;
            n.NumberNegativePattern = 0;
            n.NumberGroupSeparator = ",";
            n.NumberDecimalSeparator = ".";
        }

        /// <summary>
        /// Establece formatos de Moneda
        /// </summary>
        private void setFormatoMoneda()
        {
            NumberFormatInfo n = this.NumberFormat;
            n.CurrencyPositivePattern = 3;
            n.CurrencySymbol = "EUR";
        }

        /// <summary>
        /// Establece formatos de fecha
        /// </summary>
        private void setFormatoFecha()
        {
            DateTimeFormatInfo d = this.DateTimeFormat;
            d.LongDatePattern = "'Montcada i Reixac, 'dd' de 'MMMM' de 'yyyy" ;
            d.ShortDatePattern = "dd.MMM.yyyy";
            d.FirstDayOfWeek = DayOfWeek.Monday;
            d.TimeSeparator = "/";
            d.DateSeparator = ".";
        }
    }
}

Como se puede apreciar, la clase es bien sencilla, puesto que, desde el mismo constructor, lo que intenta es “heredar” toda la información de una referencia cultural completa (en el ejemplo es-ES, correspondiente a España). A partir de aquí, tan sólo es cuestión de ir modificando aquellas propiedades de la clase que necesitemos.

Para la ejecución de las pruebas, he creado un proyecto de consola en el cual, sencillamente, muestro una comparación entre tres referencias diferentes: americana, española y la que he creado. Es posible que existan otras opciones de hacerlo, pero he visto que me resulta muy cómodo cambiar la referencia cultural al Thread actual, extraer los datos y volver a dejar tal cuál estaba la referencia.

using System;
using System.Globalization;

namespace JnSoftware.Samples
{
    class EjecucionPruebas
    {
        // Muestra diversa información en pantalla.
        public void MuestraEjemplo(string titulo, CultureInfo cultura)
        {
            DateTime fecha = DateTime.Now;
            double numero = 12345.6789;

            System.Threading.Thread.CurrentThread.CurrentCulture = cultura;
            Console.ForegroundColor = ConsoleColor.Yellow;
            Console.WriteLine(new string('=', Console.WindowWidth - 1));
            Console.WriteLine(titulo);
            Console.WriteLine(new string('=', Console.WindowWidth - 1));
            Console.ResetColor();
            Console.WriteLine("Fecha 1  : t{0:d}", fecha);
            Console.WriteLine("Fecha 2  : t{0:D}", fecha);
            Console.WriteLine("General  : t{0:G}", numero);
            Console.WriteLine("Moneda   : t{0:C}", numero);
            Console.WriteLine("Decimal  : t{0:D}", (int)numero);
            Console.WriteLine("Número   : t{0:N}", numero);
            Console.WriteLine("P. Fijo  : t{0:F}", numero);
        }

        // Ejecución de las pruebas.
        public static void Main()
        {
            // Lectura de la referencia actual
            CultureInfo actual = System.Threading.Thread.CurrentThread.CurrentCulture;
            Console.WindowHeight = 31;
            EjecucionPruebas test = new EjecucionPruebas();
            // Cambios de referencia culturales 
            test.MuestraEjemplo("en-US", new CultureInfo("en-US"));
            test.MuestraEjemplo("es-ES", new CultureInfo("es-ES"));
            test.MuestraEjemplo("Propia", new CulturaPropia());
            test = null;
            // Se establece la referencia inicial
            System.Threading.Thread.CurrentThread.CurrentCulture = actual;
            Console.ReadKey();
        }
    }
}

Tal como puede apreciarse, salvo algún ornamento añadido para que se muestre mejor la pantalla resultante, lo único que se hace en la aplicación es mostrar los mismos datos en las diferentes referencias culturales.

El resultado será el siguiente:

image

C# – Crear ficheros delimitados

No será la primera vez que en alguna de mis aplicaciones he tenido que crear ficheros de texto para intercambiar información con otras empresas. Para bancos, cajas de ahorro, ayuntamientos y un conjunto grande de entidades es moneda de cambio el trabajar con este tipo de documentos.

Aunque no es un punto excesivamente complicado, cada vez que he tenido que hacer un fichero ASCII delimitado he creado algún procedimiento que se encargase de este trabajo. Hoy me he propuesto crearme una clase que me permita generalizarme un poco la labor.

Si cogemos un ejemplo, como puede ser la creación de un fichero de adeudo por domiciliaciones (Norma 19), podríamos resumir diciendo que tiene la siguiente estructura:

  1. cabecera
  2. lista de datos
  3. resumen

Por tanto, con el fin de facilitar el trabajo, además de los procedimientos que necesitaremos para poder crear el fichero, deberemos tener unos métodos que nos permitan escribir una lista completa de datos o una línea de texto. Algo así como:

public class CamposDelimitados
 {
     /// <summary>
     /// Permite enviar todos los elementos de una lista al fichero de salida
     /// </summary>
     /// <param name="lista">Lista de elementos que deberán aparecer 
     /// en el fichero de salida</param>
     /// <remarks>La definición de la estructura de salida deberá especificarse 
     /// en el método ToString()</remarks>
     public void InsertaLista(IList lista)
     {
         throw new System.NotImplementedException();
     }


     /// <summary>
     /// Permite la inserción de una línea individual en el fichero de salida
     /// </summary>
     /// <param name="texto">Texto a incorporar en el fichero de salida</param>
     public void InsertaLinea(string texto)
     {
         throw new System.NotImplementedException();
     }
 }

Por tanto, una clase que tenga algún tipo de generalización y que sea útil para tal acción podría ser la siguiente:

using System.Collections;
using System.IO;
using System.Text;


namespace JnSoftware.Utiles
{

    /// <summary>
    /// Representa un escritor de ficheros delimitados.
    /// </summary>
    public class CamposDelimitados
    {

        private StringBuilder textoSalida = new StringBuilder();

        /// <summary>
        /// Permite enviar todos los elementos de una lista al 
        /// fichero de salida
        /// </summary>
        /// <param name="lista">Lista de elementos que deberán 
        /// aparecer en el fichero de salida</param>
        /// <remarks>La definición de la estructura de salida 
        /// deberá especificarse en el método ToString()</remarks>
        public void InsertaLista(IList lista)
        {
            foreach (object item in lista)
                InsertaLinea(item.ToString());
        }


        /// <summary>
        /// Permite la inserción de una línea.
        /// </summary>
        /// <param name="texto">Texto a incorporar en el fichero 
        /// de salida</param>
        public void InsertaLinea(string texto)
        {
            textoSalida.AppendLine(texto);
        }


        /// <summary>
        /// Crea el fichero de salida
        /// </summary>
        /// <param name="nombreFichero">Ruta completa del fichero 
        /// que contendrá los datos delimitados</param>
        public void creaFichero(string nombreFichero)
        {
            using (StreamWriter sw =
                new StreamWriter(nombreFichero, false, Encoding.UTF8))
            {
                sw.Write(textoSalida);
                sw.Flush();
                sw.Close();
            }
        }
    }
}

¿En qué me ayudará esta clase?. Es una pregunta sencilla de responder, puesto que fijándonos en el método InsertaLista, podremos deducir que bastará con crear una implementación propia del método ToString() de nuestras clases para definir la delimitación de los datos.

Claro que está que la implementación personalizada (o customizada, que últimamente está muy de moda este vocablo) del método ToString() representa un par de problemas adicionales, Por un lado, es algo personal ya que siempre me ha fastidiado bastante que hay empresas que suelen darte la columna inicial y la final y hay otras que te dan la longitud que deberá tener cada columna de datos; por otro lado, está la problemática de hacer que nuestras columnas de datos adquieran una longitud fija. Con la intención de solventar estos dos problemas, creo una serie de métodos estáticos en la misma clase para que agilicen la adaptación de los datos a la cadena resultante.

namespace JnSoftware.Utiles
{

    /// <summary>
    /// Describe el modo de alineación de un texto dentro 
    /// de un archivo delimitado
    /// </summary>
    public enum TextAlignment { Left, Right }


    /// <summary>
    /// Representa un escritor de ficheros delimitados.
    /// </summary>
    public class CamposDelimitados
    {

        /// <summary>
        /// Permite adaptar el contenido de un campo a un formato de tamaño fijo.
        /// </summary>
        /// <param name="campo">Contenido del campo en formato string</param>
        /// <param name="inicio">Entero que representa la posición 
        /// inicial en la delimitación</param>
        /// <param name="fin">Entero que representa la posición final en 
        /// la delimitación</param>
        /// <returns>Cadena preparada para insertar en el fichero</returns>
        public static string PreparaLinea(string campo, int inicio, int fin)
        {
            return PreparaLinea(campo, inicio, fin, TextAlignment.Left);
        }


        /// <summary>
        /// Permite adaptar el contenido de un campo a un formato de tamaño fijo.
        /// </summary>
        /// <param name="campo">Contenido del campo en formato string</param>
        /// <param name="longitudCampo">Longitud que deberá tener el campo 
        /// en la delimitación</param>
        /// <returns>Cadena preparada para insertar en el fichero</returns>
        public static string PreparaLinea(string campo, int longitudCampo)
        {
            return PreparaLinea(campo, longitudCampo, TextAlignment.Left);
        }


        /// <summary>
        /// Permite adaptar el contenido de un campo a un formato de tamaño fijo.
        /// </summary>
        /// <param name="campo">Contenido del campo en formato string</param>
        /// <param name="inicio">Entero que representa la posición inicial 
        /// en la delimitación</param>
        /// <param name="fin">Entero que representa la posición inicial en 
        /// la delimitación</param>
        /// <param name="alineacion">Alineación del texto en el fichero 
        /// delimitado</param>
        /// <returns>Cadena preparada para insertar en el fichero</returns>
        public static string PreparaLinea(
            string campo,
            int inicio,
            int fin,
            TextAlignment alineacion)
        {
            return PreparaLinea(campo, fin - inicio, alineacion);
        }


        /// <summary>
        /// Permite adaptar el contenido de un campo a un formato de tamaño fijo.
        /// </summary>
        /// <param name="campo">Contenido del campo en formato string</param>
        /// <param name="longitud">Longitud que deberá tener el campo en la 
        /// delimitación</param>
        /// <param name="alineacion">Alineación del texto en el fichero 
        /// delimitado</param>
        /// <returns>Cadena preparada para insertar en el fichero</returns>
        public static string PreparaLinea(
            string campo,
            int longitud,
            TextAlignment alineacion
            )
        {
            string espacios = new string(' ', longitud - campo.Length);
            if (alineacion == TextAlignment.Left)
                return campo + espacios;
            else
                return espacios + campo;
        }
    }
}

Por tanto, podemos decir que la clase, una vez finalizada, será la siguiente:

using System.Collections;
using System.IO;
using System.Text;


namespace JnSoftware.Utiles
{

    /// <summary>
    /// Describe el modo de alineación de un texto dentro 
    /// de un archivo delimitado
    /// </summary>
    public enum TextAlignment { Left, Right }


    /// <summary>
    /// Representa un escritor de ficheros delimitados.
    /// </summary>
    public class CamposDelimitados
    {

        private StringBuilder textoSalida = new StringBuilder();

        /// <summary>
        /// Permite enviar todos los elementos de una lista al 
        /// fichero de salida
        /// </summary>
        /// <param name="lista">Lista de elementos que deberán 
        /// aparecer en el fichero de salida</param>
        /// <remarks>La definición de la estructura de salida 
        /// deberá especificarse en el método ToString()</remarks>
        public void InsertaLista(IList lista)
        {
            foreach (object item in lista)
                InsertaLinea(item.ToString());
        }


        /// <summary>
        /// Permite la inserción de una línea.
        /// </summary>
        /// <param name="texto">Texto a incorporar en el fichero 
        /// de salida</param>
        public void InsertaLinea(string texto)
        {
            textoSalida.AppendLine(texto);
        }


        /// <summary>
        /// Crea el fichero de salida
        /// </summary>
        /// <param name="nombreFichero">Ruta completa del fichero 
        /// que contendrá los datos delimitados</param>
        public void creaFichero(string nombreFichero)
        {
            using (StreamWriter sw =
                new StreamWriter(nombreFichero, false, Encoding.UTF8))
            {
                sw.Write(textoSalida);
                sw.Flush();
                sw.Close();
            }
        }


        /// <summary>
        /// Permite adaptar el contenido de un campo a un formato de tamaño fijo.
        /// </summary>
        /// <param name="campo">Contenido del campo en formato string</param>
        /// <param name="inicio">Entero que representa la posición 
        /// inicial en la delimitación</param>
        /// <param name="fin">Entero que representa la posición final en 
        /// la delimitación</param>
        /// <returns>Cadena preparada para insertar en el fichero</returns>
        public static string PreparaLinea(string campo, int inicio, int fin)
        {
            return PreparaLinea(campo, inicio, fin, TextAlignment.Left);
        }


        /// <summary>
        /// Permite adaptar el contenido de un campo a un formato de tamaño fijo.
        /// </summary>
        /// <param name="campo">Contenido del campo en formato string</param>
        /// <param name="longitudCampo">Longitud que deberá tener el campo 
        /// en la delimitación</param>
        /// <returns>Cadena preparada para insertar en el fichero</returns>
        public static string PreparaLinea(string campo, int longitudCampo)
        {
            return PreparaLinea(campo, longitudCampo, TextAlignment.Left);
        }


        /// <summary>
        /// Permite adaptar el contenido de un campo a un formato de tamaño fijo.
        /// </summary>
        /// <param name="campo">Contenido del campo en formato string</param>
        /// <param name="inicio">Entero que representa la posición inicial 
        /// en la delimitación</param>
        /// <param name="fin">Entero que representa la posición inicial en 
        /// la delimitación</param>
        /// <param name="alineacion">Alineación del texto en el fichero 
        /// delimitado</param>
        /// <returns>Cadena preparada para insertar en el fichero</returns>
        public static string PreparaLinea(
            string campo,
            int inicio,
            int fin,
            TextAlignment alineacion)
        {
            return PreparaLinea(campo, fin - inicio, alineacion);
        }


        /// <summary>
        /// Permite adaptar el contenido de un campo a un formato de tamaño fijo.
        /// </summary>
        /// <param name="campo">Contenido del campo en formato string</param>
        /// <param name="longitud">Longitud que deberá tener el campo en la 
        /// delimitación</param>
        /// <param name="alineacion">Alineación del texto en el fichero 
        /// delimitado</param>
        /// <returns>Cadena preparada para insertar en el fichero</returns>
        public static string PreparaLinea(
            string campo,
            int longitud,
            TextAlignment alineacion
            )
        {
            string espacios = new string(' ', longitud - campo.Length);
            if (alineacion == TextAlignment.Left)
                return campo + espacios;
            else
                return espacios + campo;
        }
    }
}

¿Cómo utilizar la clase?

Creo un pequeño ejemplo en el que se muestra cómo podemos hacer uso de la clase en la creación de un fichero delimitado. En este ejemplo queda claro cómo definir el método ToString() para que se adapte al formato deseado:

using System;
using System.Collections.Generic;
using JnSoftware.Utiles;

namespace TextosDelimitadosTest
{
    class CreacionFicheroDelimitado
    {

        public int Id { get; set; }
        public string Texto { get; set; }
        public DateTime Fecha { get; set; }

        public override string ToString()
        {
            // En el método ToString() indicamos cómo deberá aparecer la información
            return 
                CamposDelimitados.PreparaLinea(Id.ToString(),1, 7, TextAlignment.Right)
                + CamposDelimitados.PreparaLinea(Texto, 30)
                + CamposDelimitados.PreparaLinea(Fecha.ToShortDateString(), 10);
                ;
        }

        // Ejecución de las pruebas
        public static void Main()
        {
            // Fichero de salida
            string fichero = @"C:Usersj.velascoDesktopexitTestsalida.txt";

            // Creación de una lista de ejemplo
            List<CreacionFicheroDelimitado> lista = 
                new List<CreacionFicheroDelimitado>();
            for (int i = 1; i < 20; i++)
            {
                lista.Add(
                    new CreacionFicheroDelimitado()
                    {
                        Id = i,
                        Texto = "Texto " + i.ToString(), 
                        Fecha = DateTime.Now 
                    }
                    );
            }

            // Creación del fichero
            CamposDelimitados cd = new CamposDelimitados();
            cd.InsertaLinea("-- Cabecera del texto");
            cd.InsertaLista(lista);
            cd.creaFichero(fichero);
        }
    }
}

Sql Server 2008 Management Studio

Últimamente toco poco el MS Sql Server, puesto que casi todas las empresas con las que colaboro parece que han coincidido en usar MySql en sus desarrollos. No es mi trabajo decidir sobre qué servidor se trabajará así que sólo me limito a intentar crear las aplicaciones de la manera más genérica posible (mejor dicho, que sé hacer) con la finalidad de poder cambiar en el futuro sin mucho trastorno.

Bueno, el caso es que no hace mucho se me ocurrió instalar Visual Studio 2010 en mi portátil y me actualizó SQL Server, de la versión 2005 a  la 2008 (versión Express, que tampoco necesito más). Así, me pongo el otro día a modificar una base y ¡¡Sorpresa!!. Me da problemas de permisos para poder modificar la tabla desde las herramientas del SQL SMS. ¡Pues mira qué bien!., me deja modificar elementos si trabajo desde SQL, también me deja modificar lo que quiera si lo hago desde VisualStudio (desde la versión 2005 y desde la 2010), pero no me deja usar las herramientas a las que estoy acostumbrado (Vale, reconozco que un poco de cabezonería sí que hay, que podría haber hecho lo mismo desde VisualStudio, pero si no nos pusiéramos así de vez en cuando, no sería tan divertido).

¡Vamos al grano!. Con VisualStudio se añade un “Centro de Instalación” de Sql Server el cual, como es de esperar, no sé usar, así que opto por ir a la mía y descargarme la aplicación (unos quinientos y pico megas) desde la página oficial.

Una vez descargado, lo ejecuto y la primera sorpresa es que me devuelve un error de compatibilidad, así que me lío y comienzo a descargar el SP1 de Sql Server 2008.

image

 

Como una vez descargado e instalado el SP1 de SqlServer continúo con la misma pantalla de compatibilidad, oso pulsar sobre el botón Ejecutar programa, que me lleva al mismo sitio en el que había estado al ejecutar el “Centro de Instalación”.

– ¡Mal vamos, Jesús!. Vigila, que si cuentas esto, luego se ríen de ti.

Esta es la pantalla que aparece:

image

 

Yo, que de SQL Server tampoco sé mucho, pero como ya he tenido algún que otro problema con instancias, se me ocurre intentar hacer una actualización desde SQL Server 2000 o SQL Server 2005 para evitar que se me creen nuevas instancias o desaparezcan alguna de las existentes. De este modo, me lío a ejecutar la típica instalación “Siguiente-Siguiente” que conlleva unas cuantas verificaciones para comprobar que el sistema es adecuado, los términos de licencia y poca cosa más. Justo en el momento en el que esperaba encontrar la típica pantalla en la que me permitiese seleccionar las herramientas que quiero instalar/desinstalar, me aparece otra en la que no me da la posibilidad de modificar ninguna opción y de la cual no me deja pasar.

 

image

 

Por cierto, al darle esta vez a “Siguiente”, el error con el que me encuentro es el siguiente:

 

image

 

¡Joroba!

Solución: La opción correcta es Nueva instalación independiente de SQL Server o agregar características a una instalación existente. ¿Por qué nos empeñamos siempre en leer sólo los primeros caracteres de las oraciones?. Este despiste me costó casi una tarde completamente perdida. Aunque ya empiezo a ilusionarme, aquí no acaba mi peregrinación, puesto que, desde la opción de agregar características a una instalación existente, una de las pantallas en la que no dudo la opción es la siguiente:

image

 

Como digo, sin dudar, selecciono “Agregar características a una instancia existente de SQL Server 2008”. ¡Buf!. Llego, casi igual que antes a una pantalla en la que no me deja modificar nada y que, al igual que anteriormente, me devuelve el mismo mensaje de errores de validación.

image

 

¡Ya no puedo más!. Tengo que echar mano de San Google y comprobar que he seleccionado mal la opción de actualizar en vez de seleccionar una nueva instalación.

image

 

No sé, yo ya me hago viejo para estas cosas y ya estaba acostumbradito a tener una pantalla de instalación en la que si quiero agregar nuevas opciones sólo tengo que activar una casilla de verificación y la inversa para desinstalar…

 

Por cierto, la fuente que me salvó de seguir enredado está en http://www.asql.biz/Articoli/SQLX08/Art3_1.aspx.