Generador de sopa de letras hecho en Ruby

agosto 16, 2019

A mi hija (que al momento de escribir esto tiene 9 años) le gusta resolver sopa de letras, es ese juego que trata de encontrar en una cuadrícula llena de letras una lista de palabras.

Se me ocurrió hacer un programa que generara sopas de letras y así tener una fuente inagotable de éstas y con listas de palabras personalizadas.

En esta primera versión, hay que poner directamente en el código la lista de palabras (es un arreglo) y el programa sólo pone las palabras de forma horizontal o vertical. En versiones posteriores haré los cambios necesarios para que la lista de palabras la capture el usuario al inicio, también haré que el programa pueda acomodar palabras en diagonal y que al final pregunte al usuario si desea generar un archivo pdf de la sopa de letras generada.

Aquí un ejemplo del resultado de una ejecución

Y aquí el código fuente

Archivo sopa_letras.rb

# Regresa un arreglo cuyos elementos son las letras de la A a la Z (en mayúsculas)
def abecedario
    return Array('A'..'Z')
end

# Regresa una cadena de longitud caracteres tomados al azar del arreglo obtenido de llamar a la función abecedario
def cadena_aleatoria(longitud)
    return Array.new(longitud) { abecedario.sample }.join
end

# Reemplaza los ceros en el tablero con letras al azar
def reemplaza_ceros
    for i in 0...@tablero[0].length
        for j in 0...@tablero[0].length
            if @tablero[i][j] == 0
               @tablero[i][j] = abecedario.sample
            end
        end
    end
end

def imprime_tablero
    @tablero.each_with_index do |renglon, indice_r|
       renglon.each_with_index do |columna, indice_c|
          print "#{columna} "
       end
       puts
    end
    puts
end

# Busca en el tablero un renglón en donde existan longitud número de ceros consecutivos
# y regresa la posición del renglón y columna en caso de encontrarlo; de lo contrario
# regresa -1 para el renglón y -1 para la columna
def cabe_horizontal(longitud)
    fila = -1
    columna = -1
    
    # Obtiene al azar un número entre 1 y 2
    # Si el número es uno, intenta poner la palabra de forma horizontal empezando por el primer renglón
    # e irá bajando hasta el último
    # Si el número es dos, intenta poner la palabra de forma horizontal empezando por el último renglón
    # e irá subiendo hasta el primero
    inicio = rand(1..2)
    
    if inicio == 1
       @tablero.each_with_index do |renglon, indice_r|
          # Si en el renglón actual no hay ceros, procesa el siguiente renglón
          next if !renglon.include?(0)
          
          # Guarda la posición de la columna del primer cero que se encuentra en el renglón actual
          primer_cero = renglon.index(0)
          
          columna = primer_cero
          contador = 0
          for i in primer_cero...renglon.length
              if renglon[i] == 0
                 contador += 1
                 columna = i if contador == 1
              else
                  # Si el número de ceros consecutivos en el renglón actual es suficiente
                  # se sale del ciclo, de lo contrario inicializa contador y la columna
                  # en donde se empezaría a contar el número de ceros sería la actual (es decir, i)
                  break if contador >= longitud
                  contador = 0
              end
          end
          # Si el número de ceros consecutivos en el renglón actual es mayor o igual a la longitud
          # recibida como parámetro, no busca en los renglones restantes
          if contador >= longitud
             fila = indice_r
             break
          end
       end
    else
        (@tablero[0].size - 1).downto(0) do |indice_r|
           # Si en el renglón actual no hay ceros, procesa el siguiente renglón
           next if !@tablero[indice_r].include?(0)
           
           # Guarda la posición de la columna del primer cero que se encuentra en el renglón actual
           primer_cero = @tablero[indice_r].index(0)
           
           columna = primer_cero
           contador = 0
           for i in primer_cero...@tablero[indice_r].length
               if @tablero[indice_r][i] == 0
                  contador += 1
               else
                   # Si el número de ceros consecutivos en el renglón actual es suficiente
                   # se sale del ciclo, de lo contrario inicializa contador y la columna
                   # en donde se empezaría a contar el número de ceros sería la actual (es decir, i)
                   break if contador >= longitud
                   contador = 0
                   columna = i
               end
           end
           # Si el número de ceros consecutivos en el renglón actual es mayor o igual a la longitud
           # recibida como parámetro, no busca en los renglones restantes
           if contador >= longitud
              fila = indice_r
              break
           end
        end
    end
    
    return fila, columna
end

def cabe_vertical(longitud)
    fila = -1
    columna = -1
    @tablero.each_with_index do |renglon, indice_r|
       # Si en el renglón actual no hay ceros, procesa el siguiente renglón
       next if !renglon.include?(0)
       
       # Crea un arreglo con los números de columna que contienen ceros en el renglón actual
       columnas_cero = renglon.each_index.select { |indice| renglon[indice] == 0}
       columna = columnas_cero[0]
       
       # Para cada una de las columnas de columnas_cero, va contando los ceros consecutivos hacia abajo
       columnas_cero.each do |j|
          contador = 0
          columna = j
          # Sin cambiar de columna, empieza en el renglón actual y va contando los ceros en los renglones posteriores
          for i in indice_r...renglon.length
              if @tablero[i][j] == 0
                 contador += 1
              else
                  break
              end
          end
          # Si el número de ceros consecutivos en la columna actual es mayor o igual a la longitud
          # recibida como parámetro, no busca en las columnas restantes
          if contador >= longitud
             fila = indice_r
             break
          end
       end
       
       # Si el valor de la variable fila es mayor a cero, significa que iniciando en el renglón actual
       # se encontró una columna con suficientes ceros hacia abajo
       break if fila >= 0
    end
    
    return fila, columna
end

palabras = ["perro", "gato", "conejo", "elefante", "chimpance", "tigre", "pantera", "lobo", "panda", "perico"]

# Encuentra cuál es la cadena de mayor longitud en el arreglo palabras
longest_string = palabras.max_by(&:length)
mayor_longitud = longest_string.length

# Crea una copia del arreglo palabras
palabras_respaldo = palabras[0..palabras.length]

# Crea un arreglo llamado letras_azar que contendrá cadenas formadas por letras tomadas al azar
letras_azar = []
for i in 1..(mayor_longitud/2).to_int
    letras_azar.push(cadena_aleatoria(rand(1..3)))
