C#. Unidades de capacidad

No será la primera vez que hemos creado algún sistema de visualización de archivos y, casi siempre solemos poner lo mismo, es decir, un grid en el que aparecen datos como Nombre, Extensión, Tamaño… Hoy me he fijado en el tamaño.

La magnífica clase System.IO.FileInfo nos proporciona casi todos los elementos a los que solemos hacer referencia cuando trabajamos con algún que otro archivo. Las propiedades Name, FullName, Extension y Length quizás sean los elementos más socorridos a la hora de mostrar algún que otro grid con los datos. El “problema” que le veo es que la longitud del fichero siempre nos lo está devolviendo en una misma medida, en bytes.

grid_bytes

 

¿Qué pasa si no quiero que se muestre así?. En un caso particular que me lleva ahora, al tratarse de ficheros bastante grandes, he preferido mostrar las unidades bastante abreviadas, del tipo 20,23 Mb o 34,21 Kb. Algo así como en el siguiente ejemplo:

grid_medidas

 

Bueno, supongo que el ejemplo queda claro, aunque también supongo que habrá alguien que deje el comentario refiriéndose que es más cómodo para ordenar y para hacerse una idea en una misma medida que en varias. ¡Vale!,.es el riesgo de publicar algo en Internet pero… para gustos, colores.

Vamos al grano, es decir, a crear una clase en C# que convierta una determinada cantidad de bytes en cualquier otra más abreviada. Al principio de esta labor, rememorando las clases de fundamentos de programación estuve tentado de hacer algún tipo de algoritmo de búsqueda para poder encontrar cuál era el número por el que podíamos dividir los bytes para que no fuesen 0 que, al fin y al cabo, es la única dificultad que estriba este módulo. Al final veo que también las clases de análisis en las que me intentaban explicar la funcionalidad de los logaritmos dejaron algún tipo de mella en mí.

Manos a la obra.

Necesito crear algún tipo de método en el que, pasándole un número de bytes como parámetro, nos devuelva una unidad abreviada. Así, si pasamos, por ejemplo, 2048 Bytes, el resultado que me devolverá será 2 Kb. En principio, la idea es crear un método estático del tipo.

   1: public static string GetFileSize(long numBytes)
   2: {
   3:     // TODO : Pendiente de implementar 
   4: }

Aunque, al empezar a crear el método, apercibo que será mucho mejor poder especificar desde la capa de presentación el formato. En este apartado no sé si se hace así o hubiese sido mejor echar mano de la UICulture, pero por comodidad he optado por hacer una pequeña “trampilla”. En este caso, la bendita sobrecarga ofrece la posibilidad de especificar el formato siguiendo las normas del formato compuesto, siendo {0} la cantidad y {1} la unidad de medida.

 

   1: public static string GetFileSize(long numBytes)
   2: {
   3:     return GetFileSize(numBytes, "{0:N0} {1}");
   4: }
   5:  
   6: public static string GetFileSize(long numBytes, string format)
   7: {
   8:     // TODO : Pendiente de implementar
   9: }

 

