IOKode

La inteligencia artificial en el ámbito de la programación

diciembre 05, 2022 07:43

El contenido de este blog se escribe en Markdown, pero hace tiempo que llevo dándole vueltas a la idea de cambiar a un formato igualmente ligero, pero algo más potente. Entre otras cosas, me interesa que se trate de un formato extensible. Mi amigo Alejandro Quirós me recomendó que el echase un vistazo a Asciidoc, por lo que he estado durante un tiempo viéndolo y probándolo localmente.

Asciidoc parece tener todo lo que necesito, es un formato de texto ligero que, por defecto, contiene todos los tipos de elemento de Markdown y algunos más, por ejemplo, los admonitions, que permiten crear cajas de advertencia o tip.

Echando un ojo atrás a mis entradas, me he dado cuenta que en algunas de ellas hago comentarios sobre diferentes tweets, por lo que he pensado que me vendría bien la capacidad de poder insertar un tweet dentro de una entrada. Asciidoc por defecto no permite insertar un tweet, pero al ser extensible, se puede crear un plugin para ello.

El software que transforma documentos Asciidoc en documentos HTML se llama Asciidoctor y, está escrito en Ruby. Las extensiones para Asciidoctor por lo tanto se escriben en Ruby.

ChatGPT

Salvo que hayas estado viviendo en una cueva los últimos días, sabrás que es ChatGPT, una inteligencia artificial conversational capaz de tener conversaciones a través de un chat.

ChatGPT es capaz de mantener todo tipo de conversaciones manteniendo el contexto con los mensajes anteriores y, si se le pide, es capaz de programar.

Dado que yo no tengo experiencia alguna con Ruby, he pensado en pedirle a ChatGPT que se encargue de programar dicha extensión por mí. Tras intercambiar una larga lista de mensajes pidiendole ciertos cambios, avisandole de errores que me ocurrían, y hacer algunos cambios a mano sobre el código generado, la IA ha conseguido escribir un plugin para Asciidoctor completamente funcional para insertar tweets en un documento.

La conversación con ChatGPT

Lo primero que he hecho es pedirle en líneas generales lo que necesito, y esto es lo que ha ocurrido:

Ivan:
Can you create an Asciidoctor extension in Ruby to embed a tweet inside an Ascidoc document?

ChatGPT:
Yes, it is possible to create an Ascidoctor extension in Ruby to embed a tweet inside an Asciidoc document. To do this, you would first need to create a Ruby class that extends the Asciidoctor::Extensions::BlockProcessor class. This class would define a method for processing the tweet block in your Asciidoc document.

To embed the tweet, you would need to use the Twitter API to fetch the tweet by its ID and then generate the necessary HTML code to display the tweet on the page. You can then use the Asciidoctor::Document#create_block method to create a new block containing the HTML code for the tweet and add it to the document.

Here is an example of how this might look:

class TweetBlock < Asciidoctor::Extensions::BlockProcessor
  use_dsl
  named :tweet
  on_context :open

  def process parent, target, attrs
	tweet_id = target
	
    # Fetch the tweet using the Twitter API
    tweet = TwitterClient.fetch_tweet(tweet_id)

    # Generate the HTML code for the tweet
    html = generate_tweet_html(tweet)

 	# Create a new block containing the tweet HTML and add it to the document
	create_block parent, :pass, html, attrs
  end

  def generate_tweet_html tweet
    # todo
  end
end

Once you have created this class, you can register it with Asciidoctor by adding it to the list of extensions in your Asciidoctor configuration. This will allow you to use the tweet block in your Asciidoc document to embed tweets.

Here is an example of how you might use this extension in your Asciidoc document:

tweet::1234567890[]

This would create a block containing the tweet with the ID 1234567890, as fetched from the Twitter API.

Por lo que podemos ver, en este código ocurren tres cosas:

  1. Utilizando el API de Twitter para Ruby, obtiene el contenido del tweet.
  2. Le pasa el tweet a una función para que lo transforme en HTML.
  3. Crea un bloque de Asciidoc con el contenido HTML.

Analicemos el código un poco. Una de las primeras cosas que hace es pasarle el ID directamente al API de Twitter, pero no comprueba que sea válido (númerico), así que se lo hago saber a la IA.

Ivan:
tweet_id should contains the ID of the tweet, but this script doesn't validate that is a valid tweet ID. Can you update it to add this validation?

ChatGTP:
Yes, you are correct that the current example code does not validate that the id attribute contains a valid tweet ID. This could lead to errors if an invalid ID is provided.

To add this validation, you can use the Twitter API to check if a tweet with the specified ID exists. If the tweet does not exist, you can raise an error to let the user know that the ID is invalid.

Here is an example of how you might update the process method to add this validation:

def process parent, target, attrs
  tweet_id = target

  # Validate that the ID is a valid tweet ID
  unless TwitterClient.tweet_exists?(tweet_id)
    raise  "Invalid tweet ID: #{tweet_id}"
  end

  # Fetch the tweet using the Twitter API
  tweet = TwitterClient.fetch_tweet(tweet_id)
  
  # Generate the HTML code for the tweet
  html = generate_tweet_html(tweet)
  
  # Create a new block containing the tweet HTML and add it to the document
  create_block  parent, :pass, html, attrs
end

This code will check if a tweet with the specified ID exists using the TwitterClient.tweet_exists? method. If the tweet does not exist, it will raise an error with the invalid tweet ID. Otherwise, it will proceed to fetch the tweet and generate the HTML code to embed it in the document.

Yo esperaba que validase que sea numérico pero, sorprendentemente, hace uso del API de Twitter para comprobar si existe. Aunque es una respuesta correcta, esto conlleva una llamada extra al API, que tiene un límite de peticiones. Lo ideal para mi es que la ejecución del script utilice únicamente una llamada y no varias. Se lo comento y le soy más específico con la validación que necesito.

Ivan:
That's a correct validation, but it requires to make an extra call to Twitter API, that is rate limited. A tweet ID is ever numeric. Can you update the script to validate that the tweet ID is numeric instead of checking if the tweet exists?

ChatGPT:
Yes, you are correct that making an extra call to the Twitter API to validate the tweet ID can be wasteful if the ID is not valid, as it counts against your API rate limit. Additionally, tweet IDs are always numeric, so you can use this information to validate the ID without making an extra API call.

To do this, you can simply check if the tweet_id variable contains a numeric value, and raise an error if it does not. Here is an example of how you might update the process method to do this:

def process parent, target, attrs
  tweet_id = target

  # Validate that the ID is numeric
  unless tweet_id.is_a?(Numeric)
    raise  "Invalid tweet ID: #{tweet_id}"
  end

  # Fetch the tweet using the Twitter API
  tweet = TwitterClient.fetch_tweet(tweet_id)
 
  # Generate the HTML code for the tweet
  html = generate_tweet_html(tweet)
 
  # Create a new block containing the tweet HTML and add it to the document
  create_block  parent, :pass, html, attrs
end

This code will check if the tweet_id variable contains a numeric value using the Numeric class, and raise an error if it does not. This will ensure that the tweet ID is valid without making an extra API call.

Como no tengo experiencia progrmando con Ruby, desconozco que hace el método is_a?, por lo que le pregunto directamente.

Ivan:
What is_a? method returns?

ChatGPT:
The is_a? method in Ruby returns a boolean value indicating whether the object on which it is called is an instance of the specified class or one of its descendants.

For example, if you have the following code:

x = "hello"

if x.is_a?(String)
  puts "x is a string"
end

This will output x is a string to the console, because the x variable is an instance of the String class.

On the other hand, if you have the following code:

x = 3

if x.is_a?(String)
  puts "x is a string"
end

This will not output anything to the console, because the x variable is not an instance of the String class (it is an instance of the Fixnum class, which is a descendant of the Numeric class).

In general, the is_a? method is useful for checking the type of an object at runtime and taking different actions based on that type.

Tal como ha dicho, equivaldría al operador instanceof de Java o is de C#. Debemos asumir que Asciidoctor enviará el ID como un string (así me encargaré yo mismo cuando cree los documentos Asciidoc), por lo que le informo de ello.

Ivan:
Ok. We should asume that tweet_id is ever an string. Can you check if the string is an integer value?

ChatGPT:
Yes, you are correct that the tweet_id variable in the process method is always a string, even if it contains a numeric value. To check if this string contains an integer value, you can use the String#to_i method, which converts the string to an integer if it contains a valid integer value, or returns 0 if it does not.

Here is an example of how you might update the process method to use the String#to_i method to check if the tweet_id variable contains a valid integer value:

def process parent, target, attrs
  tweet_id = target

  # Validate that the ID is an integer value
  unless tweet_id.to_i != 0
    raise  "Invalid tweet ID: #{tweet_id}" 
  end

  # Fetch the tweet using the Twitter API
  tweet = TwitterClient.fetch_tweet(tweet_id)
 
  # Generate the HTML code for the tweet
  html = generate_tweet_html(tweet)
 
  # Create a new block containing the tweet HTML and add it to the document
  create_block parent, :pass, html, attrs
end

In this code, the tweet_id.to_i != 0 expression will check if the tweet_id string contains a valid integer value. If it does not, it will raise an error with the invalid tweet ID. Otherwise, it will proceed to fetch the tweet and generate the HTML code to embed it in the document.

La IA ha entenido perfectamente lo que le he querido decir y corrige de nuevo el script, además me explica como funciona. ¡Perfecto!

Nótese que en cada cambio que está haciendo, además está actualizando los comentarios para que estos no queden desactualizados. ¡Un 10!

Una vez hecha la validación del tweet, vamos a continuar. Le voy a pedir que implemente el método generate_tweet_html que me había dejado únicamente con un TODO.

Ivan:
Please, implement generate_tweet_html method.

ChatGPT:
To implement the generate_tweet_html method, you will need to use the Twitter API to fetch the tweet by its ID and then generate the necessary HTML code to display the tweet on the page.

Here is an example of how you might implement the generate_tweet_html method:

def generate_tweet_html(tweet)
  # Use the Twitter API to fetch the tweet by its ID
  tweet = TwitterClient.fetch_tweet(tweet_id)

  # Generate the HTML code for the tweet
  html = <<~HTML
    <blockquote class="twitter-tweet">
      <p>#{tweet.text}</p>
      <a href="https://twitter.com/#{tweet.user.screen_name}/status/#{tweet.id}">
        #{tweet.created_at.strftime("%b %d %Y")}
      </a>
    </blockquote>
  HTML
end

In this code, the generate_tweet_html method takes a tweet object as an argument. It then uses the Twitter API to fetch the tweet by its ID and generates the necessary HTML code to display the tweet on the page. The generated HTML uses the <blockquote> element to display the tweet text and the <a> element to link to the tweet on Twitter.

Once you have implemented the generate_tweet_html method, you can use it in the process method to generate the HTML code for the tweet and add it to the document.

Here is an example of how you might use the generate_tweet_html method in the process method:

def process parent, target, attrs
  tweet_id = target

  # Validate that the ID is an integer value
  unless tweet_id.to_i != 0
    raise  "Invalid tweet ID: #{tweet_id}" 
  end

  # Fetch the tweet using the Twitter API
  tweet = TwitterClient.fetch_tweet(tweet_id)
 
  # Generate the HTML code for the tweet
  html = generate_tweet_html(tweet)
 
  # Create a new block containing the tweet HTML and add it to the document
  create_block  parent, :pass, html, attrs
end

This code will use the generate_tweet_html method to generate the HTML code for the tweet, and then create a new block containing the HTML and add it to the document. This will allow the tweet to be displayed on the page when the Asciidoc document is rendered.

Aquí ocurren varias cosas. La primera de ella, es que dentro de este método, está usando el API de Twitter para obtener de nuevo los datos que ya conocía previamente, pues son los que les viene por parámetro. Yo mismo me encargo de borrar la primera línea.

También se puede observar, la IA me ha devuelto de nuevo el contenido del método process sin ningún cambio. Esto es una constante lo largo de toda la conversación, por lo que a partir de aquí, obmitiré esa parte de sus respuestas.

Mi siguiente problema es que para hacer uso del API de Twitter para Ruby, este require instalarlo a través de una gema. Cargar gemas desde Asciidoc, aunque es posible, es relativamente complicado, por lo que le pido a la IA que por favor lo reemplace haciendo directamente llamadas HTTP al API de Twitter.

Ivan:
I cannot load any gem, so I need to use only libraries available in Ruby core. Can you replace TwitterClient by a method that make an HTTP request to the Twitter API?

ChatGTP:
Yes, it is possible to make an HTTP request to the Twitter API using only the libraries available in the Ruby core. To do this, you can use the Net::HTTP class to make an HTTP request to the Twitter API and parse the response.

Here is an example of how you might use the Net::HTTP class to fetch a tweet by its ID:

def fetch_tweet tweet_id
  # Build the URI for the Twitter API endpoint
  uri = URI("https://api.twitter.com/1.1/statuses/show.json?id=#{tweet_id}")

  # Set the authorization header for the request
  headers = {
    'Authorization' => 'Bearer <YOUR_BEARER_TOKEN>'
  }

  # Make the request and parse the JSON response
  response = Net::HTTP.get(uri, headers)
  tweet = JSON.parse(response)

  # Return the tweet object
  tweet
end

In this code, the fetch_tweet method takes a tweet_id as an argument and uses the Net::HTTP class to make an HTTP GET request to the Twitter API endpoint for fetching a tweet by its ID. It sets the appropriate authorization header for the request and parses the JSON response into a Ruby object. It then returns the tweet object, which can be used to generate the HTML code for the tweet.

Once you have implemented this method, you can use it in the generate_tweet_html method to fetch the tweet and generate the HTML code for it. Here is an example of how you might do this:

def generate_tweet_html(tweet_id)
  # Use the Twitter API to fetch the tweet by its ID
  tweet = fetch_tweet(tweet_id)

  # Generate the HTML code for the tweet
  html = <<~HTML
    <blockquote class="twitter-tweet">
      <p>#{tweet.text}</p>
      <a href="https://twitter.com/#{tweet.user.screen_name}/status/#{tweet.id}">
        #{tweet.created_at.strftime("%b %d %Y")}
      </a>
    </blockquote>
  HTML
end

This code will use the fetch_tweet method to fetch the tweet by its ID and generate the HTML code for it using the <blockquote> and <a> elements, as in the previous example.
Como se puede ver, no sólo me ha implementado método fetch_tweet, si no que además me ha modificado los métodos generate_tweet_html y process para que hagan uso de estos.

Once you have implemented these methods, you can use them in the process method to fetch the tweet and generate the HTML code for it, and add it to the document. Here is an example of how you might do this:

def process parent, target, attrs
  tweet_id = target

  # Generate the HTML code for the tweet
  html = generate_tweet_html(tweet)

  # Create a new block containing the tweet HTML and add it to the document
  create_block parent, :pass, html, attrs
end

Aquí no sólo me ha implementado un método para obtener el contenido del tweet, si no que además ha modificado el método generate_tweet_html para que lo llame, y ha eliminado esta llamada del método process. No soy capaz de entender a qué se debe ese cambio en la organización del código, pero no me preocupa y sigo con lo mío.

Este método que ha implementado utiliza el API 1.1 de Twitter. Para acceder al API 1.1 necesitas hacer una petición formal a Twitter y pedir que te habiliten el acceso. Mi única opción es hacer uso del API 2.0 de Twitter, por lo que se lo comento. Dicho y hecho.

Ivan:
Can you use Twitter API 2.0 in fetch_tweet method?

ChatGPT:
Yes, it is possible to use the Twitter API 2.0 to fetch a tweet by its ID in the fetch_tweet method. To do this, you will need to update the URI and the authorization header for the request to use the new API endpoint and authentication scheme.

Here is an example of how you might update the fetch_tweet method to use the Twitter API 2.0:

def fetch_tweet tweet_id
  # Construct the URL for the Twitter API request
  tweet_url = "https://api.twitter.com/2/tweets/#{tweet_id}"
  uri = URI.parse(tweet_url)

  # Create the HTTP request and set the necessary headers
  request = Net::HTTP::Get.new(uri)
  request['Authorization'] = 'Bearer <YOUR_BEARER_TOKEN>'
  request['Accept'] = 'application/json'

  # Make the HTTP request and retrieve the response
  response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
    http.request(request)
  end

  # Parse the JSON response
  tweet = JSON.parse(response.body)

  # Return the tweet object
  tweet
end

In this code, the fetch_tweet method uses the new Twitter API 2.0 endpoint for fetching a tweet by its ID, and sets the appropriate authorization header for the request. It then makes the request and parses the JSON response into a Ruby object, just like in the previous example.

Como se puede observar, en el header de autorización me pone un string que yo debería de reemplazar por mi bearer token. Le pido que lo obtenga directamente desde una variable de entorno. Otra vez más, dicho y hecho.

