rom-rb/rom-sql

View on GitHub
lib/rom/sql/extensions/postgres/types/geometric.rb

Summary

Maintainability
A
3 hrs
Test Coverage
# frozen_string_literal: true

module ROM
  module SQL
    module Postgres
      module Values
        Point = ::Struct.new(:x, :y)

        Line = ::Struct.new(:a, :b, :c)

        Circle = ::Struct.new(:center, :radius)

        Box = ::Struct.new(:upper_right, :lower_left)

        LineSegment = ::Struct.new(:begin, :end)

        Path = ::Struct.new(:points, :type) do
          def open?
            type == :open
          end

          def closed?
            type == :closed
          end

          def to_a
            points
          end
        end
      end

      # @api public
      module Types
        # The list of geometric data types supported by PostgreSQL
        # @see https://www.postgresql.org/docs/current/static/datatype-geometric.html

        Point = Type("point") do
          SQL::Types.define(Values::Point) do
            input do |point|
              "(#{ point.x },#{ point.y })"
            end

            output do |point|
              x, y = point.to_s[1...-1].split(",", 2)
              Values::Point.new(Float(x), Float(y))
            end
          end
        end

        Line = Type("line") do
          SQL::Types.define(Values::Line) do
            input do |line|
              "{#{ line.a },#{ line.b },#{line.c}}"
            end

            output do |line|
              a, b, c = line.to_s[1..-2].split(",", 3)
              Values::Line.new(Float(a), Float(b), Float(c))
            end
          end
        end

        Circle = Type("circle") do
          SQL::Types.define(Values::Circle) do
            input do |circle|
              "<(#{ circle.center.x },#{ circle.center.y }),#{ circle.radius }>"
            end

            output do |circle|
              x, y, r = circle.to_s.tr("()<>", "").split(",", 3)
              center = Values::Point.new(Float(x), Float(y))
              Values::Circle.new(center, Float(r))
            end
          end
        end

        Box = Type("box") do
          SQL::Types.define(Values::Box) do
            input do |box|
              "((#{ box.upper_right.x },#{ box.upper_right.y }),"\
              "(#{ box.lower_left.x },#{ box.lower_left.y }))"
            end

            output do |box|
              x_right, y_right, x_left, y_left = box.to_s.tr("()", "").split(",", 4)
              upper_right = Values::Point.new(Float(x_right), Float(y_right))
              lower_left = Values::Point.new(Float(x_left), Float(y_left))
              Values::Box.new(upper_right, lower_left)
            end
          end
        end

        LineSegment = Type("lseg") do
          SQL::Types.define(Values::LineSegment) do
            input do |segment|
              "[(#{ segment.begin.x },#{ segment.begin.y }),"\
              "(#{ segment.end.x },#{ segment.end.y })]"
            end

            output do |segment|
              x_begin, y_begin, x_end, y_end = segment.to_s.tr("()[]", "").split(",", 4)
              point_begin = Values::Point.new(Float(x_begin), Float(y_begin))
              point_end = Values::Point.new(Float(x_end), Float(y_end))
              Values::LineSegment.new(point_begin, point_end)
            end
          end
        end

        Polygon = Type("polygon") do
          SQL::Types.define(::Array) do
            input do |points|
              points_joined = points.map { |p| "(#{ p.x },#{ p.y })" }.join(",")
              "(#{ points_joined })"
            end

            output do |polygon|
              coordinates = polygon.to_s.tr("()", "").split(",").each_slice(2)
              coordinates.map { |x, y| Values::Point.new(Float(x), Float(y)) }
            end
          end
        end

        Path = Type("path") do
          SQL::Types.define(Values::Path) do
            input do |path|
              points_joined = path.to_a.map { |p| "(#{ p.x },#{ p.y })" }.join(",")

              if path.open?
                "[#{ points_joined }]"
              else
                "(#{ points_joined })"
              end
            end

            output do |path|
              open = path.to_s.start_with?("[") && path.to_s.end_with?("]")
              coordinates = path.to_s.tr("()[]", "").split(",").each_slice(2)
              points = coordinates.map { |x, y| Values::Point.new(Float(x), Float(y)) }

              if open
                Values::Path.new(points, :open)
              else
                Values::Path.new(points, :closed)
              end
            end
          end
        end
      end
    end
  end
end