Una vez establecido el inicio y resuelto el problema de cómo localizar la unidad de un modo correcto, sólo nos queda el desarrollo del resto de la clase:

   1: using System;
   2:  
   3:  
   4:  
   5: namespace JnSoftware.Utiles
   6: {
   7:  
   8:     /// <summary>
   9:     /// Servicios de utilidad general de trabajo con ficheros
  10:     /// </summary>
  11:     public class Ficheros
  12:     {
  13:  
  14:         /// <summary>
  15:         /// Posibles capacidades de un archivo
  16:         /// </summary>
  17:         public enum CapacidadEnum
  18:         {
  19:             /// <summary>
  20:             /// Byte
  21:             /// </summary>
  22:             Byte = 0,
  23:             /// <summary>
  24:             /// Kilobyte
  25:             /// </summary>
  26:             Kb = 1,
  27:             /// <summary>
  28:             /// Megabyte
  29:             /// </summary>
  30:             Mb = 2,
  31:             /// <summary>
  32:             /// Gigabyte
  33:             /// </summary>
  34:             Gb = 3,
  35:             /// <summary>
  36:             /// Terabyte
  37:             /// </summary>
  38:             Tb = 4,
  39:             /// <summary>
  40:             /// Petabyte
  41:             /// </summary>
  42:             Pt = 5,
  43:             /// <summary>
  44:             /// Exabyte
  45:             /// </summary>
  46:             Ex = 6,
  47:             /// <summary>
  48:             /// Zetabyte
  49:             /// </summary>
  50:             Zb = 7
  51:         }
  52:  
  53:         private const long baseExp = 1024; // 2 ^10
  54:  
  55:         /// <summary>
  56:         /// Obtiene el valor de la medida en la que nos encontraremos
  57:         /// con valores diferentes de 0.
  58:         /// </summary>
  59:         /// <param name="numBytes">Bytes del fichero</param>
  60:         private int getPotencia(long numBytes)
  61:         {
  62:             return (int)Math.Log(numBytes, baseExp);
  63:         }
  64:  
  65:  
  66:         /// <summary>
  67:         /// Obtiene la unidad de capacidad
  68:         /// </summary>
  69:         /// <param name="potencia">Valor de la medida en la que nos
  70:         /// encontramos con valores diferentes de 0</param>
  71:         private string getUnidadCapacidad(int potencia)
  72:         {
  73:             return Enum.GetName(typeof(CapacidadEnum), potencia);
  74:         }
  75:  
  76:  
  77:  
  78:         private string getSizeAndUnity(long numBytes, string formatoNumerico)
  79:         {
  80:             int pot = getPotencia(numBytes);
  81:             double valor;
  82:             if (pot != 0)
  83:                 valor = numBytes / (Math.Pow(baseExp, pot));
  84:             else
  85:                 valor = numBytes;
  86:  
  87:             string unidad = getUnidadCapacidad(pot);
  88:             return string.Format(formatoNumerico, valor, unidad);
  89:         }
  90:  
  91:  
  92:  
  93:         /// <summary>
  94:         /// Estandariza el tamaño pasado como parámetro
  95:         /// en unidad de capacidad informática
  96:         /// </summary>
  97:         /// <param name="numBytes">Bytes del fichero</param>
  98:         /// <returns>
  99:         /// Cadena de texto en formato "{0:N0} {1}", siendo el 
 100:         /// primer parámetro el número de bytes y el segundo parámetro
 101:         /// la unidad de capacidad
 102:         /// </returns>
 103:         public static string GetFileSize(long numBytes)
 104:         {
 105:             return GetFileSize(numBytes, "{0:N0} {1}");
 106:         }
 107:  
 108:  
 109:  
 110:         /// <summary>
 111:         /// Estandariza el tamaño pasado como parámetro
 112:         /// en unidad de capacidad informática
 113:         /// </summary>
 114:         /// <param name="numBytes">Bytes del fichero</param>
 115:         /// <param name="format">Cadena de formato compuesto.
 116:         ///     {0} = Cantidad numérica
 117:         ///     {1} = Unidad de medida
 118:         /// </param>
 119:         /// <returns>
 120:         /// Cadena de texto en formato "{0:N0} {1}", siendo el 
 121:         /// primer parámetro el número de bytes y el segundo parámetro
 122:         /// la unidad de capacidad</returns>
 123:         public static string GetFileSize(long numBytes, string format)
 124:         {
 125:             Ficheros f = new Ficheros();
 126:             return f.getSizeAndUnity(numBytes, format);
 127:         }
 128:  
 129:  
 130:  
 131:     }
 132: }

Observaciones

Imagino que alguien me comentará que me he dejado el YottaByte en la enumeración. ¡Pues vale!, Pasando de los Mb, el resto lo puse más porque me hacía gracia que por la utilidad. Hasta el momento no suelo estar habituado a trabajar con ficheros que superen los Tb.

