diff --git a/c-busqueda/handler.cr b/c-busqueda/handler.cr new file mode 100644 index 0000000..ee6c82b --- /dev/null +++ b/c-busqueda/handler.cr @@ -0,0 +1,216 @@ +require "http/request" +require "http/headers" +require "ishi/html" +require "json" + +class Handler + def format_buffer(buffer, canvas_name) + # Process the gnuplot output so it works in the page + # + # buffer is the Ishi output + # name is a string to replace for gnuplot_canvas so + # we can have multiple charts in a page + + html = buffer.to_s.split("\n") + html = html[html.index("")] + html = html.join("\n") + %( +
+ + + +
+ + Sorry, your browser seems not to support the HTML 5 canvas element + +
+ +
+ ) + # This ID needs to be unique in case + # we have 2 charts in the same page + html.gsub("gnuplot_canvas", canvas_name) + end + + def query(sql) + # Runs a SQL query against the Rqlite database. + # + # Returns an array of values (which need to be casted) + # Or nil if there are no results + + params = URI::Params.encode({"q": sql}) + response = HTTP::Client.get URI.new( + "http", + "10.61.0.1", + 4001, + "/db/query", + params) + + # This API only has a values key when there are actual results + results = JSON.parse(response.body)["results"][0].as_h + if results.has_key?("values") + return results["values"].as_a + end + # No result, return nil + nil + end + + def normalize_name(s) + # Remove diacritics, turn lowercase + normalized = s.unicode_normalize(:nfkd).chars + normalized.reject! { |c| + !c.ascii_letter? + }.join("").downcase + end + + def feminidad(nombre) + nombre = nombre.to_s.upcase + sql1 = %( + SELECT COALESCE(frecuencia,0) + FROM mujeres WHERE nombre='#{nombre}' + + ) + sql2 = %( + SELECT COALESCE(frecuencia,0) + FROM hombres WHERE nombre='#{nombre}' + ) + + # Yes this database is upper case + mujeres = query(sql1) + mujeres = mujeres.nil? ? 0 : mujeres[0][0].as_i + hombres = query(sql2) + hombres = hombres.nil? ? 0 : hombres[0][0].as_i + if hombres == mujeres == 0 + return 0.5 + end + return mujeres / (hombres + mujeres) + end + + def split_por_genero(nombres) + femeninos = Array(Array(String | Int32)).new + masculinos = Array(Array(String | Int32)).new + nombres.map { |n| + fem = feminidad(n[1]) + # El overlap en 0.5 es intencional! + if fem >= 0.5 + femeninos << n + end + if fem <= 0.5 + masculinos << n + end + } + { + "f": femeninos, + "m": masculinos, + } + end + + def run(request : HTTP::Request) + unless (body = request.body).nil? + query = Hash(String, String).from_json(body) + else + query = {"p": "", "g": "", a: ""} + end + + p! query + + # Sanitize input. + # Each one either a valid string or nil + prefijo = query.fetch("p", "") + genero = query.fetch("g", "") + año = query.fetch("a", "") + + if !prefijo.empty? + prefijo = normalize_name(prefijo) + else + prefijo = nil + end + + if !["f", "m"].includes?(genero) + genero = nil + end + + if año.empty? + año = nil + # TODO: check for valid integer + end + + if prefijo.nil? && año.nil? + # Global totals + sql = %( + SELECT total, nombre + FROM totales + ORDER BY total DESC + LIMIT 50 + ) + elsif prefijo.nil? && !año.nil? + # Per-year totals + sql = %( + SELECT contador, nombre + FROM nombres + WHERE + anio = '#{año}' + ORDER BY contador DESC + LIMIT 50 + ) + elsif !prefijo.nil? && año.nil? + # Filter only by prefix + sql = %( + SELECT total, nombre + FROM totales + WHERE + nombre LIKE '#{prefijo}%' + ORDER BY total DESC + LIMIT 50 + ) + else + # We have all the filters + sql = %( + SELECT contador, nombre + FROM nombres + WHERE + anio = '#{año}' AND + nombre LIKE '#{prefijo}%' + ORDER BY contador DESC + LIMIT 50 + ) + end + results = query(sql) + + if results.nil? + # This is bad 😀 + return { + body: "Que raro, no tengo *idea*!", + status_code: 200, + headers: HTTP::Headers{"Content-Type" => "text/html"}, + } + end + datos = results.map { |r| + [r[0].as_i, r[1].as_s] + } + + if genero + datos = split_por_genero(datos)[genero] + end + + datos = datos[..10] + + buffer = IO::Memory.new + Ishi.new(buffer) do + x = (1..10).to_a + y = (1..10).to_a + canvas_size(800, 300) + plot(x,y, style: :boxes, fs: 0.25) + .boxwidth(0.5) + .ylabel("Popularidad") + .xlabel("Nombre") + end + + { + body: format_buffer(buffer, "busqueda"), + status_code: 200, + headers: HTTP::Headers{"Content-Type" => "text/html"}, + } + end +end \ No newline at end of file diff --git a/c-busqueda/shard.yml b/c-busqueda/shard.yml new file mode 100644 index 0000000..87fc635 --- /dev/null +++ b/c-busqueda/shard.yml @@ -0,0 +1,6 @@ +name: busqueda +version: 0.1.0 + +dependencies: + ishi: + github: toddsundsted/ishi diff --git a/functions.yml b/functions.yml index ba4fe5a..67d34de 100644 --- a/functions.yml +++ b/functions.yml @@ -21,3 +21,10 @@ functions: image: ralsina/c-historico:latest build_args: ADDITIONAL_PACKAGE: gnuplot + c-busqueda: + lang: crystal-http + handler: ./c-busqueda + image: ralsina/c-busqueda:latest + build_args: + ADDITIONAL_PACKAGE: gnuplot +