Ivan:
Can you replace 'Bearer <YOUR_BEARER_TOKEN>' string by an environment variable?

ChatGPT:
Yes, it is possible to use an environment variable to store the bearer token for the Twitter API instead of hardcoding it in the fetch_tweet method. To do this, you can use the ENV global variable in Ruby to access the value of the environment variable.

Here is an example of how you might update the fetch_tweet method to use an environment variable for the bearer token:

def fetch_tweet tweet_id
  # Construct the URL for the Twitter API request
  tweet_url = "https://api.twitter.com/2/tweets/#{tweet_id}"
  uri = URI.parse(tweet_url)

  # Create the HTTP request and set the necessary headers
  request = Net::HTTP::Get.new(uri)
  request['Authorization'] = "Bearer #{ENV['TWITTER_BEARER_TOKEN']}"
  request['Accept'] = 'application/json'

  # Make the HTTP request and retrieve the response
  response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
    http.request(request)
  end

  # Parse the JSON response
  tweet = JSON.parse(response.body)

  # Return the tweet object
  tweet
end

In this code, the fetch_tweet method uses the ENV global variable to access the value of the TWITTER_BEARER_TOKEN environment variable and use it as the bearer token for the request. This allows you to store the bearer token in an environment variable and use it in the code without hardcoding it.

Ahora le pido a la IA que modifique el método fetch_tweet para que lance un error si no existe.

Ivan:
Can you raise an error if tweet id doesn't exists?

ChatGPT:
Yes, it is possible to raise an error if the tweet ID does not exist by checking the response from the Twitter API. If the tweet does not exist, the API will return a response with a 404 status code and an error message in the body. You can check for this response in the fetch_tweet method and raise an error if it is received.

Here is an example of how you might update the fetch_tweet method to raise an error if the tweet ID does not exist:

def fetch_tweet tweet_id
  # Construct the URL for the Twitter API request
  tweet_url = "https://api.twitter.com/2/tweets/#{tweet_id}"
  uri = URI.parse(tweet_url)

  # Create the HTTP request and set the necessary headers
  request = Net::HTTP::Get.new(uri)
  request['Authorization'] = "Bearer #{ENV['TWITTER_BEARER_TOKEN']}"
  request['Accept'] = 'application/json'

  # Make the HTTP request and retrieve the response
  response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
    http.request(request)
  end
  
  # Check the status code of the response 
  if response.code == '404'
    raise  "Tweet not found: #{tweet_id}"
  end

  # Parse the JSON response
  tweet = JSON.parse(response.body)

  # Return the tweet object
  tweet
end

In this code, the fetch_tweet method checks the status code of the response from the Twitter API. If the status code is 404, it means that the tweet does not exist, and the method raises an error with the message "Tweet not found: #{tweet_id}". If the tweet exists, the method parses the JSON response and returns the tweet object as before.

Esto está bien para cuando el tweet no existe, sin embargo podría haber otros motivos por los que la respuesta no sea correcta, como usuario no autenticado, o API caída. Le pido a la IA que lo compruebe.

Ivan:
That's is correct, but it only checks that the tweet doesn't exists. There are any other reasons because the API can returns a response that is not 200, like unauthorised. Can you raise an error if response is not OK?

ChatGPT:
Yes, it is possible to raise an error if the response from the Twitter API is not 200 by checking the status code of the response. If the response has a different status code, it means that there was an error and you should raise an error with the appropriate message.

Here is an example of how you might update the fetch_tweet method to raise an error if the response is not 200:

def fetch_tweet tweet_id
  # Construct the URL for the Twitter API request
  tweet_url = "https://api.twitter.com/2/tweets/#{tweet_id}"
  uri = URI.parse(tweet_url)

  # Create the HTTP request and set the necessary headers
  request = Net::HTTP::Get.new(uri)
  request['Authorization'] = "Bearer #{ENV['TWITTER_BEARER_TOKEN']}"
  request['Accept'] = 'application/json'

  # Make the HTTP request and retrieve the response
  response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
    http.request(request)
  end
  
  # Check the status code of the response 
  if response.code != '200'
    if response.code == '404'
      raise "Error: Tweet not found with ID #{tweet_id}"
    else
      raise "Error: #{response.code} #{response.message}"
    end
  end

  # Parse the JSON response
  tweet = JSON.parse(response.body)

  # Return the tweet object
  tweet
end

Ahora comprueba que el la respuesta sea sí o sí 200, y no sólo que no sea 404. Además, manteniendo el contexto anterior, ha mantenido el caso de que pueda ser 404. ¡Bravo!

Llegados a este punto, parece que hay una extensión de Asciidoctor que podría funcionar. Hay algunos detalles más que pulir, como por ejemplo que la plantilla que genera el HTML espera un objeto como el que proporciona el método TwitterClient.fetch_tweet que hemos visto al principio, en vez de un Hash que devuelve el método JSON.parse. He intentado que lo arregle, pero cada cambio que añadía la rompía más, y eso me ha tocado arreglarlo a mí.

También le ha faltado añadir los imports necesarios para que pueda encontrar las clases JSON, Net::HTTP entre otras. La comprobación de que el ID del tweet sea un entero tampoco funcionaba. Tras arreglar todos estos detallitos, algunos por mí mismo, otros con ayuda de la IA, tengo una extensión para Asciidoctor totalmente funcional.

El código de la extensión está disponible en un repositorio que he creado en GitHub.

Conclusiones

Aunque he tenido que hacer algunos cambios a mano, la IA ha hecho la mayor parte del trabajo. Yo no sé nada de Ruby y hacer algo así me habría costado varias horas o incluso días, entre aprender la sintaxis de Ruby, buscar como se hacen llamadas HTTP, como se parsea un JSON, como se comprueba una expresión regular, etc.

En cambio, ordenando a la IA y, con algunas modificaciones menores propias, he conseguido mi objetivo en una hora.

El ahorro de tiempo y trabajo que esta tecnología supone hoy en día es algo que no tiene precedentes.

» Leer más, comentarios, etc...

Variable not found

Enlaces interesantes 507

diciembre 05, 2022 07:05

Enlaces interesantes

Pasando del 503 ninguno de los códigos de estado HTTP son demasiado conocidos, así que esto se está convirtiendo en una oportunidad para echarles un vistazo y al menos, saber que están ahí.

Esta semana seguimos con el HTTP 507 (Insufficient storage), que indica que el espacio de almacenamiento en el servidor no es suficiente para completar la operación. Según la especificación, la petición no debe ser reintentada hasta que sea solicitada por una acción independiente del usuario; o sea, que cuando recibamos este error, deberíamos informar al usuario y, si procede, permitirle reintentar.

Y ahora, ahí van los enlaces recopilados durante la semana pasada. Espero que os resulten interesantes. :-)

Por si te lo perdiste...

.NET Core / .NET

ASP.NET Core / ASP.NET / Blazor

Azure / Cloud

Conceptos / Patrones / Buenas prácticas

Data

Machine learning / IA / Bots

Web / HTML / CSS / Javascript

Visual Studio / Complementos / Herramientas