También me anduve mirando otro punto un poco delicado, referente a los prefijos binarios, sobretodo, por las diferencias existentes entre las medidas de discos y las estándares y, al final he optado por seguir la unidad estándar. Al fin y al cabo, la necesidad inicial era la de trabajar con ficheros almacenados en un servidor Web… No sé si realmente notaré mucho las diferencias hablando de mebis o megas…

LastUpdateUser y LastUpdateDate en capa de negocios

Desde los primeros programitas que desarrollé, algo que me ha gustado tener y que, en más de una ocasión me ha servido para descubrir algún que otro problemilla ha sido guardar en las tablas de la base de datos los campos LastUpdateUser y LastUpdateDate con la finalidad de almacenar el usuario y la fecha de la última modificación. Este proceso, ya desde los tiempos de VB5 era un proceso bastante sencillo desde cualquier apartado de la aplicación, fuese un fichero .EXE o una .DLL. En principio, si la memoria no me falla, sólo había que hacer una llamada a la función GetUserName de la librería advapi32.dll.

Ha cambiado todo bastante desde aquellos tiempos y, lo más importante, ha cambiado el entorno de trabajo. Ahora ya no hace falta hacer llamadas a las API, o mejor dicho, sí pero muchísimo más sencillo que antaño. En el namespace System nos encontramos con una clase muy útil, Environment que incluye entre otras cosas, una propiedad UserName que nos devuelve el nombre de usuario que ha iniciado la sesión en Windows. Hasta aquí está todo bien, siempre que estemos trabajando en un entorno WinForms y siempre que la aplicación completa esté siendo ejecutada en la máquina del cliente.

En ASP.NET el problema es bien distinto dependiendo del escenario y realmente es uno bastante frecuente puesto que pocas son las aplicaciones ya que incluyen en el mismo proyecto la lógica de negocios y de presentación juntas.

¿Dónde nos encontramos realmente el problema?. Como siempre, intento explicarlo con un ejemplo. Pongamos que tenemos una solución con dos proyectos: Web como capa de presentación y BAL como capa de negocios. Ya tenemos montados (y funcionando) todo el tema de membresías y usuarios para poder acceder. Desde la Web sí que podemos tener el nombre del usuario actual

   1: string usuarioActual = User.Identity.Name;

pero sólo desde la web. Si necesitamos tener el mismo dato desde la capa de negocios, deberíamos ir pasando este dato como parámetro en cualquiera de los métodos que alteren datos.

   1: GuardaDatos(dato1,dato2,...,usuarioActual);

¿No quedaría mucho mejor que este dato lo tuviésemos ya en la capa de negocio para hacer algo similar a lo siguiente?:

   1: public void GuardaDatos(string dato1, int dato2)
   2: {
   3:     ...
   4:     c.Dato1 = dato1;
   5:     c.Dato2 = dato2;
   6:     c.LastUpdateUser = xxxx;
   7:     c.LastUpdateDate = System.DateTime.Now;
   8:     ...
   9: }

El problema nos lo encontramos en lo que he denominado xxxx en el ejemplo, aunque para renombrarlo un poco, vamos a darle un nombre diferente creando, por ejemplo, una propiedad estática en una nueva clase a la que denominaremos Configuracion.

   1: namespace BAL
   2: {
   3:     public class Configuracion
   4:     {
   5:         private static string _usuario = "DESCONOCIDO";
   6:  
   7:         /// <summary>
   8:         /// Obtiene o establece el usuario identificado en la aplicación Web
   9:         /// </summary>
  10:         public static string UsuarioActual
  11:         {
  12:             get { return _usuario; }
  13:             set { _usuario = value; }
  14:         }
  15:     }
  16: }