end

# Agrega el contenido del arreglo letras_azar al arreglo palabras
palabras.concat(letras_azar)
# Revuelve los elementos del arreglo aleatoriamente
palabras.shuffle!

# Crea una matriz de longitud+3 x longitud+3 y la llena de ceros
@tablero = Array.new(mayor_longitud+3){Array.new(mayor_longitud+3,0)}

# Va sacando al azar elementos del arreglo palabras
while !palabras.empty? do
      selecciona = rand(palabras.length)
      # Copia la palabra que se encuentra en la posición selecciona del arreglo palabras
      palabra_seleccionada = palabras[selecciona].dup
      # Elimina del arreglo palabaras la palabra que se encuentra en la posición selecciona
      palabras.delete_at(selecciona)
      
      # Decide al azar si pondrá la palabra invertida o no
      invertir = [true, false].sample
      palabra_seleccionada.reverse! if invertir
      
      # Obtiene al azar un número entre 1 y 2
      # Si el número es uno, intenta poner la palabra de forma horizontal y si no es posible
      # después lo intenta de forma vertical
      # Si el número es dos, intenta poner la palabra de forma vertical y si no es posible
      # después lo intenta de forma horizontal
      opcion = rand(1..2)
      
      case opcion
           when 1
                # Verifica si la palabra seleccionada se puede poner de forma horizontal
                # en algún renglón del tablero
                row, column = cabe_horizontal(palabra_seleccionada.length)
                
                if row>=0 and column>=0
                   # Pone la palabra de forma horizontal en la posición indicada por los valores de las variables row y column
                   palabra_seleccionada.each_char do |letra|
                      @tablero[row][column] = letra.upcase
                      column +=1
                   end
                else
                    # Verifica si la palabra seleccionada se puede poner de forma vertical
                    # en algún renglón del tablero
                    row, column = cabe_vertical(palabra_seleccionada.length)
                    
                    if row>=0 and column>=0
                       # Pone la palabra de forma vertical en la posición indicada por los valores de las variables row y column
                       palabra_seleccionada.each_char do |letra|
                          @tablero[row][column] = letra.upcase
                          row +=1
                       end
                    end
                end
         when 2
              # Verifica si la palabra seleccionada se puede poner de forma vertical
              # en algún renglón del tablero
              row, column = cabe_vertical(palabra_seleccionada.length)
              
              if row>=0 and column>=0
                 # Pone la palabra de forma vertical en la posición indicada por los valores de las variables row y column
                 palabra_seleccionada.each_char do |letra|
                    @tablero[row][column] = letra.upcase
                    row +=1
                 end
              else
                  # Verifica si la palabra seleccionada se puede poner de forma horizontal
                  # en algún renglón del tablero
                  row, column = cabe_horizontal(palabra_seleccionada.length)
                  
                  if row>=0 and column>=0
                     # Pone la palabra de forma horizontal en la posición indicada por los valores de las variables row y column
                     palabra_seleccionada.each_char do |letra|
                        @tablero[row][column] = letra.upcase
                        column +=1
                     end
                  end 
              end
      end
end

# Reemplaza los ceros en el tablero con letras al azar
reemplaza_ceros

# Muestra la lista de palabras a encontrar en la sopa de letras
puts "SOPA DE LETRAS"
puts "="*50
puts "PALABRAS A ENCONTRAR:"
puts
palabras_respaldo.sort.each do |palabra|
   puts palabra.upcase
end
puts
puts "*"*50
imprime_tablero
Anuncios

Instalar el verdadero MySQL en Debian 9

mayo 30, 2019

Por si alguien no lo sabe, en los repositorios de Debian 9 ya no viene incluido MySQL, en su lugar viene MariaDB. El paquete llamado mysql-server, depende del paquete default-mysql-server y este depende del paquete mariadb-server-10.1.

Pueden encontrar más información al respecto en los siguientes enlaces:

Moving from MySQL to MariaDB in Debian 9
Debian 9 released with MariaDB as the only MySQL variant

Para instalar el verdadero MySQL debemos incluir los repositorios APT de MySQL para Debian en el archivo /etc/apt/sources.list agregandole las siguientes líneas

deb http://repo.mysql.com/apt/debian/ stretch mysql-5.7
deb-src http://repo.mysql.com/apt/debian/ stretch mysql-5.7

Antes de ejecutar apt update, debemos descargar el archivo RPM-GPG-KEY-mysql de https://repo.mysql.com

wget https://repo.mysql.com/RPM-GPG-KEY-mysql

Agregamos el paquete a apt keyring

sudo apt-key add RPM-GPG-KEY-mysql

Ahora si actualizamos la lista de paquetes
sudo apt update

Instalamos MySQL
sudo apt install mysql-server

Nos pide que asignemos un password para el usuario root de MySQL

Y eso es todo, tenemos instalado MySQL y no MariaDB

Instalar nginx + tomcat en Debian 9

febrero 1, 2019

Es muy común que aplicaciones web desarrolladas con Java Server Pages (JSP) se ejecuten con Apache + Tomcat. A mi me gusta más usar Nginx como servidor wen en lugar de Apache; así que en este post voy a poner los pasos a seguir para ejecutar en Debian 9 aplicaciones web desarrolladas con JSP usando Nginx en lugar de Apache.

Instalar Java

sudo apt install openjdk-8-jdk

Aparece lo siguiente:

Leyendo lista de paquetes… Hecho
Creando árbol de dependencias
Leyendo la información de estado… Hecho
Se instalarán los siguientes paquetes adicionales:
libice-dev libpthread-stubs0-dev libsm-dev libx11-dev libx11-doc libxau-dev
libxcb1-dev libxdmcp-dev libxt-dev openjdk-8-jdk-headless x11proto-core-dev
x11proto-input-dev x11proto-kb-dev xorg-sgml-doctools xtrans-dev
Paquetes sugeridos:
libice-doc libsm-doc libxcb-doc libxt-doc openjdk-8-demo openjdk-8-source
visualvm
Se instalarán los siguientes paquetes NUEVOS:
libice-dev libpthread-stubs0-dev libsm-dev libx11-dev libx11-doc libxau-dev
libxcb1-dev libxdmcp-dev libxt-dev openjdk-8-jdk openjdk-8-jdk-headless
x11proto-core-dev x11proto-input-dev x11proto-kb-dev xorg-sgml-doctools
xtrans-dev
0 actualizados, 16 nuevos se instalarán, 0 para eliminar y 0 no actualizados.
Se necesita descargar 13.7 MB de archivos.
Se utilizarán 60.0 MB de espacio de disco adicional después de esta operación.
¿Desea continuar? [S/n]