.NET MAUI / Xamarin

    Por cierto, por compromisos de esta semana no podré ejercer mis habituales tareas de recopilación, así que la semana que viene no habrá enlaces :)

    Publicado en Variable not found.

    » Leer más, comentarios, etc...

    IOKode

    ¿Es necesario aprender programación orientada a objetos para empezar a programar?

    diciembre 03, 2022 02:17

    Hace unos días, me crucé con el siguiente tweet que preguntaba “¿Crees que sea necesario aprender POO (Programación Orientada a Objetos) para empezar a programar?”, a lo que respondí que no. En esta entrada quiero extender esta respuesta.

    Para reflexionar sobre ello, primero necesitamos definir qué es programar y qué es la programación orientada a objetos.

    ¿Qué es programar?

    Programar se puede definir como escribir programas que ejecutará una computadora.

    La base de un programa de computadora es secuencia, selección y repetición. Todo programa durante la ejecución se reduce a ello y, aquellos diseñados con un lenguaje orientado a objetos, no son una excepción.

    ¿Qué es la programación orientada a objetos?

    La programación orientada a objetos es un paradigma de programación que impone cierta disciplina sobre la organización del código y la transferencia del control, pero es sólo eso, una disciplina organizativa. Al final, se ejecutan los métodos, no las clases ni los objetos.

    Aprender a programar

    Hechas les definiciones, toca reflexionar sobre ello cuando se está empezando a aprender a programar.

    Cuando una persona quiere aprender a programar, es mucho más importante que aprenda primero los fundamentos más básicos y, luego ya, podrá pasar (o no) a aprender sobre el paradigma de programación orientada a objetos.

    Sobre estos fundamentos, me gustaría hacer especial mención a la encapsulación y al polimorfismo por su estrecha relación con la programación orientada a objetos.

    Los lenguajes orientados a objetos como Java o C# facilitan muchísimo la implementación de encapsulamiento y polimorfismo y, aunque son conceptos fundamentales e importantes, no es necesario aprender POO para entenderlos ni para aplicarlos. Ya en el lenguaje C, que no es orientado a objetos, se podía hacer encapsulamiento y polimorfismo, aunque no fuese algo tan trivial y cómodo de hacer como en Java o C#.

    » Leer más, comentarios, etc...

    info.xailer.com

    Xailer 8.1 – Control editor de HTML

    diciembre 01, 2022 07:17

    Este nuevo control permite editar documentos HTML de forma completamente visual y con cierta complejidad ya que permite incluir tablas e imágenes.

    El control está basado en el control Summernote, que es un simple editor realizado en JavaScript, que es especialmente liviano, apenas consume recursos y cuya licencia nos permite el uso comercial.

    Este control hereda del control TWebView que es un control navegador Web basado en Microsoft Edge que únicamente está disponible para Xailer Enterprise.

    No es necesario incluir ningún código fuente de SummerNote, todo está autocontenido. Su comportamiento es exactamente igual que cualquier otro control de Xailer.

    Nota importante: las imágenes se guardan dentro del documento

    Más en breve.

    Un cordial saludo

    » Leer más, comentarios, etc...

    info.xailer.com

    Xailer 8.1 – Nuevo control TAnimatedGif

    diciembre 01, 2022 06:39

    Este control permite mostrar GIFs animados sin absolutamente ningún tipo de retrasos o trompicones. Y esto es debido a que el pintado se hace absolutamente en bajo nivel y en un segundo hilo, lo cual tiene un grado de complejidad muy alto.

    Con este control de GIF animado no es necesario llamar a ProcessMessages() para que el control se siga pintando correctamente.

    Está disponible para cualquier versión de Xailer, incluso la personal.

    Más en breve.

    Un cordial saludo

    » Leer más, comentarios, etc...

    Blog Bitix

    Base de datos de países, regiones, ciudades, códigos postales y otra información geográfica

    diciembre 01, 2022 06:30

    Entre las funcionalidades habituales en una aplicación está un formulario para introducir una dirección. El formulario de dirección consta de varios campos en el que suele ser recomendable realizar unas mínimas validaciones como que el código postal o ciudad sea correcto para evitar errores inadvertidos del usuario o intencionados con información incorrecta. La base de datos de GeoNames proporciona información geográfica muy útil y de forma gratuita que incluye países, ciudades, subdivisiones administrativas, códigos postales, nombres en diferentes idiomas y mucha otra información de todos los paises del mundo.

    No todo son microservicios es más ni siquiera estos son la mejor opción en muchos casos, las cosas mundanas tienen también mucha importancia en la aplicación cómo ordenar alfabéticamente cadenas o introducir una dirección postal. Es habitual un formulario en el que haya que introducir direcciones lo que incluye códigos postales, ciudad, país y quizá adicionalmente un teléfono.

    Lo que parece un simple formulario con varios campos de datos se complica cuando hay que validar los códigos postales, ciudad de un país o al incluir información de las diferentes subdivisiones administrativas. Más si en el formulario hay que tener en cuenta las diferentes variaciones según cada país, los códigos postales de España son muy diferentes de los de Reino Unido lo que significa que la expresión regular es muy diferente y según la normativa del país.

    Estas tareas que parecen sencillas realmente tienen su dificultad, ordenar caracteres alfabéticamente se complica ya que hay que tener en cuenta las tildes o mayúsculas y minúsculas y validar direcciones para recibir datos correctos requiere validaciones y conocer cuales son los valores válidos.

    El listado de países, unidades administrativas como comunidades autónomas, ciudades y códigos postales además del código del prefijo telefónico del país o zona horaria es habitual configurarlo como información más o menos estática en las aplicaciones ya sea en una base de datos o en archivos de configuración.

    Contenido del artículo

    La base de datos de información geográfica de GeoNames

    Una fuente que permite obtener esta información geográfica es la página de GeoNames que tiene una licencia de Creative Commons. La base de datos de información está compuesta por varios archivos CSV muchos separados por tabuladores con las varias columnas de información. En el archivo README hay una lista de los archivos de los que consta la base de datos.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    
    XX.zip                   : features for country with iso code XX, see 'geoname' table for columns. 'no-country' for features not belonging to a country.
    allCountries.zip         : all countries combined in one file, see 'geoname' table for columns
    cities500.zip            : all cities with a population > 500 or seats of adm div down to PPLA4 (ca 185.000), see 'geoname' table for columns
    cities1000.zip           : all cities with a population > 1000 or seats of adm div down to PPLA3 (ca 130.000), see 'geoname' table for columns
    cities5000.zip           : all cities with a population > 5000 or PPLA (ca 50.000), see 'geoname' table for columns
    cities15000.zip          : all cities with a population > 15000 or capitals (ca 25.000), see 'geoname' table for columns
    alternateNamesV2.zip     : alternate names with language codes and geonameId, file with iso language codes, with new columns from and to
    alternateNames.zip       : obsolete use V2, this file does not have the new columns to and from and will be removed in the future
    admin1CodesASCII.txt     : names in English for admin divisions. Columns: code, name, name ascii, geonameid
    admin2Codes.txt          : names for administrative subdivision 'admin2 code' (UTF8), Format : concatenated codes <tab>name <tab> asciiname <tab> geonameId
    iso-languagecodes.txt    : iso 639 language codes, as used for alternate names in file alternateNames.zip
    featureCodes.txt         : name and description for feature classes and feature codes 
    timeZones.txt            : countryCode, timezoneId, gmt offset on 1st of January, dst offset to gmt on 1st of July (of the current year), rawOffset without DST
    countryInfo.txt          : country information : iso codes, fips codes, languages, capital ,...
    archives.txt
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    The main 'geoname' table has the following fields :
    ---------------------------------------------------
    geonameid         : integer id of record in geonames database
    name              : name of geographical point (utf8) varchar(200)
    asciiname         : name of geographical point in plain ascii characters, varchar(200)
    alternatenames    : alternatenames, comma separated, ascii names automatically transliterated, convenience attribute from alternatename table, varchar(10000)
    latitude          : latitude in decimal degrees (wgs84)
    longitude         : longitude in decimal degrees (wgs84)
    feature class     : see http://www.geonames.org/export/codes.html, char(1)
    feature code      : see http://www.geonames.org/export/codes.html, varchar(10)
    country code      : ISO-3166 2-letter country code, 2 characters
    cc2               : alternate country codes, comma separated, ISO-3166 2-letter country code, 200 characters
    admin1 code       : fipscode (subject to change to iso code), see exceptions below, see file admin1Codes.txt for display names of this code; varchar(20)
    admin2 code       : code for the second administrative division, a county in the US, see file admin2Codes.txt; varchar(80) 
    admin3 code       : code for third level administrative division, varchar(20)
    admin4 code       : code for fourth level administrative division, varchar(20)
    population        : bigint (8 byte int) 
    elevation         : in meters, integer
    dem               : digital elevation model, srtm3 or gtopo30, average elevation of 3''x3'' (ca 90mx90m) or 30''x30'' (ca 900mx900m) area in meters, integer. srtm processed by cgiar/ciat.
    timezone          : the iana timezone id (see file timeZone.txt) varchar(40)
    modification date : date of last modification in yyyy-MM-dd format
    table-geoname.txt
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    The table 'alternate names' :
    -----------------------------
    alternateNameId   : the id of this alternate name, int
    geonameid         : geonameId referring to id in table 'geoname', int
    isolanguage       : iso 639 language code 2- or 3-characters; 4-characters 'post' for postal codes and 'iata','icao' and faac for airport codes, fr_1793 for French Revolution names,  abbr for abbreviation, link to a website (mostly to wikipedia), wkdt for the wikidataid, varchar(7)
    alternate name    : alternate name or name variant, varchar(400)
    isPreferredName   : '1', if this alternate name is an official/preferred name
    isShortName       : '1', if this is a short name like 'California' for 'State of California'
    isColloquial      : '1', if this alternate name is a colloquial or slang term. Example: 'Big Apple' for 'New York'.
    isHistoric        : '1', if this alternate name is historic and was used in the past. Example 'Bombay' for 'Mumbai'.
    from		  : from period when the name was used
    to		  : to period when the name was used
    table-alternate-names.txt

    countryInfo.txt

    Este archivo contiene la lista de países. Entre su información está el código del país, las expresiones regulares de los códigos postales, el prefijo del teléfono, código de la divisa, lenguaje entre otra información como población, superficie y la capital además de los países vecinos.

    1
    2
    
    ES	ESP	724	SP	Spain	Madrid	504782	46723749	EU	.es	EUR	Euro	34	#####	^(\d{5})$	es-ES,ca,gl,eu,oc	2510769	AD,PT,GI,FR,MA	
    
    
    country-es.txt

    allCountries.zip y XX.zip

    El archivo allCountries.zip contiene las ciudades de todos los países (todos los archivos XX.zip) y en los archivos XX.zip están las ciudades de un país en concreto, ES.zip para las ciudades de España. Contienen el código ciudad, latitud y longitud, zona horaria y los códigos de las unidades administrativas junto con su población, elevación e identificador geonameid

    1
    2
    
    3128026	Bilbao	Bilbao	BIO,Bil'baa,Bil'bao,Bilbao,Bilbau,Bilbaum,Bilbaw,Bilbo,Bilbo / Bilbao,Bilmpao,El Bocho,bi er ba e,bil ba xo,bilba'o,bilbao,birubao,blbaw,bylbaw,bylbayw,Μπιλμπάο,Билбао,Бильбао,Більбаа,Більбао,Բիլբաո,בילבאו,بلباو,بیلبائو,بیلباو,बिल्बाओ,บิลบาโอ,ბილბაო,ビルバオ,毕尔巴鄂,빌바오
    
    
    es-bilbao.txt

    Hay varios archivos de ciudades filtrados según si superan una cantidad de población, cities500.zip, cities1000.zip, cities5000.zip, cities15000.zip. Las columnas son las mismas que las del archivo anterior donde están agrupadas por país pero en este están agrupadas por población.

    1
    2
    
    3128026	Bilbao	Bilbao	BIO,Bil'baa,Bil'bao,Bilbao,Bilbau,Bilbaum,Bilbaw,Bilbo,Bilbo / Bilbao,Bilmpao,El Bocho,bi er ba e,bil ba xo,bilba'o,bilbao,birubao,blbaw,bylbaw,bylbayw,Μπιλμπάο,Билбао,Бильбао,Більбаа,Більбао,Բիլբաո,בילבאו,بلباو,بیلبائو,بیلباو,बिल्बाओ,บิลบาโอ,ბილბაო,ビルバオ,毕尔巴鄂,빌바오
    
    
    cities15000-bilbao.txt

    alternateNamesV2.zip

    Contiene diversa información más extendida de cada uno de los geonames, incluyendo su nombre en diferentes idiomas, el conjunto de códigos postales asociados, identificador del aeropuerto y enlaces a la wikipedia.

    El archivo alternateNamesV2.txt es tan grande que ocupa varios cientos de megabytes descomprimido de modo que casi no se puede abrir con un editor y hay que recurrir al comando grep para buscar dado el geonameid.

    El geonameid de la ciudad de Bilbao es 3128026 y buscando en el archivo se encuentran estas referencias que incluyen los diferentes nombres en diferentes lenguajes, códigos postales, iata y uncl para la denominación del aeropuerto además de otra información como enlaces a la wikipedia. En casi de en un mismo lenguaje haber varios nombres el 1 indica cual es la denominación preferida.

    1
    2
    
    $ grep "3128026" alternateNamesV2.txt
    
    
    grep-alternate-names-bilbao.sh
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    
    1325769	3128026	eu	Bilbo						
    1565386	3128026	es	El Bocho						
    1968272	3128026	es	Bilbao	1					
    1968273	3128026		Bilbao						
    2039076	3128026	eu	Bilbao	1					
    2039077	3128026	pt	Bilbau						
    2039078	3128026	eo	Bilbao						
    2148428	3128026	post	48080						
    2148598	3128026	post	48002						
    2148599	3128026	post	48003						
    2148600	3128026	post	48004						
    2148601	3128026	post	48070						
    2148602	3128026	post	48071						
    2148614	3128026	post	48006						
    2148615	3128026	post	48014						
    2148618	3128026	post	48008						
    2148619	3128026	post	48015						
    2148621	3128026	post	48007						
    2148622	3128026	post	48005						
    2148623	3128026	post	48012						
    2148650	3128026	post	48001						
    2148652	3128026	post	48009						
    2148653	3128026	post	48013						
    2148655	3128026	post	48010						
    2148670	3128026	post	48011						
    2426128	3128026	ru	Бильбао						
    2920566	3128026	link	https://en.wikipedia.org/wiki/Bilbao						
    3055848	3128026	link	https://ru.wikipedia.org/wiki/%D0%91%D0%B8%D0%BB%D1%8C%D0%B1%D0%B0%D0%BE						
    5882689	3128026	ja	ビルバオ						
    5882690	3128026	cy	Bilbo						
    5882691	3128026	os	Бильбао						
    5882692	3128026	ce	Билбао						
    5882693	3128026	fa	بیلبائو						
    5882694	3128026	sr	Билбао						
    5882695	3128026	uk	Більбао						
    5882696	3128026	br	Bilbo						
    5882697	3128026	ug	Bilbaw						
    5882698	3128026	ext	Bilbau						
    5882699	3128026	el	Μπιλμπάο						
    5882700	3128026	ko	빌바오						
    5882701	3128026	bg	Билбао						
    5882702	3128026	he	בילבאו						
    5882703	3128026	be	Більбаа						
    5882704	3128026	la	Bilbaum						
    5882705	3128026	an	Bilbau						
    5882706	3128026	ka	ბილბაო						
    5882707	3128026	th	บิลบาโอ						
    5882708	3128026	zh	毕尔巴鄂						
    5882709	3128026	ar	بلباو						
    5882710	3128026	mr	बिल्बाओ						
    5882711	3128026	gl	Bilbo						
    5882712	3128026	pms	Bilbo / Bilbao						
    5882713	3128026	mhr	Бильбао						
    6634068	3128026	iata	BIO						
    8183693	3128026	hy	Բիլբաո						
    8183694	3128026	mn	Бильбао						
    8183695	3128026	xmf	ბილბაო						
    8183696	3128026	ckb	بیلباو						
    8183697	3128026	kk	Бильбао						
    13750954	3128026	unlc	ESBIO						
    3128026	134091	fa	Gerakān
    grep-alternate-names-bilbao.out

    admin1CodesASCII.txt

    Contiene las unidades administrativas del primer nivel, en el caso de España son las comunidades autónomas. El archivo incluye el geonameid asociado a la comunidad autónoma.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    ES.31	Murcia	Murcia	2513413
    ES.CE	Ceuta	Ceuta	2519582
    ES.07	Balearic Islands	Balearic Islands	2521383
    ES.51	Andalusia	Andalusia	2593109
    ES.53	Canary Islands	Canary Islands	2593110
    ES.54	Castille-La Mancha	Castille-La Mancha	2593111
    ES.57	Extremadura	Extremadura	2593112
    ES.60	Valencia	Valencia	2593113
    ES.34	Asturias	Asturias	3114710
    ES.32	Navarre	Navarre	3115609
    ES.29	Madrid	Madrid	3117732
    ES.27	La Rioja	La Rioja	3336897
    ES.39	Cantabria	Cantabria	3336898
    ES.52	Aragon	Aragon	3336899
    ES.55	Castille and León	Castille and Leon	3336900
    ES.56	Catalonia	Catalonia	3336901
    ES.58	Galicia	Galicia	3336902
    ES.59	Basque Country	Basque Country	3336903
    ES.ML	Melilla	Melilla	6362988
    admin1-es.txt

    admin2Codes.txt

    Contiene las unidades administrativas del segundo nivel, en el caso de España son las provincias. El archivo incluye el geonameid asociado a la provincia.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    
    ES.60.V	Província de València	Provincia de Valencia	2509951
    ES.54.TO	Province of Toledo	Province of Toledo	2510407
    ES.51.SE	Provincia de Sevilla	Provincia de Sevilla	2510910
    ES.53.TF	Provincia de Santa Cruz de Tenerife	Provincia de Santa Cruz de Tenerife	2511173
    ES.51.MA	Provincia de Málaga	Provincia de Malaga	2514254
    ES.53.GC	Provincia de Las Palmas	Provincia de Las Palmas	2515271
    ES.51.J	Provincia de Jaén	Provincia de Jaen	2516394
    ES.51.H	Provincia de Huelva	Provincia de Huelva	2516547
    ES.51.GR	Provincia de Granada	Provincia de Granada	2517115
    ES.54.CU	Provincia de Cuenca	Provincia de Cuenca	2519034
    ES.51.CO	Province of Córdoba	Province of Cordoba	2519239
    ES.54.CR	Provincia de Ciudad Real	Provincia de Ciudad Real	2519401
    ES.51.CA	Provincia de Cádiz	Provincia de Cadiz	2520597
    ES.57.CC	Provincia de Cáceres	Provincia de Caceres	2520610
    ES.57.BA	Provincia de Badajoz	Provincia de Badajoz	2521419
    ES.51.AL	Almería	Almeria	2521883
    ES.60.A	Provincia de Alicante	Provincia de Alicante	2521976
    ES.54.AB	Provincia de Albacete	Provincia de Albacete	2522257
    ES.52.Z	Provincia de Zaragoza	Provincia de Zaragoza	3104323
    ES.55.ZA	Provincia de Zamora	Provincia de Zamora	3104341
    ES.59.BI	Bizkaia	Bizkaia	3104469
    ES.55.VA	Provincia de Valladolid	Provincia de Valladolid	3106671
    ES.52.TE	Provincia de Teruel	Provincia de Teruel	3108125
    ES.56.T	Província de Tarragona	Provincia de Tarragona	3108287
    ES.55.SO	Provincia de Soria	Provincia de Soria	3108680
    ES.55.SG	Provincia de Segovia	Provincia de Segovia	3109254
    ES.39.S	Provincia de Cantabria	Provincia de Cantabria	3109716
    ES.55.SA	Provincia de Salamanca	Provincia de Salamanca	3111107
    ES.58.PO	Provincia de Pontevedra	Provincia de Pontevedra	3113208
    ES.55.P	Provincia de Palencia	Provincia de Palencia	3114530
    ES.58.OR	Provincia de Ourense	Provincia de Ourense	3114964
    ES.58.LU	Provincia de Lugo	Provincia de Lugo	3117813
    ES.55.LE	Provincia de León	Provincia de Leon	3118528
    ES.58.C	Provincia da Coruña	Provincia da Coruna	3119840
    ES.52.HU	Provincia de Huesca	Provincia de Huesca	3120513
    ES.59.SS	Gipuzkoa	Provincia de Guipuzcoa	3120935
    ES.54.GU	Provincia de Guadalajara	Provincia de Guadalajara	3121069
    ES.60.CS	Província de Castelló	Provincia de Castello	3125881
    ES.55.BU	Provincia de Burgos	Provincia de Burgos	3127460
    ES.56.B	Província de Barcelona	Provincia de Barcelona	3128759
    ES.55.AV	Provincia de Ávila	Provincia de Avila	3129138
    ES.59.VI	Araba / Álava	Araba / Alava	3130717
    ES.56.GI	Província de Girona	Provincia de Girona	6355230
    ES.56.L	Província de Lleida	Provincia de Lleida	6355231
    ES.27.LO	Provincia de La Rioja	Provincia de La Rioja	6355232
    ES.29.M	Provincia de Madrid	Provincia de Madrid	6355233
    ES.31.MU	Murcia	Murcia	6355234
    ES.32.NA	Provincia de Navarra	Provincia de Navarra	6355235
    ES.34.O	Province of Asturias	Province of Asturias	6355236
    ES.07.PM	Illes Balears	Illes Balears	6424360
    ES.CE.CE	Ceuta	Ceuta	8335119
    ES.ML.ME	Melilla	Melilla	8335120
    admin2-es.txt

    featureCodes.txt, timeZones.txt y hierarchy.zip

    Los geonames pueden tener un metadato asociado con varios identificadores que en la documentación se denominan features, hay varias categorías de identificadores y varios identificadores dentro de cada categoría. Por ejemplo, la feature P se utiliza para ciudades y villas y PPLA1, PPLA2, PPLA3, PPLA4, PPLA5 indica el nivel de unidad administrativa. Las features permiten conocer a qué representa el geoname por ejemplo si es un aeropuerto o una universidad entre muchos otros elementos.

    El archivo hierarchy.zip permite reconstruir la relación jerárquica de los diferentes geonames

    shapes_all_low.zip

    Contiene el polígono del geoname que permite una representación gráfica de su superficie, por ejemplo del país.

    1
    2
    
    337996	{"type":"Polygon","coordinates":[[[37.916,14.894],[38.045,14.72],[38.258,14.679],[38.455,14.413],[38.797,14.467],[38.978,14.545],[39.032,14.639],[39.154,14.653],[39.261,14.516],[39.252,14.409],[39.346,14.466],[39.373,14.54],[39.538,14.491],[39.508,14.55],[39.587,14.609],[39.77,14.546],[39.93,14.415],[40.127,14.547],[40.213,14.387],[40.26,14.411],[40.942,14.082],[41.249,13.61],[41.644,13.386],[42.056,12.801],[42.218,12.765],[42.404,12.469],[41.973,11.839],[41.835,11.733],[41.771,11.493],[41.81,11.278],[41.787,10.979],[41.947,10.913],[42.066,10.927],[42.146,10.983],[42.42,10.982],[42.634,11.096],[42.689,11.055],[42.752,11.08],[42.79,10.988],[42.967,10.997],[42.67,10.624],[42.799,10.452],[42.854,10.219],[43.009,10.086],[43.087,9.903],[43.255,9.847],[43.299,9.606],[43.396,9.554],[43.46,9.419],[43.643,9.357],[43.995,9.003],[47.001,8.001],[48.001,8.001],[44.97,4.913],[44.01,4.959],[43.635,4.855],[43.082,4.607],[42.844,4.284],[42.541,4.211],[42.079,4.179],[41.945,4.01],[41.833,3.947],[41.718,3.994],[41.677,3.958],[41.551,3.983],[41.197,3.937],[40.756,4.285],[39.866,3.871],[39.773,3.672],[39.553,3.405],[39.493,3.471],[39.192,3.483],[39.079,3.545],[38.903,3.517],[38.712,3.577],[38.684,3.629],[38.576,3.609],[38.536,3.659],[38.446,3.607],[38.127,3.61],[37.029,4.388],[36.843,4.452],[36.05,4.457],[35.94,4.551],[35.949,4.631],[35.809,4.791],[35.862,5.32],[35.538,5.424],[35.316,5.335],[35.315,5.498],[35.126,5.624],[35.13,5.687],[34.998,5.897],[35.005,6.075],[34.95,6.247],[35.022,6.447],[34.939,6.558],[34.864,6.611],[34.768,6.594],[34.64,6.74],[34.531,6.745],[34.543,6.817],[34.471,6.919],[34.293,6.948],[34.305,6.978],[34.192,7.041],[34.19,7.132],[34.022,7.246],[34.002,7.421],[33.716,7.663],[33.542,7.694],[33.47,7.752],[33.323,7.708],[33.247,7.779],[33.046,7.79],[32.998,7.944],[33.121,8.111],[33.193,8.129],[33.17,8.296],[33.219,8.435],[33.618,8.472],[33.7,8.378],[33.769,8.368],[33.876,8.411],[33.903,8.486],[34.024,8.489],[34.145,8.607],[34.102,9.557],[34.224,9.887],[34.233,10.049],[34.334,10.119],[34.295,10.585],[34.466,10.814],[34.603,10.906],[34.795,10.754],[34.786,10.706],[34.868,10.731],[34.869,10.805],[34.987,10.89],[34.953,10.949],[35.001,11.187],[34.928,11.241],[35.077,11.535],[35.044,11.734],[35.27,11.944],[35.662,12.637],[36.009,12.724],[36.16,12.697],[36.176,12.904],[36.143,12.948],[36.497,13.835],[36.466,13.978],[36.556,14.284],[36.866,14.322],[37.018,14.252],[37.097,14.275],[37.135,14.41],[37.312,14.448],[37.523,14.182],[37.916,14.894]]]}
    
    
    shape-es.txt

    Información de la base de datos

    Cada uno de los elementos geográficos tiene asociado un identificador de geonameid, con este identificador es posible obtener más información en el archivo alternateNamesV2.zip y relacionar la información de unos archivos con la información de otros archivos. La información que incluyen están en columnas separadas por tabuladores, las columnas son las indicadas en estructura de la tabla geoname y alternate names.

    Los archivos contienen mucha información, posiblemente en una aplicación solo interese parte de toda esa información y se puede ignorar el resto. Por ejemplo, en la información de los países se incluye su extensión de superficie, población, continente moneda, lenguaje o la capital del país que para validar direcciones no es necesaria pero la moneda o lenguajes es útil para mostrar precios o textos según el idioma del país.

    Hay otra información muy útil como los nombres de las ciudades en diferentes idiomas e incluso las abreviaturas de los aeropuertos. Por supuesto están las expresiones regulares de los códigos postales o los códigos de prefijo de los teléfonos, los propios del país conoceremos pero la de muchos otros países seguro que no.

    La expresión regular de España es esta.

    1
    2
    
    ^(\d{5})$
    
    
    regexp-es.txt

    La de Reino Unido es esta otra.

    1
    2
    
    ^([Gg][Ii][Rr]\s?0[Aa]{2})|((([A-Za-z][0-9]{1,2})|(([A-Za-z][A-Ha-hJ-Yj-y][0-9]{1,2})|(([A-Za-z][0-9][A-Za-z])|([A-Za-z][A-Ha-hJ-Yj-y][0-9]?[A-Za-z]))))\s?[0-9][A-Za-z]{2})$
    
    
    regexp-uk.txt

    Cómo acceder a la información

    Los archivos son muy grandes, seguramente no sea necesaria toda la información y no está en el formato adecuado para un rápido acceso al dato que se necesita en una aplicación, con lo que hay que procesarlos para adaptarlos. Los archivos son archivos de texto con los datos de las columnas separados por tabuladores, una librería para procesar los archivos con el lenguaje Java es OpenCSV.

    Es necesario procesar los archivos de alguna forma para extraer y transformar la información de los archivos de GeoNames a un formato adecuado para la aplicación posiblemente con un script en el lenguaje Groovy, Java, Kotlin u otro lenguaje. La información geográfica quizá no cambie muy habitualmente pero seguramente hay que procesar los archivos cada cierto tiempo o si por alguna circunstancia se produce un cambio significativo que requiera una actualización.

    Una vez disponible la información en la aplicación conviene crear una serie de clases que hagan de fachada en el acceso a la información de tal forma que el resto del código no se acople a la implementación interna de los datos que puede cambiar en el futuro. Hay varias formas de implementación, una en que las clases de información geográfica serían un módulo en la aplicación y propietaria de los datos geográficos. Otra opción es que las tablas estén en una base de datos y sea compartida por varias aplicaciones, en este caso las tablas o vistas se convierten en una interfaz para las aplicaciones.

    » Leer más, comentarios, etc...

    Variable not found

    Restricciones de ruta personalizadas parametrizables en ASP.NET Core

    noviembre 29, 2022 07:05

    ASP.NET Core

    Hace algunas semanas vimos cómo crear inline route constraints, o restricciones de ruta en línea en ASP.NET Core, y creamos un ejemplo simple que permitía al sistema de routing detectar si el valor suministrado a través de un parámetro de ruta era una palabra palíndroma.

    Para ello, creamos la restricción "palindrome" que, implementada en la clase PalindromeConstraint podíamos usar de la siguiente forma:

    // Uso en minimal API:
    app.MapGet("/test/{str:palindrome}", (string str) => $"{str} is palindrome");

    // Uso en MVC:
    public class TestController : Controller
    {
    [HttpGet("/test/{str}")]
    public string Text(string str) => $"{str} is palindrome";
    }

    Sin embargo, si atendemos a la lista de restricciones disponibles de serie en ASP.NET Core, vemos que hay algunas de ellas que son parametrizadas, como maxlength o range:

    Plantilla de rutaSignificado
    /order/{orderId:minlength(5)orderId debe tener como mínimo 5 caracteres
    /setAge/{age:int:range(0,120)age debe ser un entero entre 0 y 120

    En este post vamos a ver precisamente eso, cómo crear una restricción personalizada con parámetros.

    Creación de restricciones parametrizadas

    Como ejemplo, vamos a desarrollar una restricción que permitirá verificar que el valor suministrado sea divisible por el número que indiquemos. Es decir, la idea es poder especificar una restricción como la que usamos en el siguiente ejemplo:

    app.MapGet("/test/{value:int:divisibleBy(3)}", (int value) => $"{value} is divisible by 3"); 

    Como hicimos en el post anterior, vamos a crear la clase que implementa la restricción. Fijaos que en esta ocasión insertaremos en ella un constructor en el que recibiremos los parámetros de la restricción:

    public class DivisibleByConstraint : IRouteConstraint
    {
    private readonly int _divisor;

    public DivisibleByConstraint(int divisor)
    {
    _divisor = divisor;
    }

    public bool Match(HttpContext httpContext, IRouter route, string routeKey,
    RouteValueDictionary values, RouteDirection routeDirection)
    {
    if (!int.TryParse(values[routeKey]?.ToString(), out var intValue))
    return false;
    return intValue % _divisor == 0;
    }
    }

    Hecho esto, ya podríamos registrarla en el sistema de routing, como siempre, en Program.cs:

    var builder = WebApplication.CreateBuilder(args);
    ...
    builder.Services.AddRouting(options =>
    {
    options.ConstraintMap.Add("divisibleBy", typeof(DivisibleByConstraint));
    });
    ...

    El tipo del parámetro del constructor debe coincidir con el usado en el momento de insertar la restricción en la ruta. Por ejemplo, en el caso anterior, el constructor espera recibir un entero, por lo que el siguiente mapeo de ruta generará un error en tiempo de ejecución la primera vez que se intente matchear con una petición entrante:

    // Esto fallará en tiempo de ejecución, porque el 
    // constructor de DivisibleByContraint espera un entero:
    app.MapGet("/div/{value:int:divisibleBy(a)}", (int value) => $"{value} is ok");
    GET https://localhost:7196/test/6
    ...
    HTTP 500
    Microsoft.AspNetCore.Routing.RouteCreationException: An error occurred while trying to
    create an instance of 'DivisibleByConstraint'.
    ---> System.FormatException: Input string was not in a correct format.
    ...

    También podemos crear constraints que soporten más de un parámetro añadiendo todos los que necesitemos al constructor. Por ejemplo, hemos visto que la restricción range() proporcionada de serie por el framework, permite indicar el rango de valores permitidos; en la práctica, la clase que implementa esta constraint simplemente ha definido un constructor con los dos parámetros entrantes:

    public class RangeRouteConstraint: IRouteConstraint
    {
    public RangeRouteConstraint(long min, long max)
    {
    ...
    }
    }

    También existe la posibilidad de usar varios constructores, lo cual puede ser útil si queremos implementar restricciones con un número variable de parámetros. Aunque resulta algo farragoso, podemos crear distintos constructores para cubrir los casos que queramos soportar; por ejemplo, a continuación vemos la restricción OneOfContraint que permite verificar si el valor del parámetro de ruta coincide con alguno de los valores (hasta cuatro) que indiquemos en línea:

    public class OneOfConstraint : IRouteConstraint
    {
    private readonly HashSet<string> _allowedValues = new();

    public OneOfConstraint(string v1) => Add(v1);
    public OneOfConstraint(string v1, string v2) => Add(v1, v2);
    public OneOfConstraint(string v1, string v2, string v3) => Add(v1, v2, v3);
    public OneOfConstraint(string v1, string v2, string v3, string v4) => Add(v1, v2, v3, v4);

    public bool Match(HttpContext? httpContext, IRouter? route, string routeKey,
    RouteValueDictionary values, RouteDirection routeDirection)
    {
    var value = values[routeKey]?.ToString();
    return value != null && _allowedValues.Contains(value);
    }

    private void Add(params string[] args)
    {
    ArgumentNullException.ThrowIfNull(args);
    foreach (var arg in args)
    {
    _allowedValues.Add(arg);
    }
    }
    }

    Fijaos que debemos utilizar sobrecargas para cada número de parámetros, pues no se permite el uso de params o arrays en el constructor.

    Una vez registrada en el sistema de routing, podríamos usar la restricción de la siguiente manera:

    app.MapGet("/one-or-two/{value:oneOf(1,2)}", (string value) => $"{value} is 1-2");
    app.MapGet("/one-two-or-three/{value:oneOf(1,2,3)}", (string value) => $"{value} is 1-3");

    ¡Espero que os sea de utilidad!

    Publicado en Variable not found.

    » Leer más, comentarios, etc...

    info.xailer.com

    Xailer 8.1 – Soporte High DPI Desktop Application Development

    noviembre 28, 2022 07:11

    Xailer 8.1 incluye soporte de High DPI lo que permite sacar la máxima resolución de los potentes monitores 4K. Para activarlo tan sólo que poner la propiedad Appliction:lDpiAware a verdadero al principio de ejecución de su aplicación. Eso es todo. Realmente esta propiedad lo que hace es que su programa reconozca la verdadera resolución del monitor.

    Cuando windows se ejecuta en un monitor de alta resolución y se establece una escala distinta del 100%, los programas reciben una información «ajustada» de la resolución, no la resolución real. De igual forma, cuando un programa establece unas coordenadas determinadas en una ventana, windows reajusta esas coordenadas según el valor de la escala que tenga establecida. P.ej., si windows está configurado con una escala del 150%, cuando un programa establece unas dimensiones de 400×400 pixels en una ventana, windows lo reajusta a un tamaño real de 600×600 pixels, incluidos todos los controles que contenga.

    Esto hace que no tengamos que preocuparnos por la escala que tenga windows configurada, pero tiene el inconveniente de que los textos y las imágenes se ven difuminados. Cuando establecemos Application:lDpiAware a .T., nuestro programa trabajará con coordenadas reales, sin ajustes por parte de windows. Las imágenes y textos se verán perfectamente nítidos y en su tamaño original, aunque esto suponga que se verán más pequeños de lo esperado. En estos casos, podemos usar la propiedad Application:nScale para ajustar nosotros la escala deseada sin perder la nitidez de imágenes y textos.

    Más sobre Xailer 8.1 en breve.

    Un saludo

    » Leer más, comentarios, etc...

    info.xailer.com

    Xailer 8.1 – Mejoras en el generador de informes

    noviembre 28, 2022 06:13

    En Xailer 8.1 hemos introducidos dos mejoras en nuestro generador de informes que han sido largamente solicitadas, que son:

    1. El envío directo de los informes a archivos PDF
    2. La posibilidad de anidar múltiples informes en uno solo

    Para la primera opción se ha creado una nueva cláusula en el comando REPORT con el nombre TO PDF <cPdfFilename>. Para que está cláusula funcione correctamente el usuario debe de estar utilizando Windows 10 o posterior.

    Para la segunda opción hemos creado la cláusula KEEP también para el comando REPORT. Y su uso es muy sencillo: Cuando cree un informe con esta cláusula, éste no se mostrará y se acumulará internamente. Puede apilar todos los informes que desee, incluso con distinta orientación de páginas. Para forzar la terminación del informe no tiene más que quitar la cláusula KEEP en el último informe que realice. Esta cláusula está pensada para ser utilizada conjuntamente con la cláusula PREVIEW o TO PDF en el último informe que se genera.

    Más novedades sobre Xailer 8.1 en breve.

    Un saludo

    » Leer más, comentarios, etc...

    info.xailer.com

    Xailer 8.1 – Mejoras en TMemDataset

    noviembre 28, 2022 09:15

    Esta es una pequeña mejora, pero creo que es digna de mención ya que puede interesar a más de uno.

    Para los puristas del modelo vista-controlador, el uso de los clásicos Data-controls de Xailer no es lo apropiado, ya que todo el proceso de edición se produce también en la vista, cuando debería realizarse en el controlador. La forma más conveniente de evitar este problema es utilizar objetos de tipo TMemDataset que devuelven un recordset de memoria que está completamente desvinculado de la tabla de la cual procede. Pero tiene el inconveniente de que las operaciones de inserción y edición son más complejas ya que requiere de la construcción de las cadenas SQL de insert y update de forma manual. Veamos como resuelve este problema Xailer 8.1 con un pequeño ejemplo:

    CLASS TControlador
      oDataSource
      METHOD RSClientes( oRs )
      METHOD ClientesUpdate( oRs, nId )
    END CLASS
    METHOD RsClientes( oRS as CLASS TMemdataset ) CLASS TControlador
      WITH OBJECT ::oDatasource
        IF oRs != NIL
          :QueryMemDataset("SELECT * FROM CLIENTES", "", oRs )
        ELSE
          oRs := :QueryMemDataset("SELECT * FROM CLIENTES")
        ENDIF
      END WITH 
    RETURN oRs
    METHOD ClientesUpdate( oRS, nId ) CLASS TControlador
      WITH OBJECT ::oDatasource
        TRY
          :BeginTrans() 
          :Execute(oRs:SqlUpdate("idCliente", nId ))
          // Aquí se pueden hacer más operaciones de forma clara  
          :CommitTrans()  
        CATCH
          :RollbackTrans()
        END
      END WITH 
    RETURN NIL
    

    La operación de update se realiza en el controlador con la ventaja adicional de que podemos incluir cualquier operación dentro de la misma transacción. Para ello se utiliza el nuevo método SqlUpdate( aWhereCols, aWhereValues) que crea la instrucción SQL con sólo las columnas que han cambiado su valor. Como se puede imaginar, también hay un nuevo método de nombre SqlInsert().

    Otra de las particularidades que tiene este código en su método RsClientes() que creo es muy interesante, es que el objeto TMemDataset se pasa como parámetro, en vez de crearse en el propio método y devolverse como valor de retorno, que podría ser la primera aproximación que haría cualquier programador. Esta técnica tiene una gran ventaja, que consiste en poder incorporar en el propio formulario un TMemDataset vacío e incluso vincularlo con un browse en el cual hemos definido las columnas completamente y todo de forma visual. Si nuestro método fuera el responsable de crear el TMemdatset y lo retornase como valor de retorno para luego asignarlo al browse con oBrowse:oDataset := oControlador:RsClientes(…) observariamos como todas nuestras columnas del browse desaparecerían ya que se ha asignado un nuevo dataset y eso provoca un reset de todo el control. Por el contrario si pasamos el recordset como parámetro todo se mantiene perfectamente.

    Y eso es todo por ahora. En breve, más novedades.

    Un saludo

    » Leer más, comentarios, etc...

    Variable not found

    Enlaces interesantes 506

    noviembre 28, 2022 07:05

    Enlaces interesantes

    El desconocido (al menos para mí) código de estado HTTP 506 (Variant also negotiates) indica que el servidor tiene un error de configuración en el contexto de la Negociación Transparente de Resultados (RFC 2295). Permite informar al cliente de que la variante que ha elegido está configurada para participar en la negociación de contenidos, pero no es válida como endpoint para el proceso de negociación-

    Ahí van los enlaces recopilados durante la semana pasada. Espero que os resulten interesantes. :-)

    Por si te lo perdiste...

    .NET Core / .NET

    ASP.NET Core / ASP.NET / Blazor

    Azure / Cloud

    Conceptos / Patrones / Buenas prácticas

    Data

      Web / HTML / CSS / Javascript

      Visual Studio / Complementos / Herramientas

      .NET MAUI / Xamarin

      Otros

      Publicado en Variable not found.

      » Leer más, comentarios, etc...

      info.xailer.com

      Xailer 8.1: Mejoras en TWebDatasource

      noviembre 26, 2022 05:21

      Xailer 8.1 será publicado en un futuro próximo y en una serie de breves artículos os iremos desgranando las novedades que incorporará Xailer 8.1, dejando para el final las más importantes. Éste es uno de ellos.

      La próxima versión de Xailer 8.1 incorpora todo lo necesario para poder prescindir de cualquier tipo de conexión FTP. Es decir, todas las operaciones clásicas que se realizan por FTP, como:

      • carga de ficheros
      • descarga de ficheros
      • borrado de ficheros
      • renombrado de ficheros
      • listado de directorios
      • creación de directorios
      • borrado de directorios

      Son incluidas en la nueva clase TWebDataSource.

      Nota: Los servicios FTP que incluye TWebDatasource están limitados al directorio donde se encuentra el módulo PHP y sus directorios dependientes.

      El uso del protocolo FTP cada día es más complejo debido a las distintas opciones existentes: FTP seguro a través de certificado digital (FTPS) que puede ser implícito o explicito, a través de protocolo SSH (SFTP) o FTP no seguro. Desgraciadamente el soporte de Microsoft Windows en su librería WININET a cualquier tipo de FTP aparte del no seguro es nula y la opción más apropiada para atacar este problema habría sido utilizar la librería CURL que está disponible en Harbour, pero su uso es complicado ya que exige la instalación de bastantes DLLs. La otra opción sería utilizar CURL por línea de comando que está disponible en Windows desde hace tiempo, pero eso implica tener que ‘lanzar’ un programa de consola desde nuestra aplicación que no suele gustar ni a programadores ni a usuarios y tampoco a los antivirus.

      Por todos estos motivos y uno adicional, que es que es prácticamente imposible que el puerto 80 esté cerrado (que es el que usa la Web), hemos decidido incorporar a nuestra clase TWebDatasource de todos los servicios que incluiría cualquier clase de manejo de ficheros por FTP.

      Pero ya puestos, hemos añadido un servicio más, que es la posibilidad de enviar correos electrónicos utilizando uno de las librerías más utilizadas en PHP, que es PHPMailer. Al igual que ocurre con las credenciales de acceso a la base de datos que hay que incluir en el archivo PHP, lo mismo ocurre con las credenciales para el envío de correo. En el propio archivo PHP y bajo la función XA_SendMail() de una forma muy sencilla y sin tener que saber absolutamente nada de PHP se pueden establecer todos los parámetros que deseemos para el envío de los correos.

      Os recordamos que está clase sólo está disponible en la versión Enterprise de Xailer.

      Un saludo

      » Leer más, comentarios, etc...

      Blog Bitix

      Obtener el ancho y alto, escalar y convertir a otro formato imágenes con Java

      noviembre 25, 2022 06:30

      Las aplicaciones que tratan con imágenes es posible que necesiten conocer algunos datos de la imagen como anchura y altura, realizar algunas operaciones de manipulación básicas como escalado y conversión entre formatos. El propio JDK de Java ofrece algunas clases y soporta varios de los formatos más comunes de imagen. Para usos más avanzados hay que recurrir a librerías y si no fueran suficientes a comandos del sistema más avanzados.

      Java

      El lenguaje de programación Java es un lenguaje de propósito general con el que es posible realizar cualquier tarea. Sin embargo, Java como lenguaje o plataforma aparte de Android el mayor uso que se le da es para aplicaciones y servicios en el lado del servidor.

      Una de las necesidades que pueden surgir es escalar una imagen o cambiarle de formato, aunque Java solo ofrece un soporte básico para el tratamiento de imágenes, hay varias librerías pero pocas bien mantenidas y con buen soporte, si ni una librería ofrece la funcionalidad deseada en el caso más extremo se puede invocar a un proceso del sistema que ofrezca la funcionalidad com ImageMagick o FFmpeg.

      La conversión y cambio de formato es un proceso costoso en CPU y memoria ya que requieren cargar la imagen en memoria más si las imágenes a tratar son muy grandes, de varias decenas de megabytes.

      El propio JDK ofrece en su API soporte para los formatos de imágenes más comunes y algunas manipulaciones de imagen, hay varias librerías Java para la manipulación de imágenes y finalmente están los comandos del sistema.

      Contenido del artículo

      Formatos de imágenes

      Hay varios formatos de imágenes que se pueden categorizar en dos grandes grupos: imágenes sin pérdida o con pérdida. Los formatos sin pérdida son más fieles a la imagen original pero con mayor tamaño que la equivalente en un formato con pérdida de calidad, en las imágenes con pérdida de calidad los algoritmos tratan de que visualmente la pérdida sea lo menos apreciable posible con un tamaño de imagen significativamente menor.

      El formato png es un formato sin pérdida que ofrece buenos resultados para capturas de pantalla del ordenador. Para fotos el formato jpg ofrece un archivo de tamaño significativamente más reducido con una calidad bastante fiel al original.

      Más recientemente han surgido nuevos formatos de imagen que utilizan otros algoritmos para la reducción de tamaño heic y heif utilizan el algoritmo con coste de licencia de x265 y el formato sin coste de licencia webp que ofrece similar o mejor resultado que el x265 que además soporta imágenes sin pérdida.

      Procesar imágenes con Java

      En el propio JDK hay varias clases para la utilización de imágenes en el paquete java.awt.

      Formatos de imagen soportados

      Los formatos de imagen soportados se pueden obtener mediante código y son extensibles añadiendo librerías al classpath, es posible que un formato se soporte en modo lectura pero no en escritura.

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      
      package io.github.picodotdev.blogbitix.javaimageprocess;
      
      import java.awt.*;
      import java.awt.image.BufferedImage;
      import java.awt.image.BufferedImageOp;
      import java.io.FileInputStream;
      import java.io.FileOutputStream;
      import java.io.IOException;
      import java.io.InputStream;
      import java.io.OutputStream;
      import java.text.MessageFormat;
      
      import javax.imageio.ImageIO;
      
      import com.twelvemonkeys.image.ResampleOp;
      import net.coobird.thumbnailator.Thumbnails;
      
      public class Main {
      
          public static void main(String[] args) throws Exception {
              printSupportedFormats();
      
              BufferedImage image = readImage(Main.class.getResourceAsStream("/gnome.jpg"));
              BufferedImage imageWebp = readImage(Main.class.getResourceAsStream("/gnome.webp"));
      
              BufferedImage scaledImage = scaleJava(image);
              convertJava(scaledImage, ImageFormat.JPG, new FileOutputStream("gnome-scaled-java.jpg"));
      
              scaleThumbnailator(Main.class.getResourceAsStream("/gnome.jpg"), new FileOutputStream("gnome-scaled-thumbnailator.jpg"));
              scaleImageMagick(Main.class.getResourceAsStream("/gnome.jpg"), new FileOutputStream("gnome-scaled-imagemagick.jpg"));
              scaleTwelvemonkeys(readImage(Main.class.getResourceAsStream("/gnome.jpg")), new FileOutputStream("gnome-scaled-twelvemonkeys.jpg"));
      
              printImageWidthHeight("original", image);
              printImageWidthHeight("original webp", imageWebp);
              printImageWidthHeight("scaled", scaledImage);
              printImageWidthHeight("scaled thumbnailator", readImage(new FileInputStream("gnome-scaled-thumbnailator.jpg")));
              printImageWidthHeight("scaled imagemagick", readImage(new FileInputStream("gnome-scaled-imagemagick.jpg")));
              printImageWidthHeight("scaled twelvemonkeys", readImage(new FileInputStream("gnome-scaled-twelvemonkeys.jpg")));
          }
      
          ...
      }
      
      Main.java
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      
      package io.github.picodotdev.blogbitix.javaimageprocess;
      
      ...
      
      public class Main {
      
          ...
      
          private static void printSupportedFormats() {
              String[] readerFormatNames = javax.imageio.ImageIO.getReaderFormatNames();
              String[] writerFormatNames = javax.imageio.ImageIO.getWriterFormatNames();
      
              System.out.printf("Reader format names: %s%n", String.join(",", readerFormatNames));
              System.out.printf("Writer format names: %s%n", String.join(",", writerFormatNames));
          }
      
          ...
      }
      
      Main-formats.java

      En esta lista de formatos aparece webp como formato soportados en la lectura porque en el ejemplo de código está incluida la librería de TwelveMonkeys que añade el soporte para la lectura de webp.

      1
      2
      
      Reader format names: JPG,jpg,tiff,bmp,BMP,wbp,WBP,gif,GIF,WBMP,png,PNG,JPEG,webp,tif,TIF,TIFF,WEBP,jpeg,wbmp
      Writer format names: JPG,jpg,tiff,bmp,BMP,gif,GIF,WBMP,png,PNG,JPEG,tif,TIF,TIFF,wbmp,jpeg
      
      Main-formats.out

      Lectura y escritura de una imagen

      La clase que representa una imagen en Java es BufferedImage que se obtiene al leer el archivo del sistema de archivos o el InputStream, para la lectura y la escritura está la clase ImageIO.

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      
      package io.github.picodotdev.blogbitix.javaimageprocess;
      
      ...
      
      public class Main {
      
          ...
      
          private static BufferedImage readImage(InputStream is) throws IOException {
              return ImageIO.read(is);
          }
      
          private static void writeImage(BufferedImage image, ImageFormat format, OutputStream os) throws IOException {
              ImageIO.write(image, format.name(), os);
          }
      
          ...
      }
      
      Main-read-write.java

      La imagen en formato jpg tiene un tamaño de 192 KB y en formato webp tiene un tamaño de 80 KB, menos de la mitad en webp con una calidad similar.

      Imagen en formato jpg Imagen en formato webp

      Imagenes en formatos jpg y webp

      Obtener el ancho y alto de una imagen

      Una vez obtenida una instancia de BufferedImage esta clase ofrece métodos para conocer el ancho y alto de la imagen y a partir de estos la proporción o aspect ratio entre ambos valores.

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      
      package io.github.picodotdev.blogbitix.javaimageprocess;
      
      ...
      
      public class Main {
      
          ...
      
          private static void printImageWidthHeight(String name, BufferedImage image) {
              System.out.printf("Width (%s): %s%n", name, image.getWidth());
              System.out.printf("Height (%s): %s%n", name,  image.getHeight());
          }
      
          ...
      }
      
      Main-width-height.java
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      
      Width (original): 1920
      Height (original): 1072
      Width (original webp): 1920
      Height (original webp): 1072
      Width (scaled): 650
      Height (scaled): 362
      Width (scaled thumbnailator): 650
      Height (scaled thumbnailator): 363
      Width (scaled imagemagick): 650
      Height (scaled imagemagick): 363
      Width (scaled twelvemonkeys): 650
      Height (scaled twelvemonkeys): 362
      Main-width-height.out

      Escalar una imagen

      Escalar una imagen consiste en cambiar de tamaño a la imagen, normalmente el escalado se realiza conservando la proporción de anchura y altura para no distorsionar la imagen original.

      El escalado suele realizarse para reducir la imagen con lo que se perderá información o píxeles, dependiendo del algoritmo de escalado aplicado la calidad y tamaño de la imagen resultante es diferente. En un escalado que implica reducir el tamaño los bytes de la imagen escalada serán menores.

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      
      package io.github.picodotdev.blogbitix.javaimageprocess;
      
      ...
      
      public class Main {
      
          ...
      
          private static BufferedImage scaleJava(BufferedImage image) {
              Resolution scaledResolution = new Resolution(image.getWidth(), image.getHeight()).scale(650, 450);
              BufferedImage scaledImage = new BufferedImage(scaledResolution.getWidth(), scaledResolution.getHeight(), BufferedImage.TYPE_INT_RGB);
              Graphics2D graphics2D = scaledImage.createGraphics();
              graphics2D.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
              graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
              graphics2D.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
              graphics2D.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
              graphics2D.drawImage(image, 0, 0, scaledResolution.getWidth(), scaledResolution.getHeight(), null);
              graphics2D.dispose();
              return scaledImage;
          }
      
          ...
      }
      
      Main-scale-java.java

      Dependiendo de las opciones empleadas en el escalado la calidad y tamaño resultante es diferente y hay que conocerlas. Utilizando una librería permite realizar la operación de escalado con mejor calidad.

      Imagen escalada con Java

      Imagen escalada con Java

      Cambiar el formato de una imagen

      Cambiar de formato a una imagen es otra operación común, por ejemplo convertir al formato webp que ofrece una calidad mejor en el mismo tamaño o un significativo reducción de tamaño que puede llegar al más del 50% con una calidad similar.

      Una vez cargada la imagen en un BufferedImage es posible escribir la imagen a un archivo en cualquier otro formato de escritura soportado.

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      
      package io.github.picodotdev.blogbitix.javaimageprocess;
      
      ...
      
      public class Main {
      
          ...
      
          private static void convertJava(BufferedImage image, ImageFormat format, OutputStream os) throws IOException {
              ImageIO.write(image, format.name().toLowerCase(), os);
          }
      
          ...
      }
      
      Main-convert-java.java

      Librerías de terceros

      Hay varias librerías que añaden o facilitan algunas funcionalidades sobre la API de Java. Thuilmator permite realizar escalados de imágenes de una forma más sencilla que la API de Java.

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      
      package io.github.picodotdev.blogbitix.javaimageprocess;
      
      ...
      
      public class Main {
      
          ...
      
          private static void scaleThumbnailator(InputStream is, OutputStream os) throws IOException {
              Thumbnails.of(is)
                      .size(650, 450)
                      .outputQuality(0.9)
                      .outputFormat("jpg")
                      .toOutputStream(os);
          }
      
          ...
      }
      
      Main-scale-thuilmator.java

      Otra librería es TwelveMonkeys que añade soporte para algunos formatos de imagen como webp.

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      
      package io.github.picodotdev.blogbitix.javaimageprocess;
      
      ...
      
      public class Main {
      
          ...
      
          private static void scaleTwelvemonkeys(BufferedImage image, OutputStream os) throws IOException, InterruptedException {
              Resolution scaledResolution = new Resolution(image.getWidth(), image.getHeight()).scale(650, 450);
              BufferedImageOp resampler = new ResampleOp(scaledResolution.getWidth(), scaledResolution.getHeight(), ResampleOp.FILTER_LANCZOS);
              BufferedImage scaledImage = resampler.filter(image, null);
              writeImage(scaledImage, ImageFormat.JPG, os);
          }
      
          ...
      }
      
      Main-scale-twelvemonkeys.java

      El tamaño de la imagen escalada por el código Java es de 28 KB, el tamaño de la imagen de Thuilmator es de 38 KB y el tamaño de TwelveMonkeys es de 26 KB con algunas pequeñas diferencias apreciables en el resultado de cada una de ellas.

      Imagen escalada con Thumbnailator Imagen escalada con Twelvemonkeys Imagen escalada con ImageMagick y Java

      Imagen escaladas con diferentes librerías (Thumbnailator, Twelvemonkeys y ImageMagick)

      Usando ImageMagick

      Las funcionalidades de Java y de las librerías de Java para casos avanzados no son suficientes. El software de ImageMagick ofrece a través de un comando muchas más opciones para la manipulación de imágenes. El inconveniente de ImageMagick es que para ser usado desde Java requiere instalar el comando en el sistema. El uso de ImageMagick desde Java es como la invocación de cualquier otro proceso del sistema, es posible pasar la imagen a manipular por la entrada estándar del sistema y obtener la imagen resultado por la salida estándar del sistema del proceso.

      Si no hay ninguna librería Java que proporcione soporte para la escritura a formato webp el siguiente código invoca el comando convert de ImageMagick para realizar la conversión. El tamaño de la imagen en formato webp es de 80 KB que son muchos menos que en cualquiera de los formatos jpg generados con Java de los casos anteriores.

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      
      package io.github.picodotdev.blogbitix.javaimageprocess;
      
      ...
      
      public class Main {
      
          ...
      
          private static void scaleImageMagick(InputStream is, OutputStream os) throws IOException, InterruptedException {
              ProcessBuilder builder = new ProcessBuilder().command("convert", "-resize", "650x450", "jpeg:-", "jpeg:-");
              Process process = builder.start();
      
              process.getOutputStream().write(is.readAllBytes());
              process.getOutputStream().close();
              os.write(process.getInputStream().readAllBytes());
      
              process.waitFor();
              int value = process.exitValue();
              if (value != 0) {
                  throw new IOException(MessageFormat.format("Código de salida con error (%d)", value));
              }
          }
      
          ...
      }
      
      Main-scale-imagemagick.java
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      
      package io.github.picodotdev.blogbitix.javaimageprocess;
      
      ...
      
      public class Main {
      
          ...
      
          private static void convertImageMagick(InputStream is, OutputStream os) throws IOException, InterruptedException {
              ProcessBuilder builder = new ProcessBuilder().command("convert", "jpeg:-", "webp:-");
              Process process = builder.start();
      
              process.getOutputStream().write(is.readAllBytes());
              process.getOutputStream().close();
              os.write(process.getInputStream().readAllBytes());
      
              process.waitFor();
              int value = process.exitValue();
              if (value != 0) {
                  throw new IOException(MessageFormat.format("Código de salida con error (%d)", value));
              }
          }
      }
      
      Main-convert-imagemagick.java

      Imagen convertida a webp con ImageMagick y Java

      Imagen convertida a webp con ImageMagick y Java
      Terminal

      El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando siguiente comando:
      ./gradlew run

      » Leer más, comentarios, etc...

      Arragonán

      Responsabilidades de la Arquitectura de Software

      noviembre 24, 2022 12:00

      La mayor parte de las veces cuando discuto sobre cuestiones relacionadas con Arquitectura de Software suele ser para referirme a prácticas (cómo diagramar y comunicar, tipos de workshops, ADRs, ASRs, prototipado…) o a enfoques de soluciones (monolitos modulares, microservicios, DDD, servicios de cloud en particular, contenedores, hexagonal, event-driven…).

      Pero cuando hace unos meses desde el departamento de tecnología de Grupo Carreras (con quienes llevo un tiempo colaborando de vez en cuando) me pidieron que preparase una charla para un evento interno sobre la importancia de la arquitectura de software. Tenía claro que no podía enfocarme demasiado en prácticas y soluciones, porque iba a haber combinación de público técnico y no técnico.

      Finalmente opté por ser más abstracto y hablar acerca de las responsabilidades asociadas a la arquitectura de software, ya que podía conectarlo con referencias a decisiones que se han tomado en la propia empresa y sus razones, que me servían como ejemplo.

      Conociendo mi desmemoria, este es un contenido que me daba un poco de rabia perder, así que decidí transformarlo en un post.

      Diagrama de una posible arquitectura dibujada en una pizarra

      ¿Qué es Arquitectura de Software?

      Explicaciones de terceros de las que caben en un tuit:

      • Según la entrada en la Wikipedia: “La arquitectura de software se refiere a las estructuras fundamentales de un sistema de software y la disciplina de crear tales estructuras y sistemas”
      • Según Michael Keeling en Design It: “La arquitectura de software de un sistema es el conjunto de decisiones significativas sobre cómo se organiza el software para promover los atributos de calidad y otras propiedades”
      • Según Ralph Johnson: “La arquitectura se trata de las cosas importantes. Sea lo que sea”

      Por intentar aterrizarlo un poco más, son las decisiones que afectan a:

      • Cómo se diseña el software y su calidad interna.
      • Cómo encaja el software con los objetivos de negocio.
      • Cuándo y cómo se entrega el software.
      • Cuál es el impacto organizativo.

      No es una torre de marfil

      Que las decisiones de arquitectura se tomen exclusivamente por personas que no están en la realidad del desarrollo diario es un anti-patrón. Se corre el peligro de desconexión con la realidad y/o con las personas que lo llevan a cabo.

      Esto es porque la arquitectura de software es una actividad de grupo. Hay personas que aportan diferentes puntos de vista: conocimiento del dominio, visión del impacto organizacional, conocimiento de diferentes tecnologías disponibles, habilidades de diseño de software, consciencia sobre el legacy existente…

      Además debemos tener en cuenta que las decisiones del día a día pueden impactar en la evolución de la arquitectura. Algunos casos pueden ser: introducir una nueva librería que nos abra nuevas posibilidades, modificar parte del software para facilitar su delivery, refactorizar (o reescribir) una parte del sistema para mejorar su testeabilidad, introducir una convención de equipo respecto a la organización de directorios y ficheros…

      La arquitectura no es estática, se itera y evoluciona

      Antaño con el enfoque waterfall, el trabajo de arquitectura se hacía antes de arrancar el desarrollo de un sistema de software. Normalmente tras una fase de análisis y toma de requisitos en las que se definía hasta el aburrimiento los últimos detalles de la funcionalidad a construir. Así que con ese enfoque había que pensarlo muy fuerte todo, para no dejarse ningún punto ciego y que la gente que desarrollaba “solo tuviera que centrarse en ejecutar” como code monkeys. Y… luego pasaba lo que pasaba.

      Por suerte, se va percibiendo que en el sector nos vamos adaptando poco a poco a enfoques más evolutivos en cuanto a la definición y construcción de lo que debe hacer un sistema de software. De forma paralela va ocurriendo lo mismo con la arquitectura de software.

      No es lo mismo arrancar un MVP para validar/invalidar rápido una hipótesis, que estar buscando generar volumen tras ver que hay encaje en el mercado y ya hay clientes, o tener que soportar el crecimiento del negocio teniendo en cuenta el legacy, etc. Los problemas van cambiando con el tiempo, y hay que ir evolucionando las soluciones de arquitectura.

      No perder de vista la Big Picture

      Los sistemas de software son sistemas socio-técnicos, no viven aislados del resto del mundo, así que debemos introducir una visión más de alto nivel y tratar que el trabajo del desarrollo del día a día esté alineado con esa visión. Para esto debemos tener en cuenta a las personas, el negocio y la tecnología.

      Respecto a las personas de la propia organización: ¿Qué habilidades tenemos dentro de la organización? ¿Necesitamos fichar personas con habilidades que no tenemos? ¿Cómo y qué tipos de equipos tenemos? ¿Cómo interactúan entre ellos? ¿Deberíamos cambiar su estructura?

      Cuestiones relacionadas con el negocio y los procesos: ¿Nos dirigimos a un mercado concreto? ¿Queremos expandir a más mercados a corto/medio plazo? ¿Tenemos que adaptarnos a un cambio en el negocio o del mercado? ¿Queremos automatizar un proceso existente? ¿Queremos cambiarlo para ser más eficientes?

      Factores puramente técnicos: ¿Tenemos ya soluciones que conocemos en la compañía? ¿Creemos que podemos sacarle ventaja competitiva al uso de una nueva herramienta o tecnología? ¿Un proveedor nos avisa que va discontinuar una herramienta que usamos o su nuevo pricing lo hace inasumible?

      Algo que nos puede resultar útil para cualquier tema relacionado con arquitectura de software, pero especialmente para no perder de vista este alto nivel, es tener en cuenta la Ley de Conway y enfrentarla con nuestro contexto.

      Definir el problema desde una perspectiva de ingeniería

      Para cumplir los objetivos del negocio, además de hacer el trabajo de producto para ir descubriendo, definiendo e implementando la funcionalidades de un software; debemos tener razonablemente claros cuáles son los atributos de calidad que esperamos que tenga nuestro software para que nos facilite cumplirlos, o como mínimo que no entorpezca alcanzarlos.

      Estos atributos de calidad se venían conociendo más como requisitos no funcionales. Que es algo que suena ya a viejuno, ya que no son requisitos que usuarios o stakeholders suelan pedir, sino atributos que como equipo se han detectado como facilitadores para la consecución de los objetivos. Algunos atributos de calidad serían escalabilidad, seguridad, operabilidad, testeabilidad, rendimiento, disponibilidad…

      Debemos tener en cuenta que tal como construimos software seguimos aprendiendo tanto acerca del negocio como de lo apropiada que puede ser o no nuestra arquitectura, así que debemos dar seguimiento a las restricciones del diseño y a las funcionalidades que pueden impactar en la arquitectura. ¿Nuestra arquitectura dificulta o limita añadir nuevas funcionalidades? ¿Tenemos que evolucionar el diseño manteniendo compatibilidad con otro software existente? ¿Necesitamos pivotar el producto porque han cambiado los objetivos de negocio?

      También debemos tener cuidado porque a veces pecamos de hablar de estos atributos de calidad en genérico, cuando lo normal es que no todos los componentes de un sistema deban tener el mismo nivel de calidad en un atributo. Una práctica que podemos utilizar son los Architecturally Significant Requirements para ayudarnos a discutir y definir los diferentes niveles de atributos de calidad que necesitamos.

      Dividir el sistema y asignar responsabilidades

      Debemos ser estratégicos en cómo dividir un sistema en distintos componentes dependiendo de nuestro contexto. Normalmente la forma de dividirlo estará relacionada con cuestiones técnicas o cuestiones organizativas.

      Las razones técnicas para dividir un sistema están relacionadas con la identificación de funcionalidades que requieren atributos de calidad muy diferentes (escalabilidad, rendimiento, testeabilidad…). Por ejemplo, no son las mismas necesidades las de guardar datos personales de menores, donde legalmente debemos tener en cuenta muchas cuestiones de seguridad y auditoría; con tener que identificar anomalías en fotos utilizando computer vision, donde requerimos uso intensivo de GPU.

      Las razones de divisiones organizativas suelen tener que ver con que existan distintos objetivos del negocio que repetidamente entran en conflicto, además de que haya muchas personas involucradas en una base de código y se haga inmanejable. No es lo mismo tener un equipo de 3 personas en una pequeña startup que una compañía mucho más establecida con 3000 personas y múltiples equipos.

      Una vez hechas esas divisiones no debemos olvidarnos de que se deberá delimitar las responsabilidades y definir cómo van a interactuar los distintos componentes del sistema. Prácticas interesantes para ambas cosas pueden ser Context Map, y entrando más en detalle se puede utilizar una aproximación API-first para especificar la forma de interactuar de los componentes.

      Decidir Trade-Offs sobre los atributos de calidad y la división del sistema

      Tenemos que ser conscientes de que no existe una división del sistema perfecto, así que debemos asumir compromisos intermedios, para ello debemos trabajar con los stakeholders. Podemos tener limitaciones presupuestarias, de conocimiento en la compañía, etc.

      Suena bien lo de tener un nivel de servicio por encima del 99% durante un año, pero tal vez el negocio no lo necesite si el sobrecoste conlleva comprar o alquilar el doble de servidores para tener alta disponibilidad. Tal vez haya partes legacy del sistema que no podemos re-escribir por cuestiones de presupuesto y tiempo. Puede que haya alguna tecnología en el mercado que nos resulte prometedora, pero para la que todavía no haya un soporte que nos parezca fiable. Quizás queramos validar o invalidar una hipótesis rápidamente frente a que soporte mucha carga en caso de que acertemos.

      Es posible que al decidir los trade-offs nos podamos equivocar, o incluso aunque a corto plazo veamos que resultan acertados con el paso del tiempo puede que no lo resulten tanto. Así que terminará emergiendo deuda técnica relacionada.

      Prácticas útiles en este caso pueden ser registrar ADRs o registrar los caminos no tomados.

      Gestionar la Deuda Técnica

      Es inevitable que lleguen momentos en los que los objetivos del negocio y las decisiones técnicas tomadas en un sistema de software no encajen, hay diferencias entre el diseño actual del sistema y el ideal hacia el que se quiere ir. Esas diferencias son la deuda técnica que tenemos adquirida.

      Puede haber más o menos, la podemos haber asumido conscientemente o inconscientemente, pero al final siempre va a surgir deuda técnica. Así que en cuanto la identifiquemos debemos darle visibilidad y analizar cuándo es el momento de pagarla, ya que tenemos que equilibrar la capacidad de entrega de valor actual con la futura.

      Hay tipos de deuda que pueden ser baratas de pagar y tener alto retorno en forma de refactors progresivos de forma continuada. Pero en las situaciones que requieren de re-escrituras que impactan en la división del sistema o la interacción de los distintos componentes, debemos implicar a los stakeholders para alinearnos y decidir cuándo y cómo vamos a pagar esa deuda.

      Resumiendo

      • La arquitectura no son los diagramas hechos por un comité de sabios, se refleja en lo que está en desarrollo y que finalmente termina en producción.
      • Debemos tener la mentalidad de que la arquitectura de software es evolutiva y actuar en consecuencia.
      • A veces el día a día nos pierde. Nos ayudará el tener momentos en los que levantemos la vista más a largo plazo y veamos si seguimos alineados con la visión y necesidades del negocio.
      • Definamos los atributos de calidad, evitando caer en ser falsamente específicos.
      • Pensemos en una división del sistema de una forma estratégica, analicemos el impacto de las dependencias entre los distintos componentes.
      • No vivimos en un mundo ideal, tengamos en cuenta las limitaciones de nuestro contexto y elijamos a qué renunciamos.
      • No nos obsesionemos con no tener deuda técnica, es inevitable. Pero no la perdamos de vista, si no la tenemos controlada puede poner en peligro la sostenibilidad del sistema o incluso el éxito del negocio.

      Agradecimientos a Alberto Gualis, Vanessa Rubio, Rubén Salado y Javi Rubio por su tiempo revisando y sugiriendo mejoras para este post.

      Referencias:

      » Leer más, comentarios, etc...

      Picando Código

      [Libro] The Last Man in Europe – Dennis Glover

      noviembre 23, 2022 10:31

      1984 de George Orwell es uno de mis libros favoritos. No recuerdo cuándo aprendí sobre The Last Man in Europe de Dennis Glover, pero estuvo en mi lista de libros para leer por al menos un par de años. Hace poco me lo encontré en una librería de Edimburgo y decidí adquirirlo. Es una novela, ficción, pero basado en una admirable investigación y pasión por George Orwell y su obra. Tras haberlo terminado de leer, el comentario que adorna la tapa es acertado en mi opinión: Escrito en el estilo que Orwell hubiera usado si hubiera decidido escribir una novela sobre su propia vida.

      Dennis Glover - The Last Man in Europe

      1947. En una casa de campo húmeda y venida a menos en la Isla de Jura, George Orwell se está embarcando en su mayor obra: Mil Novecientos Ochenta y Cuatro. Cuarenta y cuatro años de edad y sufriendo de una tuberculosis que eventualmente tomaría su vida, este libro es su legado – la cuminación de una carrera peleando para preservar las libertades que las guerras y los levantamientos del siglo veinte han amenazado. Completar el libro es una tarea urgente – una carrera contra su muerte.

      Dennis Glover explora la creación de la última obra de Orwell, la cual definió el siglo veinte para millones de lectores en el mundo. Simultáneamente un drama cautivante, una excavación literaria única y un inquebrantable retrato de un amado escritor, El Último Hombre en Europa cambiará la forma en que entiendes Mil Novecientos Ochenta y Cuatro y a George Orwell mismo.

      Si bien el autor inventó o exageró algunas cosas para escribir una novela, está basado en conocimiento real sobre Eric Arthur Blair, mejor conocido por su seudónimo George Orwell. Así que si bien no es una biografía, sí aprendemos muchísimo de la persona que fue y sus experiencias. Aparte de seguramente haberse leído las obras completas de Orwell, Dennis Glover acudió a su correspondencia (algunas disponibles online, en un libro que las compila, y originales en algunas bibliotecas). En el libro aparecen también H.G. Wells -a quien Orwell admiraba- y Aldous Huxley -quien fuera su profesor de Francés-. Hay una investigación admirable sobre la vida y obra de George Orwell.

      Me fascinó el libro, aparte de ser sumamente entretenido, me dió cosas en que pensar y momentos en los que frenaba al terminar un capítulo para buscar más información. Terminaba leyendo y aprendiendo más sobre Orwell. También saqué muchas notas (costumbre que arranqué en uno de los primeros 10 libros que leí este año) de partes que me resultaban particularmente interesantes o me gustaban. En esta ocasión, al ver lo horrible que quedaban los apuntes escritos a mano en la cama mientras leía, empecé a sacar fotos con mi teléfono y después pasarlos en limpio a mi cuaderno de apuntes de libros. Sinceramente la caligrafía no mejoró mucho…

      Hay mucho material de Orwell que vale la pena leer además de sus novelas. Uno de los ensayos que leí durante la lectura de The Last Man In Europe, fue «The Moon Under Water», donde Orwell describe su pub ideal. Llegué a él intentando averiguar si realmente la cerveza Stout tirada era su preferida y lo confirmé, ¡algo que tengo en común con George Orwell! Hay mucho más, y el autor comenta de una colección «The Complete Works of George Orwell» como referencia.

      Además de su obra, Orwell fue una persona admirable. Al viajar a España en plena Guerra Civil para escribir sobre la guerra, terminó uniéndose al POUM para luchar contra los fascistas, «la única opción concebible». Tengo pendiente leer su libro «Homenaje a Cataluña», donde relata estas experiencias. Otro de los tantos puntos interesantes de este libro es que en varias partes habla sobre lo que significa ser un escritor. Transmite varios sentimientos y obstáculos con los que se encuentra un escritor, que deben estar basados en gran parte en Orwell, pero de repente hay algo de Dennis Glover ahí también.

      La condición en la que escribió 1984 en plena tuberculosis se transmite de forma opresiva y tensa, condición que evidentemente influenció la escritura de 1984. Y no pude evitar ponerme triste llegando al final, por más que supiera de antemano «cómo termina la historia». ¿Cómo hubiera cambiado 1984 si Orwell no hubiera estado corriendo contra el tiempo mientras lo escribió? ¿Qué otro libro le quedaba por escribir después de 1984? Nunca lo sabremos.

      Me encantó, me enganchó, me resultó muy fácil de leer y fue de esos libros en los que pensaba durante el día llegar al momento libre en que pudiera seguir leyendo. Queda agregado a mi biblioteca para ser releído seguramente en el futuro. Lo recomiendo mucho si han leído 1984 o cualquier otra cosa de George Orwell y les gustó. Pero particularmente está bastante relacionado a 1984. Ya he leído 1984 más de una vez, pero después de leer The Last Man In Europe, lo quiero leer de vuelta. Le suma todavía más significado y capas a 1984, sé que lo voy a disfrutar todavía más la próxima vez que lo lea.

      Dennis Glover tiene publicados algunos libros más., además de haber escrito la introducción y algunas notas en una nueva edición de 1984. Agregué a mi lista Factory 19, una novela que tiene la pinta de ser algo parecido a 1984, pero criticando las corporaciones que dominan el mercado por internet. El estilo del escritor me gustó, y su respeto y admiración por Orwell es evidente, así que tengo muchas ganas de leer Factory 19.

      2 + 2 =

      El post [Libro] The Last Man in Europe – Dennis Glover fue publicado originalmente en Picando Código.

      » Leer más, comentarios, etc...

      Variable not found

      Añadir al routing de Blazor páginas definidas en otros proyectos o ensamblados

      noviembre 22, 2022 07:05

      Blazor

      Hoy va un post cortito, pero que puede venir bien a alguien que esté intentando "trocear" su aplicación en distintos proyectos y se haya encontrado con este problema.

      Echemos un vistazo al contenido típico del archivo App.razor de una aplicación Blazor (Server o WebAssembly, da lo mismo):

      <Router AppAssembly="@typeof(Program).Assembly">
      ...
      </Router>

      Resulta que el componente Router escanea durante su inicialización el ensamblado que hayamos indicado en su atributo AppAssembly, por defecto el ensamblado actual, en busca de componentes que:

      • Hayan definido sus rutas mediante directivas @page de Razor,
      • o bien, que tengan su ruta especificada con atributos [Route]

      En cualquiera de los casos, los componentes localizados son añadidos a la tabla de rutas y, por tanto, será posible navegar hacia ellos modificando la ruta del navegador, ya sea programáticamente (usando el servicio NavigationManager) o bien mediante la pulsación de enlaces o modificación directa en la barra de direcciones.

      Hasta aquí, todo correcto. El problema viene cuando las páginas que queremos añadir a la tabla de rutas no están en el ensamblado actual, o se encuentran repartidas en varios ensamblados distintos, por ejemplo en:

      • Otros proyectos de la solución
      • Ensamblados externos, referenciados directamente
      • Paquetes NuGet instalados en el proyecto

      ¿La solución?

      Pues es bien sencilla, y consiste únicamente en utilizar el atributo AdditionalAssemblies para indicar un array de ensamblados adicionales donde debe realizarse la búsqueda, por ejemplo así:

      <Router AppAssembly="@typeof(Program).Assembly"
      AdditionalAssemblies="@additionalAssemblies">
      ...
      </Router>

      @code {
      static Assembly[] additionalAssemblies = new[]
      {
      typeof(ExternalType1).Assembly,
      typeof(ExternalType2).Assembly,
      };
      }

      En este caso, además del ensamblado actual (donde se encuentra definida la clase Program), se añadirían al routing las páginas presentes en los ensamblados donde hayan sido definidos los tipos ExternalType1 y ExternalType2.

      Espero que os sea de utilidad :)

      Publicado en Variable not found.

      » Leer más, comentarios, etc...

      Variable not found

      Enlaces interesantes 505

      noviembre 21, 2022 07:05

      Enlaces interesantes

      Creo que nunca me he topado en persona con este código de error, así que es una buena ocasión para conocer un poco sobre él.

      El código HTTP 505 (HTTP Version Not Supported) es retornado por un servidor cuando rechaza o no soporta la versión major de HTTP utilizada en la petición. En este caso, se recomienda que el servidor incluya en la respuesta la descripción del motivo por el que no soporta la versión utilizada, y qué protocolos debería usar en su lugar.

      Ahí van los enlaces recopilados durante la semana pasada. Espero que os resulten interesantes. :-)

      Por si te lo perdiste...

      .NET Core / .NET

      ASP.NET Core / ASP.NET / Blazor

      Azure / Cloud

      Conceptos / Patrones / Buenas prácticas

      Data

      Web / HTML / CSS / Javascript

      Visual Studio / Complementos / Herramientas

      .NET MAUI / Xamarin

      Otros

      Publicado en Variable not found.

      » Leer más, comentarios, etc...

      Picando Código

      Actualizaciones en Picando Código

      noviembre 18, 2022 09:08

      Estuve trabajando un poco en el blog, y además de varios cambios mínimos (buena excusa para ocultar que no me los acuerdo), hay un par que me pareció relevante mencionar.

      En estos días noté que la «suscripción a recibir los posts por correo» había dejado de funcionar. Esto es porque el sistema usaba Feedburner, una compañía que gestionaba feeds RSS y nos daba estadísticas y un montón de funcionalidades más. Google los compró en 2007, y desde entonces no le prestaba demasiada atención. En 2021 introdujeron algunos cambios, eliminando un montón de funcionalidad entre las que se encontraba la suscripción por correo electrónico. En ese mismo anuncio explican cómo descargar la lista de correos suscriptas, pero lo vi recién cuando noté que no funcionaba. Así que lamentablemente puede que haya alguna persona que leía el blog en su correo electrónico a través de los correos de Feedburner, y piensa que el blog murió en 2021 🤷

      Creo que no quedan enlaces al feed de Feedburner en este blog, intenté cambiarlos todos a los enlaces de los feeds directo del blog (RSS y Atom). Pueden encontrar los enlaces al final de cada post y en el menú de navegación superior. Pero por si a alguien más le resulta conveniente recibir los posts del blog por correo electrónico, instalé un plugin de WordPress para eso. En la barra de la derecha del blog, se encuentra un formulario para ingresar el correo electrónico y suscribirse para recibir un email cada vez que se publica un post. Obviamente no voy a mandar spam ni usar sus correos electrónicos para nada, simplemente dejar el sistema automático funcionando para que envíe correos a las personas que así lo desean. La desuscripción es muy sencilla también, y cada correo incluye un enlace para desuscribirse.

      Otra cosa bastante mínima que agregué, pero que espero resulte divertida, son las reacciones a los posts y comentarios:

      Sí, tengo que cambiarle los estilos para que se note más que hay un botón ahí, pero eso vendrá más adelante 😬

      Bueno, si llegaste leyendo hasta acá, ¡gracias! Espero algún comentario (¡o reacción!)

      El post Actualizaciones en Picando Código fue publicado originalmente en Picando Código.

      » Leer más, comentarios, etc...

      Picando Código

      Donando a proyectos de software libre: Thunderbird

      noviembre 18, 2022 05:30

      Roc - La mascota de ThunderbirdHoy doné a Thunderbird, mi cliente de correo favorito. El desarrollo de Thunderbird es financiado por menos del 1% de los usuarios que lo usan. Hace unos años, Thunderbrid estuvo a punto de desaparecer, pero las donaciones de usuarios lo salvaron.

      Este año se ha movido bastante la cosa por el mundillo de Thunderbird. Por suerte la situación financiera les permitió contratar gente y sacar adelante varios nuevos desarrollos.

      Se empezó a trabajar en una versión Android de Thunderbird. Para ello, unieron esfuerzos con la aplicación móvil de código abierto que vengo usando en mi teléfono Android: K9. Por años ambos proyectos buscaron una forma de colaborar. En 2022 se concretó la colaboración, cuando K9 se unió a Thunderbird. La aplicación se va a transformar en Thunderbird para Android cuando se alcance cierto nivel de desarrollo. El producto final, al igual que Thunderbird, será de código abierto, respetará a los usuarios, y tendrá lo necesario para usuarios avanzados que busquen personalización y muchas funcionalidades.

      Una de las características que más me interesan de Thunderbird para Android es la sincronización entre versión móvil y de escritorio. Si bien al usar IMAP en el servidor de correo, los correos electrónicos son lo mismo en un lado y el otro, espero podamos compartir personalizaciones y configuraciones. Pero ya voy a estar agradecido de poder usar la misma aplicación en móvil y escritorio y las mejoras que traiga K9 bajo el nombre Thunderbird.

      Por su parte Thunderbird escritorio sigue mejorando y recientemente mejoró bastante su interfaz gráfica. Pero lo importante se viene en 2023 cuando Thunderbird se «reinvente» con la publicación que vienen llamando «Supernova». Prometen una interfaz modernizada y características nuevas como Firefox Sync (que imagino será la manera de sincronizar con Android). Ya mostraron en su blog el cambio a la interfaz de usuario de su calendario.

      Además de ser una aplicación de correos muy buena, Thunderbird respeta la privacidad de sus usuarios, no es manejada por una corporación, no muestra anuncios ni vende nuestra información. El software es libre, gratuito y puede seguir gracias a aportes de los usuarios. Uso Thunderbird prácticamente todos los días, así que donar un poco de dinero al proyecto -mientras se pueda- es lo mínimo que puedo hacer.

      Si te interesa donar, se puede hacer en este enlace. Puedes aprender más en el sitio oficial, o seguirlo en Twitter y Mastodon.

      El post Donando a proyectos de software libre: Thunderbird fue publicado originalmente en Picando Código.

      » Leer más, comentarios, etc...

      IOKode

      ¿Por qué este blog no se aloja en VADAVO?

      noviembre 18, 2022 09:58

      Como muchos sabéis, actualmente trabajo como líder del departamento de desarrollo de software en VADAVO, una empresa que ofrece tanto servicios de telecomunicaciones (líneas telefónicas y conexión a Internet) así como servicios de alojamiento de servicios en Internet (hosting web, servidores cloud y nombres de dominio).

      Sin embargo, si trazáis tanto el alojamiento de este blog como el nombre del dominio, podréis comprobar que se aloja en Digital Ocean y el dominio está registrado en Don Dominio.

      ¿Qué sentido tiene que trabajando en una empresa que, uno de sus principales productos sirve para dar vida a sitios web como este, contrate los servicios de la competencia para este blog?

      Un poco de contexto

      Llevo en el mundo de Internet desde que soy un niño, y con no mucha edad, unos 12 o 13 años utilizaba servicios de alojamiento web gratuitos como los extintos iEspana, Zobyhost y Nixiweb y, más tarde, Hostinger. Es un sector del que llevo utilizando sus servicios prácticamente toda mi vida, y que considero muy valioso. Es por ello que decidí trabajar en una empresa de este sector.

      Lo que la competencia puede enseñarnos

      Aunque dentro de VADAVO, no es mi labor encargarme de la experiencia al usuario de nuestros servicios, sí que es cierto que me interesa que esta sea lo mejor posible para nuestros clientes, al fin y al cabo, de forma indirecta, son ellos quienes pagan las nóminas de todo mi equipo y, perder un cliente, es una pérdida indirecta para mí.

      Para ofrecer el mejor servicio posible, una buena idea es conocer que hace nuestra competencia a ojos de los clientes, en especial aquellas empresas del sector que son más grandes que nosotros y así tomar las mejores ideas.

      Tal vez podríamos contratar algunos servicios básicos para echar un ojo a los paneles de control que ven los clientes y poco más, pero entonces no profundizas y no te encuentras con posibles problemas que puedes ver al llegar más a fondo.

      Para poder analizar correctamente desde el punto de vista del cliente a la competencia, tengo que ser un cliente real, con un proyecto real, y por ello este blog está alojado en la competencia.

      » Leer más, comentarios, etc...

      Blog Bitix

      Programas para crear máquinas virtuales y virtualizar sistemas

      noviembre 17, 2022 06:30

      La virtualización es la herramienta con la que probar un sistema operativo sin tener que utilizar una máquina física. Con la cantidad de núcleos de los sistemas actuales y la cantidad de memoria RAM que tienen, la virtualización es una opción disponible para los usuarios. Una alternativa a la virtualización son los contenedores pero estos son más para servicios sin interfaz gráfica. Hay aplicaciones que permiten tener una máquina virtual de Windows en un sistema operativo GNU/Linux o probar una distribución de GNU/Linux en un sistema Windows antes de migrar definitivamente a GNU/Linux.

      Linux

      La virtualización permite crear un sistema que está instalado como software dentro de otro sistema físico. La virtualización es muy útil para probar sistemas operativos sin necesidad de instalarlo en la máquina física ni riesgo de hacer pruebas que rompan el sistema instalado, por ejemplo para probar la distribución Ubuntu que es una de las recomendadas como primera distribución para los nuevos usuarios de GNU/Linux.

      La virtualización permite sistemas aislados sin necesitar una máquina física, es posible virtualizar un sistema Windows en GNU/Linux, virtualizar un sistema GNU/Linux en un sistema Windows o otros sistema GNU/Linux en un sistema GNU/Linux. Dadas las grandes cantidades de RAM que tienen las computadoras actuales incluso las de los usuarios es posible instalar un sistema virtual dentro de un sistema físico.

      La virtualización requiere de un sistema anfitrión con suficiente potencia de cómputo y memoria RAM, pero incluso los procesadores de consumo que actualmente ya tienen más de cuatro núcleos y muchos más de 16 GB de memoria RAM son suficientemente capaces de virtualizar un sistema, con más núcleos y memoria RAM los sistemas virtualizados pueden ser más numerosos.

      Como el sistema virtualizado es algo lógico en el momento de su creación es posible definir sus características como núcleos de CPU que tiene, cantidad de memoria RAM y cantidad de almacenamiento. Es posible crear tantas máquinas como se deseen y tener tantas arrancadas al mismo tiempo como el sistema anfitrión tenga capacidad de ejecutar con buen rendimiento.

      Un sistema virtualizado tiene una pequeña pérdida de rendimiento respecto a un sistema físico, los procesadores actuales incluyen soporte e instrucciones para que la pérdida del rendimiento sea mínima. Algunas características en la virtualización como la aceleración gráfica tiene mayores penalizaciones de rendimiento o directamente no es posible. Por ejemplo, jugar a juegos en un sistema virtualizado no ofrece buen rendimiento más teniendo en cuenta que los juegos son de los programas más exigentes en capacidad de cómputo de tarjeta gráfica y de procesador.

      Para virtualizar un sistemas es necesario un programa, VirtualBox está disponible para Windows, macOS y GNU/Linux y en GNU/Linux el sistema de virtualización nativo es KVM, el sistema de virtualización nativo es de Windows es Hyper-V.

      La virtualización requiere virtualizar un sistema operativo completo que impone una pequeña penalización de rendimiento y unos tiempos de arranque elevados. Para solventar estos problemas los contenedores y una herramienta como Docker permite ofrecer servicios con menor penalización de rendimiento y tiempos de arranque mínimos, los contenedores no virtualizan un sistema operativo completo pero son procesos aislado del resto del sistema.

      Contenido del artículo

      VirtualBox

      Dado que VirtualBox está disponible para los tres sistemas operativos que usan la mayoría de usuarios y es gratuito es una opción muy utilizada como programa para virtualizar. Tiene una interfaz gráfica de usuario con la que configurar muchas de las opciones de los sistemas operativos como cantidad de RAM y capacidad de almacenamiento.

      VirtualBox VirtualBox VirtualBox

      VirtualBox

      La interfaz gráfica permite crear máquinas virtuales manualmente. Un programa Vagrant permite automatizar la creación y aprovisionamiento de las máquinas virtuales, en base a una descripción de la máquina a crear con un archivo de configuración.

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      
      # -*- mode: ruby -*-
      # vi: set ft=ruby :
      
      Vagrant.configure("2") do |config|
      
          config.vm.define "ubuntu" do |instance|
            instance.vm.box = "ubuntu/jammy64"
            instance.vm.network "private_network", ip: "192.168.56.10"
            instance.vm.synced_folder ".", "/home/vagrant/ConsulServiceMesh/"
      
            instance.vm.provider :virtualbox do |vb|
              vb.name = "Ubuntu 22.04 (Vagrant)"
              vb.memory = "4096"
      
              instance.vm.provision "shell", inline: $consul_role_script
            end
          end
      
          $consul_role_script = <<-SCRIPT
            echo "Updating..."
            sudo apt-get update
            sudo apt-get upgrade
      
            echo "Installing Docker..."
            curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker.gpg
            echo \
                "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
                $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
            sudo apt-get update && sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin && sudo systemctl enable docker.service
            sudo usermod -aG docker vagrant
          SCRIPT
        end
        
      Vagrantfile

      Cajas de GNOME

      La aplicación integrada de Cajas de el entorno de escritorio GNOME permite crear máquinas virtuales usando la virtualización nativa de GNU/Linux. Ofrece una interfaz gráfica no tan completa en opciones como VirtualBox pero si las opciones mínimas como memoria RAM, CPU, EFI, almacenamiento y aceleración gráfica. En caso de no usar GNOME o se desean más opciones de configuración virt-manager ofrece una mucha más completa interfaz y opciones de configuración para KVM.

      Cajas de GNOME Cajas de GNOME Cajas de GNOME

      Cajas de GNOME

      Con el comando virt-install es posible también automatizar la creación de máquinas virtuales. El siguiente comando permite configurar la ubicación del sistema de almacenamiento de la máquina virtual además de la RAM y definir el sistema como un sistema con arranque EFI.

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      
      sudo pacman -S virt-install dnsmasq dmidecode
      sudo usermod -a -G libvirtd picodotdev
      
      mkdir -p /etc/qemu
      sudo cat << EOF > /etc/qemu/bridge.conf
      allow virbr0
      EOF
      
      sudo systemctl start libvirtd.service
      
      
      virt-install-bridge.sh
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      
      #!/usr/bin/env bash
      set -eu
      
      DISK_DIRECTORY="/run/media/picodotdev/Samsung microSD/KVM VMs"
      ISO_DIRECTORY="/run/media/picodotdev/Samsung microSD/Iso"
      
      virt-install \
          --connect=qemu:///session \
          --name archlinux-alis \
          --os-variant archlinux \
          --vcpu 2 \
          --ram 4096 \
          --boot uefi \
          --disk path="$DISK_DIRECTORY/archlinux-alis.qcow2,format=qcow2,size=40,sparse=yes" \
          --cdrom "$ISO_DIRECTORY/archlinux-2022.11.01-x86_64.iso" \
          --disk cloud-init/alis-cloud-init.iso,device=cdrom,bus=sata \
          --network bridge=virbr0 \
          --noautoconsole
      
      virt-install.sh

      Otras opciones

      Las anteriores son opciones de software libre sin coste de licencia, VMware Workstation Player y Parallels son opciones comerciales para Windows y macOS.

      » Leer más, comentarios, etc...

      Picando Código

      [Libro] The Modern Bestiary – Joanna Bagniewska

      noviembre 16, 2022 11:30

      Este fue uno de los mejores libros que leí en 2022. ¡Viene siendo un buen año para mí en material de lectura!

      La historia de cómo llegué a él fue la siguiente: Un domingo de noviembre, después de haber terminado de leer un libro que me costó terminar (de esos que están «bien» pero no enganchan [y no, ¡no fue éste!]), salí a caminar por Edimburgo. Ya tenía la idea de entrar a una de las librerías a ver qué encontraba. Necesitaba algo entretenido, algo que me enganchara de nuevo a la lectura. Recorriendo una de estas librerías, me topé con él de casualidad al huir de un grupo de jóvenes ruidosos y molestos. Me llamó la atención la tapa, los dibujos de Jennifer N. R. Smith son muy buenos y llaman la atención. Al leer el resumen me convenció:

      De lo familiar a lo improbable, lo asqueroso a lo entrañable. El Bestiario Moderno es un compendio de criaturas curiosas. Incluye tanto animales que han llegado a los titulares y esos de los que probablemente nunca hayas escuchado, como gimnofiones que comen piel, esponjas arpa, o gusanos zombie – también conocidos como flores moco come-huesos.

      Organizado por elementos (Tierra, Agua, Aire), El Bestiario Moderno contiene especies bien conocidas contadas desde ángulos nuevos e inesperados (ratas que manejan automóviles; peces que se comunican tirándose pedos), así como criaturas más extrañas y menos conocidas, incluyendo ratones carnívoros que aullan a la luna, sepíidos travestis, y antequinos – pequeños marsupiales que literalmente se aparean hasta la muerte. Por último, están los «aliens en la tierra» – los increíbles, los surreales, los mágicos – como los tardígrados, piojos come-lenguas y medusas inmortales, creaturas tan asombrosas que hacen que los unicornios parezcan relativamente vulgares.

      Escrito por una zoóloga con un talento para la narración, esta es una celebración fascinante del reino animal.

      The Modern Bestiary - Joanna Bagniewska

      La descripción me atrapó enseguida, y el libro era exactamente lo que esperaba. Está lleno de hechos divertidos y asombrosos sobre distintos animales.

      La introducción es bastante interesante, entre otras cosas habla un poco sobre la historia de los «Bestiarios». Antiguamente eran libros coleccionando información sobre bestias (reales y de las otras) y algunas historias con moralejas, mucho basado en mitos y creencias religiosas. El arte estaba basado muchas veces en la imaginación de los artistas o descripciones de boca en boca, tanto para bestias reales como mitológicas. Este libro es lo opuesto, basado en investigación científica e ilustraciones precisas. Los animales son usados para mostrar conceptos biológicos en vez de intentar forzar lecciones de moral.

      La presentacion del libro es muy linda. En la cara interna de la tapa hay una ilustración excelente del árbol de la vida destacando los grupos taxonómicos principales. A pesar de la ilustracion de cada animal, en varios me dió curiosidad por buscar fotos para verlos a color, o videos para ver más de esos comportamientos increíbles que describe.

      La autora escribe de una manera súper entretenida, cada animal está acompañado de su ilustración científicamente precisa, y referencias o historias relacionadas. Es un libro de referencia y al que seguramente vuelva varias veces cuando quiera recordar algunos hechos específicos de distintos animales.

      Tengo un montón de temas de conversación nuevos en eventos sociales. ¿Sabías que los megamurciélagos además de ser adorables son vegetarianos y se dan sexo oral? O que algunos animales respiran por el culo (hecho que había aprendido leyendo Beasts Before Us de Elsa Panciroli, ¡libro que quier volver a leer!). Largué la carcajada al leer sobre cómo los arenques se comunican mediante pedos, y varias veces más. El libro está dividido en 3 partes: tierra, agua y aire. A veces viendo el dibujo pensamos «este animal está en el grupo equivocado», pero no, un animal que asumimos en un elemento, anda también en otro(s).

      Es una lástima saber que seguramente muchas de estas criaturas desaparezcan a corto plazo gracias a la destrucción del medio ambiente por parte de los humanos, y cuántas más habrán desaparecido antes de haber aprendido lo interesante que eran. Pero por lo menos podemos aprender sobre ellos por ahora.

      Con cien animales escritos y dibujados, habrán varios más que quedaron afuera. Espero que este libro tenga éxito, y sea el primer volúmen de un Bestiario Moderno. Si Joanna Bagniewska publica «The Modern Bestiary – Vol II», para mí es una lectura asegurada.

       

      El post [Libro] The Modern Bestiary – Joanna Bagniewska fue publicado originalmente en Picando Código.

      » Leer más, comentarios, etc...

      Picando Código

      Procesando Feeds RSS de TinyRSS para consumo OPML

      noviembre 15, 2022 11:00

      Ruby RSSHace un buen tiempo usaba TinyRSS, un lector RSS web alojado en mi servidor. Pero no recuerdo si fue por actualizar PHP o el código de Tiny Tiny RSS mismo, se rompió la instalación. Decidí volver a usar alguna aplicación de escritorio, por el momento estoy usando Akregator en KDE, pero vi que Thunderbird también permitía la importación de Feeds RSS.

      Como no quería ingresar los feeds a los que estaba suscripto de a uno y a mano, entré al phpMyAdmin de mi servidor, y me fijé en qué tabla se encontraban mis feeds. La tabla en cuestión es ttrss_feeds, que incluye los campos title y feed_url. Este último el único que realmente me importa para poder tener mis suscripciones. Desde phpMyAdmin mismo tenemos una funcionalidad de exportar las filas de una tabla, y podemos elegir entre varios formatos. Así que exporté los datos en formato JSON al archivo ‘feeds.json’, que en Ruby es súper fácil de procesar.

      Intentando importar en Thunderbird o Akregator, OPML era una de las opciones. Así que tenía que ver cómo se formatea un OPML y traducir el JSON a este formato. Agregué un feed de ejemplo a Thunderbird, exporté mis feeds a un archivo OPML, y ahí tenía mi ejemplo. OPML (Outline Processor Markup Language) es un formato XML para esquemas. Y no necesitamos demasiados detalles, en cada «outline» (esquema), hay que definir el título y la URL del sitio y el feed. Todo información que pude extraer del JSON exportado en phpMyAdmin. Cada elemento dentro de la data del JSON se ve algo así:

      {"id":"4", 
      "title":"OMG! Ubuntu!", 
      "feed_url":"http:\/\/feeds.feedburner.com\/d0od",
      "site_url":"https:\/\/www.omgubuntu.co.uk", "auth_login":"", 
      ... # varios campos más que son irrelevantes
      }, 
      

      Con los campos title, feed_url y site_url, tenemos lo necesario.

      Usando el OPML exportado de ejemplo, abrimos el JSON, lo procesamos, y creamos un nuevo archivo ‘feeds.opml’ con lo necesario:

      require 'json'
      require 'json'
      require 'time'
      
      file = File.read('./feeds.json') # El archivo con los feeds
      data = JSON.parse(file)[2]['data']
      
      template = []
      template << <<~OPML
      
        
          Custom Ruby OPML Export - Blogs & News Feeds
          #{Time.now.httpdate}
        
        
      OPML
      
      feeds = []
      data.each do |feed|
        title = feed['title'].gsub('&', '&')
        feed_item = <<-OPML
          
        OPML
        feeds << feed_item
      end
      template << feeds
      
      template << <<~OPML
        
      
      OPML
      File.write('feeds.opml', template.join)
      

      El resultado feeds.opml:

        
      
        
          Custom Ruby OPML Export - Blogs & News Feeds
          Fri, 11 Nov 2022 08:53:46 GMT
        
        
          
          
          
          ...
          
        
      
      

      No sólo lo tengo importado en Akregator, ahora tengo una forma de mantener mis feeds en un archivo portable para leer en cualquier dispositivo.
      Espero que a alguien más le sirva de algo 👍🏻

      El post Procesando Feeds RSS de TinyRSS para consumo OPML fue publicado originalmente en Picando Código.

      » Leer más, comentarios, etc...

      Variable not found

      Miembros de inicialización obligatoria en C#11 (required members)

      noviembre 15, 2022 07:05

      C#

      La llegada de C#11 viene acompañada de un buen número de novedades, algunas de ellas bastante jugosas, como los raw string literals o el nuevo modificador "file" que ya hemos ido viendo por aquí.

      En esta ocasión, vamos a echar el vistazo a otra de las novedades que pueden resultar de bastante utilidad en nuestro día a día: los miembros requeridos.

      C#11 ha introducido el modificador required, aplicable a campos y propiedades de clases y estructuras, para indicar que éstos deben ser obligatoriamente inicializados durante la construcción del objeto. Un ejemplo sencillo de uso sería el siguiente, donde creamos una clase con una propiedad requerida y otra que no lo es:

      public class Friend
      {
      public required string Name { get; set; }
      public string Email { get; set; }
      }

      Dada esta definición, el siguiente código fallará en compilación:

      var friend = new Friend(); // Error CS9035: Required member 'Friend.Name'
      // must be set in the object initializer or
      // attribute constructor.

      A la vista del error, lo que el compilador espera es que inicialicemos la propiedad obligatoria Name. Por tanto, si lo hacemos durante la misma construcción del objeto, el código compilará sin problemas:

      var friend = new Friend { Name = "John" };

      Un aspecto importante a tener en cuenta es que el modificador required sólo indica que la propiedad o campo deben ser inicializados, pero el valor asignado podría ser null si el tipo de datos así permite. Por ejemplo, el siguiente código sería válido:

      var peter = new Friend { Name = "Peter" };  // No compila, falta inicializar "Surname"
      var john = new Friend { Name = "John", Surname = null }; // Compila bien

      public class Friend
      {
      public required string Name { get; set; }
      public required string? Surname { get; set; }
      public string? Email { get; set; }
      }

      Visto esto, sería de esperar que un código como el siguiente compilase, pues estamos definiendo un único constructor donde se inicializa el miembro requerido, sin embargo no es así:

      var friend = new Friend("John"); // Error CS9035: Required member 'Friend.Name'
      // must be set in the object initializer or
      // attribute constructor.
      public class Friend
      {
      public required string Name { get; set; }
      public string? Email { get; set; }

      public Friend(string name)
      {
      Name = name;
      }
      }

      Cuando un tipo de datos tiene miembros required, es responsabilidad del código cliente (es decir, el que construye el objeto) inicializar todos estos miembros, como en los ejemplos que hemos visto antes. Esto difiere de la forma en que se hacía tradicionalmente, donde es el autor de la clase el responsable de realizar esta inicialización.

      Esto es también así cuando el código cliente utilice un constructor; observad por ejemplo el siguiente código:

      var peter = new Friend("peter@server.com") { Name = "Peter", Age = 34 }; // Compila Ok
      var john = new Friend("john@server.com") { Name = "John" }; // Error, falta "Age"

      public class Friend
      {
      public required string Name { get; set; }
      public required int Age { get; set; }
      public string? Email { get; set; }
      public Friend(string email)
      {
      Email = email;
      }
      }

      El establecimiento de la variable peter compila bien porque, aunque hemos llamado al constructor, hemos inicializado todos los miembros requeridos; sin embargo, la compilación fallará con la variable john porque no los hemos inicializado todos.

      Así a priori esto tiene bastante sentido, pero fijaos ahora en el siguiente ejemplo, que fallará en compilación:

      var john = new Friend("John", 34); // No compila

      public class Friend
      {
      public required string Name { get; set; }
      public required int Age { get; set; }
      public Friend(string name, int age)
      {
      Name = name;
      Age = age;
      }
      }

      En este caso, aunque el constructor está inicializando los miembros requeridos y debería ser válido, no lo es por lo que comentábamos antes: es responsabilidad del código cliente inicializar estos miembros. Para hacer que el compilador sea más flexible con esto, podemos usar el atributo [SetsRequiredMembers] sobre un constructor cuando queramos indicar explícitamente que éste inicializa todos los miembros requeridos, liberando al cliente de esta responsabilidad.

      var john = new Friend("John", 34); // Ok
      public class Friend
      {
      public required string Name { get; set; }
      public required int Age { get; set; }

      [SetsRequiredMembers]
      public Friend(string name, int age)
      {
      Name = name;
      Age = age;
      }
      }

      Es importante tener en cuenta que este atributo lo único que hace es relajar las comprobaciones del compilador, haciéndole pensar que en el interior del constructor se establecen todos los miembros requeridos, pero no se realiza ninguna comprobación de que sea así. Es decir, el siguiente código compilaría, aunque dejaría la puerta abierta a la creación de objetos con un estado inválido o inconsistente:

      var john = new Friend(); // Compila ok, el constructor se encarga de 
      // inicializar los miembros requeridos

      public class Friend
      {
      public required string Name { get; set; }
      public required int Age { get; set; }

      [SetsRequiredMembers]
      public Friend()
      {
      // Aquí no inicializamos nada!
      }
      }

      Dado que permite saltarse la restricción required de las propiedades, [SetRequiredMembers] debe utilizarse con precaución, pues podría dar lugar a instancias de objetos con valores no válidos.

      Otra forma de saltarse estas restricciones required es mediante los mecanismos de introspección de .NET (reflection) o las herramientas que tenemos disponibles en tiempo de ejecución para crear instancias al vuelo. Por ejemplo, el siguiente código muestra un par de opciones que compilarán bien, aunque darán lugar a instancias inconsistentes:

      var john = new Friend(); // No compila, falta inicializar "Name" y "Age"
      var friend1 = Activator.CreateInstance<Friend>(); // Compila Ok
      var friend2 = Assembly
      .GetExecutingAssembly()
      .CreateInstance(typeof(Friend).FullName)
      as Friend; // Compila Ok

      public class Friend
      {
      public required string Name { get; set; }
      public required int Age { get; set; }
      }

      Bueno, creo que con lo que hemos visto ya podemos hacernos una idea del funcionamiento, al menos a alto nivel, de esta nueva característica.

      Personalmente, la veo interesante a la hora de crear nuestros tipos de datos y creo que puede tener bastante aplicación, tanto a la hora de crear clases o estructuras nuevas como para añadir miembros a tipos existentes. Aunque es algo que podríamos conseguir con versiones anteriores utilizando constructores, esta fórmula es probablemente más sencilla en la mayoría de las ocasiones.

      Eso sí, me quedo con la sensación de que el uso del [SetsRequiredMembers] quizás podría haberse evitado en algunos casos utilizando un poco más de magia en tiempo de compilación, pero bueno, entiendo también que esto puede ser simplemente el inicio y en el futuro quizás pueda mejorarse un poco.

      Por si os interesa profundizar más, aquí tenéis algunos enlaces:

      Publicado en Variable not found.

      » Leer más, comentarios, etc...

      Picando Código

      Piques Emacs: Usar Tramp para abrir un archivo con su/sudo

      noviembre 14, 2022 01:30

      Cada tanto me olvido de la sintaxis exacta de este comando, así que lo publico acá. Emacs incluye TRAMP (Transparent Remote Access, Multiple Protocols), un paquete para editar archivos remotos. TRAMP nos permite también abrir archivos en nuestra computadora usando su o sudo.

      La sintaxis:

      C-x C-f /sudo::

      Al ejecutar esto, Tramp nos presenta un diálogo en el mini-buffer para ingresar password:

      Emacs TRAMP sudo

      Y de ahí podemos empezar a navegar nuestro sistema y abrir cualquier archivo bajo sudo (o su). Tramp permite conectarnos a otros protocolos como ssh o ftp. Más información sobre TRAMP.

      El post Piques Emacs: Usar Tramp para abrir un archivo con su/sudo fue publicado originalmente en Picando Código.

      » Leer más, comentarios, etc...

      Meta-Info

      ¿Que es?

      Planeta Código es un agregador de weblogs sobre programación y desarrollo en castellano. Si eres lector te permite seguirlos de modo cómodo en esta misma página o mediante el fichero de subscripción.

      rss subscripción

      Puedes utilizar las siguientes imagenes para enlazar PlanetaCodigo:
      planetacodigo

      planetacodigo

      Si tienes un weblog de programación y quieres ser añadido aquí, envíame un email solicitándolo.

      Idea: Juanjo Navarro

      Diseño: Albin