Ahora, con esta clase, lo único que nos queda es establecer el valor de la propiedad UsuarioActual. Para ello, tendremos que crearnos un clase de aplicación global, es decir, el fichero Global.asax que nos permitirá capturar el evento AuthenticateRequest de la clase HttpApplication. De esta manera, cada vez que se realice una solicitud sobre cualquier página, estaremos estableciendo el valor de la propiedad UsuarioActual con el nombre del usuario (en el caso que haya sido satisfactoria la autenticación).

   1: protected void Application_AuthenticateRequest(object sender, EventArgs e)
   2: {
   3:     BAL.Configuracion.UsuarioActual =
   4:       Request.IsAuthenticated
   5:       ? User.Identity.Name
   6:       : "DESCONOCIDO";
   7: }

¡Cómo me gusta el operador ternario de C#!. Aprendí a usarlo en Java y creo que es de lo más cómodo.

C#. NombrePropio

Lamento tener tan abandonado el blog pero es que ando inmerso en un proyecto de aquellos que te absorben mucho más tiempo del que te gustaría. Intentaremos sacar algo de tiempo ahora que, por lo menos ya no tengo clases.

Bueno, hoy le toca el turno a una de aquellas pequeñas cosas a las que, los que vais siguiendo el blog ya estáis acostumbrados. El caso es que el otro día, charlando con el jefe me comenta lo mal que queda que los clientes vean los datos unos en mayúscula, otros en formato Título, otros en minúscula… Sí, vale, tiene razón, queda feo. Con la naturalidad que me caracteriza le digo que no hay problema, que le pongo todo en mayúsculas y que así no hay diferencias pero no tarda en comentarme que eso es antiguo, que lo quiere en Nombre Propio, al igual que la función NOMPROPIO/PROPER del Excel.

Ale… para la “validación” de cualquier dato de texto, nos encontramos con los métodos ToUpper() y ToLower() de la clase System.String, pero no existe un ToNombrePropio() pues intentaremos crearlo nosotros.

El módulo en sí no es complicado puesto que la clase CultureInfo tiene una propiedad TextInfo que nos permitirá acceder a un método ToTitleCase() que ya hace precisamente lo que andábamos buscando.

Por tanto, lo único que nos queda es ponernos manos a la obra intentando seguir las mismas directrices que siguen los métodos ToLower() o ToUpper(), es decir, crear un método sobrecargado que nos permita, en caso necesario, pasar la cultura deseada.

   1: using System.Globalization;
   2: using System.Threading;
   3:  
   4: namespace System
   5: {
   6:     public static class StringExtensors
   7:     {
   8:         /// <summary>
   9:         /// Devuelve una copia de este objeto String en formato Título aplicando
  10:         /// las reglas de la cultura actual
  11:         /// </summary>
  12:         public static string ToNombrePropio ( this System.String  texto )
  13:         {
  14:             return texto.ToNombrePropio(Thread.CurrentThread.CurrentCulture);
  15:         }
  16:  
  17:         /// <summary>
  18:         /// Devuelve una copia de este objeto String en formato título aplicando
  19:         /// las reglas de la cultura especificada
  20:         /// </summary>
  21:         /// <param name="culture">Objeto System.Globalization.CultureInfo que proporciona
  22:         /// reglas de Titulo</param>
  23:         public static string ToNombrePropio( this System.String texto, CultureInfo culture)
  24:         {
  25:             TextInfo ti = culture.TextInfo;
  26:             return ti.ToTitleCase(texto);
  27:         }
  28:     }
  29: }

 

Eso sí, cuando andaba documentándome en la ayuda de Microsoft sobre el método ToTitleCase(), he visto que aparece el siguiente mensaje:

Como se muestra anteriormente, el método ToTitleCase() proporciona un comportamiento de grafía arbitrario que no es necesariamente lingüísticamente correcto. Una solución correcta lingüísticamente requeriría reglas adicionales y el algoritmo actual es algo más sencillo y rápido.Nos reservamos el derecho para hacer esta API más lenta en el futuro

No obstante, en la versión 4.0 del Framework todavía no se ha modificado…