Decimos que si

Instalar Tomcat

sudo apt install tomcat8

Aparece lo siguiente:

Leyendo lista de paquetes… Hecho
Creando árbol de dependencias
Leyendo la información de estado… Hecho
Se instalarán los siguientes paquetes adicionales:
authbind libapr1 libcommons-dbcp-java libcommons-pool-java libecj-java
libtcnative-1 libtomcat8-java tomcat8-common
Paquetes sugeridos:
libcommons-dbcp-java-doc libgeronimo-jta-1.1-spec-java ecj libecj-java-gcj
tomcat8-admin tomcat8-docs tomcat8-examples tomcat8-user
Se instalarán los siguientes paquetes NUEVOS:
authbind libapr1 libcommons-dbcp-java libcommons-pool-java libecj-java
libtcnative-1 libtomcat8-java tomcat8 tomcat8-common
0 actualizados, 9 nuevos se instalarán, 0 para eliminar y 0 no actualizados.
Se necesita descargar 7 092 kB de archivos.
Se utilizarán 9 161 kB de espacio de disco adicional después de esta operación.
¿Desea continuar? [S/n]

Decimos que si

Instalar nginx

sudo apt install nginx

Aparece lo siguiente:

Leyendo lista de paquetes… Hecho
Creando árbol de dependencias
Leyendo la información de estado… Hecho
Se instalarán los siguientes paquetes adicionales:
libnginx-mod-http-auth-pam libnginx-mod-http-dav-ext libnginx-mod-http-echo
libnginx-mod-http-geoip libnginx-mod-http-image-filter
libnginx-mod-http-subs-filter libnginx-mod-http-upstream-fair
libnginx-mod-http-xslt-filter libnginx-mod-mail libnginx-mod-stream
nginx-common nginx-full
Paquetes sugeridos:
fcgiwrap nginx-doc ssl-cert
Se instalarán los siguientes paquetes NUEVOS:
libnginx-mod-http-auth-pam libnginx-mod-http-dav-ext libnginx-mod-http-echo
libnginx-mod-http-geoip libnginx-mod-http-image-filter
libnginx-mod-http-subs-filter libnginx-mod-http-upstream-fair
libnginx-mod-http-xslt-filter libnginx-mod-mail libnginx-mod-stream nginx
nginx-common nginx-full
0 actualizados, 13 nuevos se instalarán, 0 para eliminar y 0 no actualizados.
Se necesita descargar 1 588 kB de archivos.
Se utilizarán 2 865 kB de espacio de disco adicional después de esta operación.
¿Desea continuar? [S/n] s

Decimos que si

En un navegador web nos vamos a localhost y nos debe aparecer la siguiente pantalla, que significa que nginx quedó correctamente instalado

Crear la estructura de directorios

Creamos un directorio llamado ejemplo dentro de /var/lib/tomcat8/webapps

sudo mkdir /var/lib/tomcat8/webapps/ejemplo

Posteriormente creamos los siguientes directorios

ejemplo (este es el directorio que acabamos de crear)
|
|--- src
|--- WEB-INF
     |
     |--- classes
     |--- lib

sudo mkdir /var/lib/tomcat8/webapps/ejemplo/src

sudo mkdir /var/lib/tomcat8/webapps/ejemplo/WEB-INF

sudo mkdir /var/lib/tomcat8/webapps/ejemplo/WEB-INF/classes

sudo mkdir /var/lib/tomcat8/webapps/ejemplo/WEB-INF/lib

Dentro del direcorio src irán los archivos *.java

En el directorio WEB-INF debe existir un archivo llamado web.xml con el siguiente contenido


<?xml version="1.0" encoding="ISO-8859-1"?>

<!DOCTYPE web-app
PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app>
</web-app>

En WEB-INF/classes van los archivos *.class y los archivos *.jar van en WEB-INF/lib
Por último, en nuestro directorio ejemplo van los *.jsp, *.js, *.html y demás contenido estático.

Crear nuestro archivo .jsp

En el directorio /var/lib/tomcat8/webapps/ejemplo vamos a crear un archivo llamado ciclo.jsp

sudo nano /var/lib/tomcat8/webapps/ejemplo/ciclo.jsp

El contenido es el siguiente:

<body>

Ejemplo JSP con nginx<br>

<%
for(int i = 0; i < 10; i++)
   {
    out.println("El valor de la variable i es: " + i + "<br>");
   }
%>
Fin del ciclo <br>
</body>

Para salir del editor nano, pulsamos las teclas Ctrl y X simultáneamente, nos pregunta si queremos guardar los cambios, a lo que respondemos que si.

En el directorio /etc/nginx/sites-available vamos a crear un archivo llamado ejemplo.conf

sudo nano /etc/nginx/sites-available/ejemplo.conf

El contenido del archivo es el siguiente:

server { 
  listen 80; 
  server_name localhost;
  
  root /var/lib/tomcat8/webapps/ejemplo; 
  index index.jsp index.html index.htm;
  
  charset utf-8; 
  client_max_body_size 4G; 
  
  location / { 
         try_files $uri $uri/ =404;
  }
  
  location ~ \.jsp$ { 
         proxy_pass http://127.0.0.1:8080;
         proxy_set_header X-Real-IP $remote_addr; 
         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 
         proxy_set_header X-Forwarded-Proto $scheme;
         proxy_set_header Host $host; 
         proxy_redirect off; 
         proxy_http_version 1.1;
  }
  
  #error_page 404 /404.html; 
  
  # redirect server error pages to the static page /50x.html 
  
  error_page 500 502 503 504 /50x.html; 
  location = /50x.html { 
         root html; 
  } 
}

Hacemos un enlace simbólico en el directorio /etc/nginx/sites-enabled que apunte al archivo de configuración que acabamos de crear en /etc/nginx/sites-available

sudo ln -s /etc/nginx/sites-available/ejemplo.conf /etc/nginx/sites-enabled/ejemplo.conf

Modificamos el archivo /etc/nginx/nginx.conf cambiando la línea
include /etc/nginx/sites-enabled/*;
por
include /etc/nginx/sites-enabled/*.conf;

Reiniciamos nginx

sudo service nginx restart

Configurar Tomcat para que trabaje en conjunto con nginx

Hacemos una copia de respaldo del archivo /etc/tomcat8/server.xml

cd /etc/tomcat8
sudo cp server.xml server_ORIG.xml

Editamos el archivo server.xml de tal forma que abajo de la línea <Engine name="Catalina" defaultHost="localhost">
quede lo siguiente:
<Valve className="org.apache.catalina.valves.RemoteIpValve"
       internalProxies="127\.0\.[0-1]\.1"
       remoteIpHeader="x-forwarded-for"
       requestAttributesEnabled="true"
       protocolHeader="x-forwarded-proto"
       protocolHeaderHttpsValue="https"/>

Después de guardar el archivo, reiniciamos Tomcat

sudo service tomcat8 restart

Probando la aplicación

En nuestro navegador web vamos a localhost/ejemplo/ciclo.jsp y el resultado debe ser el siguiente

Post 3: ¿Por qué no hago “coding challenges”?

enero 21, 2019

Es una práctica bastante extendida que las empresas subcontraten a otra empresa para hacer la búsqueda de talento cuando requieren contratar personal. La empresa que tiene el puesto vacante pasa a la agencia que hará la búsqueda de talento la descripción del puesto, los requisitos y el salario (o un rango salarial) y prestaciones que ofrecen.

Hace ya algún tiempo que empresas que buscan cubrir vacantes en el área de tecnologías de la información, específicamente para desarrollo de software, acostumbran incluir en los requisitos que se les pida a los candidatos hacer los llamados “coding challenges”, que básicamente consisten en hacer pequeños programas como una función, etc. en algún lenguaje de programación determinado.

Esto es aceptable cuando la empresa busca contratar un recién egresado o alguien que haga sus prácticas profesionales, porque desafortunadamente en México, el nivel de conocimientos de los egresados de alguna carrera relacionada con tecnologías de la información es bastante malo en la mayoría de los casos.

Lo que muchas de estas empresas parece que no han captado es que no puedes aplicar el mismo proceso de selección para alguien con experiencia. Cualquier profesional de TI enfocado en el desarrollo de software que se mantenga activo, normalmente tiene código disponible públicamente en internet al cual se puede tener acceso de forma gratuita, ya sea en una cuenta de github, en un blog, en youtube, etc. En el muy poco probable caso de que nunca hubiera hecho público algo de código programado por él, seguramente ha escrito muchos programas y podrá enviarle a la empresa el código fuente de alguno de ellos.

En lo personal yo he hecho público mucho código desarrollado por mi en este blog, y en otros lugares. Incluso el código fuente de mi proyecto final de maestría esta disponible en sourceforge.net. Cuando una empresa que me contacta para una vacante relacionada con desarrollo de software me pide que haga un “coding challenge” después de haber visto mi CV, sé que esa empresa no pone interés en los CV que recibe; ya que de ser así hubiera visto que en mi CV hay un enlace a mi blog, en el cual hay suficiente código.

No hago “coding challenges” porque si una empresa que busca ofrecerme un empleo que implique desarrollar software requiere pruebas de que sé programar, las pruebas están a la vista y ahí han estado durante años, si no quiere tomarse la molestia de revisarlas, entonces no me interesa, por lo que no tiene caso que los haga perder su tiempo ni que yo invierta el mio. Si me tratan como recién egresado, lo más probable es que me ofrezcan un salario de recién egresado, mismo que no voy a aceptar, así de simple.

El que alguien me pida que haga un examen de programación pudiendo ver todo el código que he puesto libremente y de manera gratuita a disposición del público en general, es tanto como decirme mentiroso, es una falta de respeto; en el mejor de los casos el mensaje que envía es de desconfianza y no estoy interesado en establecer una relación laboral (o de algún otro tipo) cuando hay desconfianza de la otra parte.

Post 2: ¿Cómo selecciono las herramientas que utilizo para desarrollar software?

enero 10, 2019

Debido a que existen muchos lenguajes de programación y herramientas para desarrollar software, es imposible saber todos los lenguajes y conocer todas las herramientas disponibles.

He visto que muchos desarrollan en lo que la mayoría usa o lo que está de moda.
En este post voy a describir cómo es que yo elegí algunos de los lenguajes de programación y herramientas que uso para desarrollar software.

Empece a crear programas para computadoras hace más de 30 años, el primer lenguaje de programación que aprendí fue Basic, porque era el único con el que podía programar la primer computadora a la que tuve acceso (mi hermano y yo vendimos nuestro Atari 2600 para comprar una computadora Commodore 16).

Después de varios años desarrollando software, ya sea en COBOL para una VAX 4200, o aplicaciones Desktop en Clipper, C++ y algunos otros lenguajes (antes no existía la web), llega el momento de tener que aprender a desarrollar aplicaciones web (no me refiero a una página HTML estática, sino a sistemas multiusuario con acceso a base de datos, etc.).

Aprendí Java e hice pruebas con servlets y Tomcat, no estuvo mal pero tenía que probar otros lenguajes y herramientas, así que probé con PHP; tampoco estuvo mal pero empezó a sonar por todos lados Ruby on Rails así que decidí probarlo. Leí la documentación del sitio web oficial, seguí unos tutoriales y un curso gratuito en internet y después me dispuse a hacer una aplicación (no es lo mismo un ejemplo de un tutorial o un curso que el desarrollo de un sistema real).

Una vez que ya había hecho el diagrama entidad-relación de la base de datos para la aplicación que iba a crear, empiezo a intentar crear los modelos en Rails y empiezo a descubrir algunos de los varios problemas que tiene ActiveRecord (el ORM que usa Rails), como el hecho de que la llave primaria de cada tabla tenga que estar formada por un sólo campo, que dicho campo deba llamarse id, ser de tipo entero y autoincrementable. Hay formas de “forzar” a Rails para que el campo que será la llave primaria no tenga que llamarse id (una forma de hacerlo se describe en este post http://ruby-journal.com/how-to-override-default-primary-key-id-in-rails/), pero hay otros problemas más, algunos de ellos los menciono en este post ¿Cómo diferenciar un buen desarrollador de software en Ruby del resto? en mi blog.

No tardé mucho tiempo en darme cuenta que debido a la filosofía de Rails de “Convention over Configuration”, estaba invirtiendo mucho tiempo tratando de implementar un modelo de base de datos bien diseñado en Rails. Así que en mi opinión Rails es un mal framework si quieres desarrollar aplicaciones bien hechas (con un buen diseño de base de datos para empezar). Busqué otros frameworks para desarrollo web con Ruby y vi algunos pero también eran “opinionated” como Rails, así que eso no me servía, hasta que descubrí Sinatra. En Sinatra no estás obligado a usar ActiveRecord, puedes usar otro ORM, así que intenté desarrollar mi aplicación utilizando Datamapper y pude implementar la base de datos tal y como la había diseñado, sin tener que estarme peleando con la herramienta.

La conclusión fue: Ruby si, Rails no.

No me gustan las herramientas que me quieren obligar a hacer las cosas de cierta manera, y menos si esa manera es la manera incorrecta de hacer las cosas. El hecho de que Rails te “obligue” a que la llave primaria de cada tabla esté formada por un solo campo llamado id de tipo entero autoincrementable porque “es más fácil” así, no significa que sea la forma correcta. Mi prioridad es hacer las cosas bien, no hacerlas de la forma que me resulte más fácil sin importar si están mal hechas. Pero resulta que puedas hacer las cosas bien y “fácil” si usas las herramientas correctas.

No está mal si un día usas Rails probablemente para hacer una aplicación que se va a usar una sóla vez y se necesita terminarla rápido, por ejemplo si necesitas crear un sistema para el registro de los asistentes a un congreso y ya tienes muy poco tiempo, ahí no importa mucho que la base de datos esté mal diseñada, es decir, como Rails la necesita para que funcione sin interponerse en tu camino.

Desarrollé unas cuantas aplicaciones con Sinatra y Datamapper (aplicaciones reales que actualmente están instaladas y funcionando), pero como siempre busco la mejor forma de hacer las cosas, encontré otro ORM llamado Sequel, que es más versátil que Datamapper, así que desarrollé una aplicación con Sinatra y Sequel para probarlo y quedé muy satisfecho. Desde entonces las aplicaciones web que he desarrollado (que también están instaladas y funcionando en producción) han sido utilizando Ruby con Sinatra y Sequel (además de otras herramientas desde luego).

Continuamente estoy aprendiendo y probando diferentes lenguajes y herramientas para desarrollo de software, desde luego pruebo lo que todos usan y pruebo alternativas y muy frecuentemente encuentro cosas mejores, y eso es lo que uso, no me importa que no sea lo que la mayoría usa. Yo uso lo que he probado que funciona mejor y con lo que me siento más cómodo trabajando. No decido usar un lenguaje de programación o herramienta sólo porque “hay muchas empresas que lo usan”, o “es lo que las empresas están solicitando si buscas trabajo”.

Como dice un refrán asiático: “Un tigre no pierde el sueño por la opinión de las ovejas”.

Otra combinación de lenguaje de programación y framework para desarrollo web que utilizaría es Python con Flask, (ya probé Django y aunque no es tan “opinionated” como Rails, no me ofrece la libertad que yo requiero para sentirme cómodo trabajando). No he tenido tiempo de hacer muchas pruebas con Flask pero lo poco que he probado me ha parecido bien.

Para concluir este post, mi recomendación es que si te interesa utilizar un lenguaje de programación o herramienta porque está de moda, te informes leyendo la documentación oficial, si te sigue llamando la atención lo aprendas y lo pruebes pero busques alternativas, probablemente encuentres algo mejor o que su performance y otras cualidades sean similares pero con lo cual te sientas más cómodo trabajando. Si lo encuentras, que no te importe que los demás te digan “pero la mayoría usa X en lugar de lo que tú estás usando” o algún otro argumento tonto; y si no lo encuentras, al menos estarás usando lo que usas no porque esté de moda sino porque realmente es lo que mejor funciona para ti.

Post 1: Las prácticas profesionales

enero 7, 2019

Recuerdo que en la universidad en donde estudié la licenciatura, a partir de quinto semestre podíamos realizar algo que llamaban “prácticas profesionales”, consistía en cubrir cierto número de horas trabajando en algún lugar y dicho lugar expedía un documento dirigido a la universidad en donde confirmaba que habías trabajado ahí dicho número de horas.

Estando en quinto semestre un compañero de carrera nos comento a otro estudiante y a mí que se había enterado que en una dependencia de gobierno existía la posibilidad de realizar las prácticas profesionales, fuimos los 3 y nos entrevistó una persona de dicha dependencia de gobierno.
Como yo quería que mis prácticas profesionales realmente me fueran útiles hice tres preguntas fundamentalmente.

  1.  ¿Qué trabajo es el que realizaría?
  2.  ¿Cuál es el horario?
  3.  ¿Hay alguna remuneración ?

No recuerdo las palabras exactas de las respuestas pero básicamente fue lo siguiente:

Respuestas a las preguntas 1 y 2.

Pues algunas veces tendrán que graficar datos en una hoja de cálculo, pero el trabajo realmente es estar aquí, si el “jefe” sigue en la oficina, no importa que ustedes no tengan trabajo asignado, tienen que estar aquí, porque en cualquier momento puede necesitar algo. Así que no hay hora de salida, la hora de salida es cuando hayan terminado lo que tenían asignado y el jefe ya se haya retirado.

Respuesta a la pregunta 3

No, no hay remuneración alguna

Desde ese momento yo di las gracias pero como mis compañeros (extrañamente para mi) si estuvieron interesados, esperé a que terminaran de ponerse de acuerdo con la persona que nos atendió para empezar a realizar sus prácticas profesionales en ese lugar.

Lo que pregunté era importante para mi porque muchos estudiantes que realizan sus prácticas profesionales, no hacen algo relacionado con su área de estudio, los ponen a fotocopiar documentos o hacer diligencias para los otros empleados del departamento (ir por las memelas, tacos, tamales, refrescos , etc.). El horario es importante porque debía tener libertad para seleccionar mi carga académica en la universidad y además, no estaba interesado en hacer mis prácticas profesionales en un lugar en donde te ven como esclavo y no como practicante.

Pregunté si había alguna remuneración porque desde luego no esperaba que fuera un sueldo, pero por lo menos que me dieran algo que me ayudara a pagar la gasolina o el pasaje para llegar ahí, no estaba interesado en hacer mis prácticas en un lugar en donde esperaran que yo tuviera que incurrir en gastos por ir a hacer trabajo para ellos.

Cuando los 3 nos retiramos del lugar, mis compañeros me decían que no podían creer que no hubiera aceptado y que nunca iba a encontrar un lugar en donde pagaran a las
personas que hacían sus prácticas profesionales. Yo les comenté que había tiempo todavía para buscar un lugar, creo que era posible hacer las prácticas profesionales hasta séptimo semestre, así que yo no tenía prisa. Un par de meses después un primo mío vio un anuncio en el periódico en donde solicitaban practicantes para el departamento de sistemas. Acudí a entrevista y las respuestas básicamente fueron las siguientes:

Respuesta a la pregunta 1

Vas a desarrollar reportes en COBOL con SQL

Respuesta a la pregunta 2

Tu pones tu horario pero debes cubrir un total de 20 horas a la semana (equivalente a 4 horas diarias de Lunes a Viernes)

Respuesta a la pregunta 3

Si, es una pequeña ayuda mensual

Como podrán imaginarse, hice mis prácticas profesionales en esa empresa.

Presentación de la categoría “I don’t lead, nor follow… I make my own path”

enero 7, 2019

Las personas que tenemos una mente curiosa generalmente somos como dice la canción Thunder de Imagine Dragons “Not a Yes Sir, not a follower”.
En lo personal esto me ha traído ciertas complicaciones desde que estudiaba la licenciatura en la universidad (posiblemente desde antes).

En esta categoría voy a escribir sobre algunas cosas que he vivido y que de alguna forma reflejan mi forma de pensar y de ser, sobre todo en el ámbito profesional. Probablemente le sea útil a los recién egresados de alguna licenciatura relacionada con tecnologías de la información, ya sea para aprender de mis errores y no cometerlos o para sentirse identificados si encuentran que han vivido algo similar.

Utilizando ZeroMQ en Ruby

noviembre 29, 2018

ZMQ (o ZeroMQ) es una biblioteca de mensajería desarrollada en C++. Es software libre que se distribuye bajo una licencia LGPL.

Existen bindings (APIs) para utilizar ZMQ en muchos lenguajes de programación, en éste artículo voy a mostrar un ejemplo utilizando Ruby.

Simula el desarrollo de un sistema para un restaurant; la idea es que en el área de cocina exista una computadora en donde estén llegando automáticamente las comandas que los meseros capturan en otra PC que se encuentra en el área de mesas.

La computadora que vamos a usar para simular la PC que estará en el área de cocina tiene instalado Debian 9 y la PC que utilizaremos para simular la computadora en donde los meseros capturan las comandas tiene instalado PointLinux (es una distribución basada en Debian).

En ambas PC debemos instalar Ruby, la bibilioteca zmq, el paquete ruby2.3-dev y la gema ffi-rzmq.

sudo apt install ruby
sudo apt install libzmq3-dev
sudo apt install ruby2.3-dev
sudo gem install ffi-rzmq

Aquí un video con la explicación y después los programas utilizados para cada una de las PC.

Programa servidorComandas.rb

#!/usr/bin/ruby
#encoding: utf-8

require 'ffi-rzmq'
require 'json'

# Crea un socket tipo PULL, para recibir mensajes
context = ZMQ::Context.new(1)
pull = context.socket(ZMQ::PULL)
# Escucha (recibe mensajes de cualquier IP) en el puerto 5567
pull.bind("tcp://*:5567")

puts "SERVIDOR DE COMANDAS EN FUNCIONAMIENTO"
while true
      mensaje = ''
      pull.recv_string(mensaje)
      
      comanda = JSON.parse(mensaje)
      puts "COMANDA #{comanda['folio']} recibida a las: "+Time.now.strftime("%H:%M:%S")
      puts "Mesa: #{comanda['mesa']}, Mesero: #{comanda['mesero']}"
      puts "="*50
      puts " "
      
      comanda["orden"].each do |partida|
         puts "#{partida['cantidad']} #{partida['descripcion']}"
      end
      puts "*"*50
      puts " "
end

Programa creaComandas.rb

#!/usr/bin/ruby
#encoding: utf-8

require 'ffi-rzmq'
require 'json'

# Esta función regresa verdadero si el dato que recibe es un número
# y regresa falso en caso contrario
def numero_valido?(dato)
    true if Float(dato) rescue false
end

# Crea un socket tipo PUSH, para enviar mensajes
context = ZMQ::Context.new(1)
socket = context.socket(ZMQ::PUSH)
# Se conecta al servidor en el puerto 5567
socket.connect("tcp://192.168.1.1:5567")

folio = 0

loop do
     folio += 1
     puts "COMANDA #{folio}"
     print "Mesa: "
     # Pide al usuario que teclee la mesa
     mesa = gets.chomp
     
     # Si el usuario teclea enter, sale del ciclo loop do (termina el programa)
     if mesa.empty?
        break
     else
         print "Mesero: "
         # Pide al usuario que teclee el mesero
         mesero = gets.chomp
         puts "\nOrden"
         puts "="*50
         orden = []
         
         # Inicia un ciclo loop do para ir capturando la orden (cantidad y descripción)
         loop do
            cantidad = nil
            # Mientras el usuario no teclee un número mayor o igual a cero,
            # el programa le pide que teclee la cantidad
            while true
                  print "Cantidad: "
                  cantidad = gets.chomp
                  # Si el usuario teclea enter, sale del ciclo while
                  break if cantidad.empty?
                  
                  # Si el usuario tecleó un número, sólo sale del ciclo while si el número
                  # es mayor o igual a cero
                  # Si el usuario no tecleó un número, el ciclo while continúa ejecutándose
                  if numero_valido?(cantidad)
                     break if cantidad.to_i>=0
                  end
            end
            # Si el usuario tecleó cero para el campo cantidad, significa que terminó
            # de capturar la orden; por lo tanto se sale del ciclo loop do
            
            break if cantidad.to_i == 0
            # Como el usuario no tecleó cero para el campo cantidad, le pide que teclee la descripción
            print "Descripción: "
            descripcion = gets.chomp
            # Agrega al arreglo orden la cantidad y descripción tecleadas por el usuario
            orden.push({:cantidad => cantidad, :descripcion => descripcion})
         end
         
         comanda = {:folio => folio, :mesa => mesa, :mesero => mesero,
                    :orden => orden}.to_json
         
         # Manda los datos de la comanda al servidor
         socket.send_string(comanda)
     end
end

 

Instalación del editor de textos Atom en Debian Stretch

septiembre 26, 2018

En los repositorios de la versión 9 de Debian (Stretch) no viene el editor de textos Atom. Sin embargo, los desarrolladores del editor publicaron repositorios oficiales para diferentes distribuciones de GNU/Linux.

Para todas las distribuciones que usan el sistema de paquetes .deb, sólo publicaron el repositorio para las versiones de 64 bits.

Antes de añadir el repositorio al archivo /etc/apt/sources.list, debemos agregar la llave gpg para que no marque error al hacer el apt update.

curl -L https://packagecloud.io/AtomEditor/atom/gpgkey | sudo apt-key add -

Abrimos el archivo /etc/apt/sources.list y agregamos el repositorio

deb [arch=amd64] https://packagecloud.io/AtomEditor/atom/any/ any main

Como el repositorio está en un URL con SSL (https) debemos instalar el paquete apt-transport-https

sudo apt install apt-transport-https

A continuación ejecutamos sudo apt update y finalmente instalamos el editor de textos Atom con sudo apt install atom

Crédito: Basado en el post de https://www.linuxuprising.com/2018/05/official-atom-repository-for-ubuntu.html

Ordenar un arreglo de objetos con base en un campo específico en Ruby

julio 3, 2018

Decidí hacer este post a raíz de algo que tuve que hacer cuando desarrollé el sistema Clepsidra. Lo que aquí muestro no es exactamente lo mismo, aquí está el concepto básico esperando que pueda servir para solucionar casos similares que se le puedan presentar a alguien más.

Supongamos que tenemos la siguiente información en formato json:

'[
  {"clave_asignatura":"C-101","nombre_asignatura":"Introducción al lenguaje de programción C","clave_profesor":"P210","nombre_profesor":"Dennis Ritchie"},
  {"clave_asignatura":"U-101","nombre_asignatura":"Unix avanzado","clave_profesor":"P210","nombre_profesor":"Dennis Ritchie"},
  {"clave_asignatura":"DB-101","nombre_asignatura":"Introducción a las bases de datos relacionales","clave_profesor":"P205","nombre_profesor":"Edgar Frank Codd"},
  {"clave_asignatura":"SO-101","nombre_asignatura":"Sistemas operativos","clave_profesor":"P225","nombre_profesor":"Linus Torvalds"},
  {"clave_asignatura":"C-101","nombre_asignatura":"Introducción al lenguaje de programción C","clave_profesor":"P235","nombre_profesor":"Brian Kernighan"},
  {"clave_asignatura":"SL-101","nombre_asignatura":"Introducción al software libre","clave_profesor":"P215","nombre_profesor":"Richard M. Stallman"},
  {"clave_asignatura":"C-101","nombre_asignatura":"Introducción al lenguaje de programción C","clave_profesor":"P230","nombre_profesor":"Ken Thompson"},
  {"clave_asignatura":"SO-101","nombre_asignatura":"Sistemas operativos","clave_profesor":"P220","nombre_profesor":"Andrew S. Tanenbaum"},
  {"clave_asignatura":"L-101","nombre_asignatura":"Linux básico","clave_profesor":"P225","nombre_profesor":"Linus Torvalds"},
  {"clave_asignatura":"U-101","nombre_asignatura":"Unix avanzado","clave_profesor":"P230","nombre_profesor":"Ken Thompson"}
 ]'

Son 10 registros, cada uno con 4 campos: clave_asignatura, nombre_asignatura, clave_profesor y nombre_profesor.

Esta información en formato json la pudimos haber obtenido desde un archivo o desde una base de datos, etc. eso es irrelevante. Como se puede observar, hay asignaturas que aparecen más de una vez, ya que una misma asignatura puede ser impartida por diferentes profesores, y debido a que un profesor puede impartir diferentes asignaturas, también hay profesores que aparecen más de una vez.

Supongamos que a partir de esta información tenemos que obtener una lista de las diferentes asignaturas ordenada por clave de asignatura. Es decir, en la lista ordenada, cada asignatura debe aparecer sólo una vez.

Lo primero que tenemos que hacer es convertir la información a un arreglo de hashes.

@json_data = '[
               {"clave_asignatura":"C-101","nombre_asignatura":"Introducción al lenguaje de programción C","clave_profesor":"P210","nombre_profesor":"Dennis Ritchie"},
               {"clave_asignatura":"U-101","nombre_asignatura":"Unix avanzado","clave_profesor":"P210","nombre_profesor":"Dennis Ritchie"},
               {"clave_asignatura":"DB-101","nombre_asignatura":"Introducción a las bases de datos relacionales","clave_profesor":"P205","nombre_profesor":"Edgar Frank Codd"},
               {"clave_asignatura":"SO-101","nombre_asignatura":"Sistemas operativos","clave_profesor":"P225","nombre_profesor":"Linus Torvalds"},
               {"clave_asignatura":"C-101","nombre_asignatura":"Introducción al lenguaje de programción C","clave_profesor":"P235","nombre_profesor":"Brian Kernighan"},
               {"clave_asignatura":"SL-101","nombre_asignatura":"Introducción al software libre","clave_profesor":"P215","nombre_profesor":"Richard M. Stallman"},
               {"clave_asignatura":"C-101","nombre_asignatura":"Introducción al lenguaje de programción C","clave_profesor":"P230","nombre_profesor":"Ken Thompson"},
               {"clave_asignatura":"SO-101","nombre_asignatura":"Sistemas operativos","clave_profesor":"P220","nombre_profesor":"Andrew S. Tanenbaum"},
               {"clave_asignatura":"L-101","nombre_asignatura":"Linux básico","clave_profesor":"P225","nombre_profesor":"Linus Torvalds"},
               {"clave_asignatura":"U-101","nombre_asignatura":"Unix avanzado","clave_profesor":"P230","nombre_profesor":"Ken Thompson"}
              ]'

@materias_profesores = JSON.parse(@json_data)

En el código anterior, la información en formato json la tenemos en la variable @json_data y ya como un arreglo de hashes la tenemos en la variable @materias_profesores

Si lo deseamos, podemos recorrer nuestro arreglo de hashes con el siguiente código para mostrar la información

@materias_profesores.each do |registro|
   puts registro["clave_asignatura"]+": "+registro["nombre_asignatura"]
   puts registro["clave_profesor"]+": "+registro["nombre_profesor"]
end

Para obtener la lista de asignaturas sin repetir lo que vamos a hacer es recorrer nuestro arreglo de hashes e ir agragando la clave de asignatura y su nombre a otro arreglo. Este nuevo arreglo será un arreglo de objetos en donde cada objeto sólo tendra dos campos, la clave de asignatura y el nombre de asignatura. Así que creamos la siguiente clase

class Materia
   attr_accessor :clave_asignatura, :nombre_asignatura
   
   def initialize(clave, nombre)
       @clave_asignatura = clave
       @nombre_asignatura = nombre
   end
end

Crearemos un arreglo llamado @asignaturas, en éste arreglo iremos agregando objetos de la clase Materia.

Como ya comenté antes, recorreremos nuestro arreglo @materias_profesores y antes de crear e insertar un objeto de la clase Materia a nuestro arreglo llamado @asignaturas, verificamos que no exista ya un objeto con esa información, así garantizamos que cada asignatura sólo aparezca una vez.

@asignaturas = []
@materias_profesores.each do |registro|
    # Sólo agrega la asignatura si no se encuentra ya en el arreglo @asignaturas
    if @asignaturas.all? {|asignatura| asignatura.clave_asignatura != registro["clave_asignatura"]}
       @asignaturas.push(Materia.new(registro["clave_asignatura"],registro["nombre_asignatura"]))
    end
end

Hasta ahora ya tenemos un arreglo de objetos de la clase Materia (nuestro arreglo llamado @asignaturas) en donde existe un objeto por cada una de las diferentes asignaturas.

Podemos usar el siguiente código si queremos mostrar el contenido de nuestro arreglo @asignaturas

puts "\n"
puts "="*20
puts "ASIGNATURAS"
puts "="*20
@asignaturas.each do |asignatura|
   puts asignatura.clave_asignatura+": "+asignatura.nombre_asignatura
end

Ahora sólo falta ordenar nuestro arreglo de objetos por el campo clave_asignatura

@asignaturas.sort! {|a, b| a.clave_asignatura <=> b.clave_asignatura}

Programa completo

#encoding: utf-8
require 'json'

class Materia
   attr_accessor :clave_asignatura, :nombre_asignatura
   
   def initialize(clave, nombre)
       @clave_asignatura = clave
       @nombre_asignatura = nombre
   end
end

@json_data = '[
               {"clave_asignatura":"C-101","nombre_asignatura":"Introducción al lenguaje de programción C","clave_profesor":"P210","nombre_profesor":"Dennis Ritchie"},
               {"clave_asignatura":"U-101","nombre_asignatura":"Unix avanzado","clave_profesor":"P210","nombre_profesor":"Dennis Ritchie"},
               {"clave_asignatura":"DB-101","nombre_asignatura":"Introducción a las bases de datos relacionales","clave_profesor":"P205","nombre_profesor":"Edgar Frank Codd"},
               {"clave_asignatura":"SO-101","nombre_asignatura":"Sistemas operativos","clave_profesor":"P225","nombre_profesor":"Linus Torvalds"},
               {"clave_asignatura":"C-101","nombre_asignatura":"Introducción al lenguaje de programción C","clave_profesor":"P235","nombre_profesor":"Brian Kernighan"},
               {"clave_asignatura":"SL-101","nombre_asignatura":"Introducción al software libre","clave_profesor":"P215","nombre_profesor":"Richard M. Stallman"},
               {"clave_asignatura":"C-101","nombre_asignatura":"Introducción al lenguaje de programción C","clave_profesor":"P230","nombre_profesor":"Ken Thompson"},
               {"clave_asignatura":"SO-101","nombre_asignatura":"Sistemas operativos","clave_profesor":"P220","nombre_profesor":"Andrew S. Tanenbaum"},
               {"clave_asignatura":"L-101","nombre_asignatura":"Linux básico","clave_profesor":"P225","nombre_profesor":"Linus Torvalds"},
               {"clave_asignatura":"U-101","nombre_asignatura":"Unix avanzado","clave_profesor":"P230","nombre_profesor":"Ken Thompson"}
              ]'

@materias_profesores = JSON.parse(@json_data)

@materias_profesores.each do |registro|
   puts registro["clave_asignatura"]+": "+registro["nombre_asignatura"]
   puts registro["clave_profesor"]+": "+registro["nombre_profesor"]
end

@asignaturas = []
@materias_profesores.each do |registro|
   # Sólo agrega la asignatura si no se encuentra ya en el arreglo @asignaturas
   if @asignaturas.all? {|asignatura| asignatura.clave_asignatura != registro["clave_asignatura"]}
      @asignaturas.push(Materia.new(registro["clave_asignatura"],registro["nombre_asignatura"]))
   end
end

puts "\n"
puts "="*20
puts "ASIGNATURAS"
puts "="*20
@asignaturas.each do |asignatura|
   puts asignatura.clave_asignatura+": "+asignatura.nombre_asignatura
end

# Ordena el arreglo @asignaturas por el campo clave_asignatura
@asignaturas.sort! {|a, b| a.clave_asignatura <=> b.clave_asignatura}

puts "\n"
puts "="*20
puts "ASIGNATURAS ORDENADAS POR CLAVE"
puts "="*20
@asignaturas.each do |asignatura|
   puts asignatura.clave_asignatura+": "+asignatura.nombre_asignatura
end

Quien quiera practicar, puede hacer una versión de este programa en donde utilizando la misma información en formato json, obtenga una lista de los diferentes profesores ordenada por el campo nombre_profesor