pax_global_header00006660000000000000000000000064136077437400014525gustar00rootroot0000000000000052 comment=7eee135a295866215ff110f8c9b6bcf741f7b65e pg_rational-0.0.2/000077500000000000000000000000001360774374000140235ustar00rootroot00000000000000pg_rational-0.0.2/.gitignore000066400000000000000000000000361360774374000160120ustar00rootroot00000000000000*.o *.so results regression.* pg_rational-0.0.2/LICENSE000066400000000000000000000020321360774374000150250ustar00rootroot00000000000000Copyright 2018 Joe Nelson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. pg_rational-0.0.2/Makefile000066400000000000000000000003351360774374000154640ustar00rootroot00000000000000MODULES = pg_rational EXTENSION = pg_rational DATA = pg_rational--0.0.1.sql REGRESS = pg_rational_test PG_CPPFLAGS = -std=c99 -Wextra -Wpedantic PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) include $(PGXS) pg_rational-0.0.2/README.md000066400000000000000000000107121360774374000153030ustar00rootroot00000000000000## Precise fractions for PostgreSQL An efficient custom type. Perfect for exact arithmetic or user-specified table row ordering. Holds values as big as an integer, with matching precision in the denominator. ### Features * Stores fractions in exactly 64 bits (same size as float) * Written in C for high performance * Detects and halts arithmetic overflow for correctness * Uses native CPU instructions for fast overflow detection * Defers GCD calculation until requested or absolutely required * Supports btree and hash indices * Implements Stern-Brocot trees for finding intermediate points * Coercion from integer/bigint/tuple * Custom aggregate ### Motivation See my blog post about [User-Defined Order in SQL](https://begriffs.com/posts/2018-03-20-user-defined-order.html). ### Usage Basics ```sql -- fractions are precise -- this would not work with a float type select 1::rational / 3 * 3 = 1; -- => t -- provides the usual operations, e.g. select '1/3'::rational + '2/7'; -- => 13/21 -- helper "ratt' type to coerce from tuples select 1 + (i,i+1)::ratt from generate_series(1,5) as i; -- => 3/2, 5/3, 7/4, 9/5, 11/6 -- simplify if desired select rational_simplify('36/12'); -- => 3/1 -- convert float to rational select 0.263157894737::float::rational; -- => 5/19 -- convert rational to float select '-1/2'::rational::float; -- => -0.5 ``` Reorder items without renumbering surrounding items. ```sql create sequence todos_seq; create table todos ( prio rational unique default nextval('todos_seq'), what text not null ); insert into todos (what) values ('install extension'), ('read about it'), ('try it'), ('profit?'); select * from todos order by prio asc; /* ┌──────┬───────────────────┐ │ prio │ what │ ├──────┼───────────────────┤ │ 1/1 │ install extension │ │ 2/1 │ read about it │ │ 3/1 │ try it │ │ 4/1 │ profit? │ └──────┴───────────────────┘ */ -- put "try" between "install" and "read" update todos set prio = rational_intermediate(1,2) where prio = 3; select * from todos order by prio asc; /* ┌──────┬───────────────────┐ │ prio │ what │ ├──────┼───────────────────┤ │ 1/1 │ install extension │ │ 3/2 │ try it │ │ 2/1 │ read about it │ │ 4/1 │ profit? │ └──────┴───────────────────┘ */ -- put "read" back between "install" and "try" update todos set prio = rational_intermediate(1,'3/2') where prio = 2; select * from todos order by prio asc; /* ┌──────┬───────────────────┐ │ prio │ what │ ├──────┼───────────────────┤ │ 1/1 │ install extension │ │ 4/3 │ read about it │ │ 3/2 │ try it │ │ 4/1 │ profit? │ └──────┴───────────────────┘ */ ``` This extension uses Stern-Brocot trees to find efficient intermediate points as fractions in lowest terms. It can continue to split deeper between fractions as much as any practical application requires. Using floats, on the other hand, and picking the midpoints between adjacent values runs out of space rapidly (you only need 50-odd inserts at the wrong spot to start hitting problems). ### Installation Clone this repo, go inside and simply run: ```bash make sudo make install ``` Then, in your database: ```sql create extension pg_rational; ``` ### Caveats The `rational_intermediate` function is super fast on typical intervals, but the narrower the range between arguments the longer it takes. We may want to add a max search depth parameter to prevent malicious values from hogging the server. ### Thanks This is my first PostgreSQL extension, and these resources were helpful in learning to write it. * https://www.postgresql.org/docs/10/static/extend-extensions.html * https://www.postgresql.org/docs/10/static/xtypes.html * http://big-elephants.com/2015-10/writing-postgres-extensions-part-i/ * https://wiki.postgresql.org/wiki/User-specified_ordering_with_fractions * #postgresql and ##c channels on freenode pg_rational-0.0.2/expected/000077500000000000000000000000001360774374000156245ustar00rootroot00000000000000pg_rational-0.0.2/expected/pg_rational_test.out000066400000000000000000000267361360774374000217310ustar00rootroot00000000000000create extension pg_rational; set client_min_messages to error; -- I/O -- can parse a simple fraction select '1/3'::rational; rational ---------- 1/3 (1 row) -- can parse negatives select '-1/3'::rational; rational ---------- -1/3 (1 row) select '1/-3'::rational; rational ---------- -1/3 (1 row) -- SEND works select rational_send('1/3'); rational_send -------------------- \x0000000100000003 (1 row) -- casting -- tuple helper select (1,2)::ratt = '1/2'::rational; ?column? ---------- t (1 row) -- int select 42 = '42/1'::rational; ?column? ---------- t (1 row) -- from float select 0.263157894737::float::rational; rational ---------- 5/19 (1 row) select 3.141592625359::float::rational; rational ----------------- 4712235/1499951 (1 row) select 0.606557377049::float::rational; rational ---------- 37/61 (1 row) select -0.5::float::rational; ?column? ---------- -1/2 (1 row) select 1.000001::float::rational; rational ----------------- 1000001/1000000 (1 row) select 1.0000001::float::rational; rational ------------------ 10000000/9999999 (1 row) select 1.00000001::float::rational; rational --------------------- 100000001/100000000 (1 row) select 1.000000001::float::rational; rational --------------------- 999999918/999999917 (1 row) select 1.0000000001::float::rational; rational ---------- 1/1 (1 row) select 2147483647::float::rational; rational -------------- 2147483647/1 (1 row) select 2147483647.1::float::rational; ERROR: value too large for rational select 'NAN'::float::rational; ERROR: value too large for rational -- to float select '1/2'::rational::float; float8 -------- 0.5 (1 row) set extra_float_digits = 0; -- default changed in PG12 select '1/3'::rational::float; float8 ------------------- 0.333333333333333 (1 row) reset extra_float_digits; select '-1/2'::rational::float; float8 -------- -0.5 (1 row) -- too big select '2147483648/2147483647'::rational; ERROR: numerator or denominator outside valid int32 value LINE 1: select '2147483648/2147483647'::rational; ^ -- no spaces select '1 /3'::rational; ERROR: Expecting '/' after number but found ' ' LINE 1: select '1 /3'::rational; ^ -- no zero denominator select '1/0'::rational; ERROR: fraction cannot have zero denominator LINE 1: select '1/0'::rational; ^ -- quoted number treated as int select '1'::rational; rational ---------- 1/1 (1 row) select '-1'::rational; rational ---------- -1/1 (1 row) -- no garbage select ''::rational; ERROR: Missing or invalid numerator LINE 1: select ''::rational; ^ select '/'::rational; ERROR: Missing or invalid numerator LINE 1: select '/'::rational; ^ select '2/'::rational; ERROR: Expecting value after '/' but got '\0' LINE 1: select '2/'::rational; ^ select '/2'::rational; ERROR: Missing or invalid numerator LINE 1: select '/2'::rational; ^ select 'sdfkjsdfj34984538'::rational; ERROR: Missing or invalid numerator LINE 1: select 'sdfkjsdfj34984538'::rational; ^ -- simplification -- double negative becomes positive select rational_simplify('-1/-3'); rational_simplify ------------------- 1/3 (1 row) -- works with negative value select rational_simplify('-3/12'); rational_simplify ------------------- -1/4 (1 row) -- dodge the INT32_MIN/-1 mistake select rational_simplify('-2147483648/2147483647'); rational_simplify ------------------------ -2147483648/2147483647 (1 row) -- don't move negative if it would overflow select rational_simplify('1/-2147483648'); rational_simplify ------------------- 1/-2147483648 (1 row) -- biggest value reduces select rational_simplify('2147483647/2147483647'); rational_simplify ------------------- 1/1 (1 row) -- smallest value reduces select rational_simplify('-2147483648/-2147483648'); rational_simplify ------------------- 1/1 (1 row) -- idempotent on simplified expression select rational_simplify('1/1'); rational_simplify ------------------- 1/1 (1 row) -- addition -- additive identity select '0/1'::rational + '1/2'; ?column? ---------- 1/2 (1 row) -- additive inverse select '1/2'::rational + '-1/2'; ?column? ---------- 0/4 (1 row) -- just regular select '1/2'::rational + '1/2'; ?column? ---------- 4/4 (1 row) -- forcing intermediate simplification select '2147483647/2147483647'::rational + '1/1'; ?column? ---------- 2/1 (1 row) -- overflow (sqrt(max)+1)/1 + 1/sqrt(max) select '46342/1'::rational + '1/46341'; ERROR: intermediate value overflow in rational addition -- multiplication -- multiplicative identity select '1/1'::rational * '1/2'; ?column? ---------- 1/2 (1 row) -- multiplicative inverse select '2/1'::rational * '1/2'; ?column? ---------- 2/2 (1 row) -- just regular select '5/8'::rational * '3/5'; ?column? ---------- 15/40 (1 row) -- forcing intermediate simplification select '2147483647/2147483647'::rational * '2/2'; ?column? ---------- 2/2 (1 row) -- overflow select '46342/46341'::rational * '46341/46342'; ERROR: intermediate value overflow in rational multiplication -- division select 1::rational / 3; ?column? ---------- 1/3 (1 row) select '2/3'::rational / '2/3'; ?column? ---------- 6/6 (1 row) -- negation -- flips sign of numerator select -('1/2'::rational); ?column? ---------- -1/2 (1 row) -- flips back select -('-1/2'::rational); ?column? ---------- 1/2 (1 row) -- overflow not possible select -('-2147483648/1'::rational); ?column? ---------------- -2147483648/-1 (1 row) select -('1/-2147483648'::rational); ?column? ---------------- -1/-2147483648 (1 row) select -('-2147483648/-2147483648'::rational); ?column? ---------- -1/1 (1 row) -- subtraction -- just regular select '1/2'::rational - '1/2'; ?column? ---------- 0/4 (1 row) -- can go negative select '1/2'::rational - '1/1'; ?column? ---------- -1/2 (1 row) -- forcing intermediate simplification select '2147483647/2147483647'::rational - '100/100'; ?column? ---------- 0/100 (1 row) -- overflow (sqrt(max)+1)/1 - 1/sqrt(max) select '46342/1'::rational - '1/46341'; ERROR: intermediate value overflow in rational addition -- comparison -- equal in every way select '1/1'::rational = '1/1'; ?column? ---------- t (1 row) -- same equivalence class select '20/40'::rational = '22/44'; ?column? ---------- t (1 row) -- negatives work too select '-20/40'::rational = '-22/44'; ?column? ---------- t (1 row) -- overflow not possible select '46342/46341'::rational = '46342/46341'; ?column? ---------- t (1 row) -- high precision select '1/2147483647'::rational = '1/2147483646'; ?column? ---------- f (1 row) select '1/3'::rational * 3 = 1; ?column? ---------- t (1 row) select 1.0/3.0 = 1.0; ?column? ---------- f (1 row) -- not everything is equal select '2/3'::rational = '8/5'; ?column? ---------- f (1 row) -- negates equality select '1/1'::rational <> '1/1'; ?column? ---------- f (1 row) -- overflow not possible select '46342/46341'::rational <> '46342/46341'; ?column? ---------- f (1 row) -- not equal select '2/3'::rational <> '8/5'; ?column? ---------- t (1 row) -- lt anti-reflexive select '1/2'::rational < '1/2'; ?column? ---------- f (1 row) -- gt anti-reflexive select '1/2'::rational > '1/2'; ?column? ---------- f (1 row) -- overflow not possible select '1/2147483647'::rational < '2/2147483647'; ?column? ---------- t (1 row) -- lte select r from unnest(ARRAY[ '303700050/303700050', '-2/1', '0/9999999', '-11/17', '100/1', '3/4', '-1/2', '-1/1', '5/8', '6/9', '5/8' ]::rational[]) as r order by r asc; r --------------------- -2/1 -1/1 -11/17 -1/2 0/9999999 5/8 5/8 6/9 3/4 303700050/303700050 100/1 (11 rows) -- gte select r from unnest(ARRAY[ '303700050/303700050', '-2/1', '0/9999999', '-11/17', '100/1', '3/4', '-1/2', '-1/1', '5/8', '6/9', '5/8' ]::rational[]) as r order by r desc; r --------------------- 100/1 303700050/303700050 3/4 6/9 5/8 5/8 0/9999999 -1/2 -11/17 -1/1 -2/1 (11 rows) -- btree create table rs ( r rational ); create index rs_r_btree on rs using btree(r); insert into rs values ('0/7'), ('1/7'), ('2/7'), ('3/7'), ('4/7'), ('5/7'), ('6/7'); set enable_seqscan=false; explain select * from rs where r > '1/7' and r <= '10/14'; QUERY PLAN -------------------------------------------------------------------------- Bitmap Heap Scan on rs (cost=4.27..14.97 rows=11 width=8) Recheck Cond: ((r > '1/7'::rational) AND (r <= '10/14'::rational)) -> Bitmap Index Scan on rs_r_btree (cost=0.00..4.27 rows=11 width=0) Index Cond: ((r > '1/7'::rational) AND (r <= '10/14'::rational)) (4 rows) select * from rs where r > '1/7' and r <= '10/14'; r ----- 2/7 3/7 4/7 5/7 (4 rows) set enable_seqscan=true; drop table rs cascade; -- hash create table rs ( r rational ); create index rs_r_hash on rs using hash(r); insert into rs values ('0/7'), ('1/7'); set enable_seqscan=false; explain select * from rs where r = '0/1'; QUERY PLAN ------------------------------------------------------------------------- Bitmap Heap Scan on rs (cost=4.09..14.76 rows=11 width=8) Recheck Cond: (r = '0/1'::rational) -> Bitmap Index Scan on rs_r_hash (cost=0.00..4.08 rows=11 width=0) Index Cond: (r = '0/1'::rational) (4 rows) select * from rs where r = '0/1'; r ----- 0/7 (1 row) select * from rs where r = '2/7'; r --- (0 rows) set enable_seqscan=true; drop table rs cascade; -- aggregates select min(r) from unnest(ARRAY[ '100/1', NULL, '-11/17', '-1/1' ]::rational[]) as r; min ------ -1/1 (1 row) select max(r) from unnest(ARRAY[ '100/1', NULL, '-11/17', '-1/1' ]::rational[]) as r; max ------- 100/1 (1 row) select max(r) from unnest(ARRAY[ NULL, NULL, NULL ]::rational[]) as r; max ----- (1 row) select rational_simplify(sum(r)) from unnest(ARRAY[ '1/1', '1/2', NULL, '-3/2', '1/16' ]::rational[]) as r; rational_simplify ------------------- 1/16 (1 row) select sum(r) from unnest(ARRAY[ NULL, NULL, NULL ]::rational[]) as r; sum ----- (1 row) -- stern-brocot intermediates -- intermediates start at 1 -- between 0 and Infinity select rational_intermediate(NULL, NULL); rational_intermediate ----------------------- 1/1 (1 row) -- random example select rational_intermediate('15/16', 1); rational_intermediate ----------------------- 16/17 (1 row) select rational_intermediate('15/16', 1) between '15/16'::rational and 1; ?column? ---------- t (1 row) select rational_intermediate('44320/39365', '77200/12184'); rational_intermediate ----------------------- 2/1 (1 row) select rational_intermediate('44320/39365', '77200/12184') between '44320/39365'::rational and '77200/12184'; ?column? ---------- t (1 row) -- unbounded upper limit produces least greater integer select rational_intermediate('1/3', NULL); rational_intermediate ----------------------- 1/1 (1 row) select rational_intermediate('3/2', NULL); rational_intermediate ----------------------- 2/1 (1 row) -- though not the other direction select rational_intermediate(NULL, '15/16'); rational_intermediate ----------------------- 1/2 (1 row) pg_rational-0.0.2/pg_rational--0.0.1--0.0.2.sql000066400000000000000000000000631360774374000202610ustar00rootroot00000000000000-- there are no sql changes between these versions pg_rational-0.0.2/pg_rational--0.0.1.sql000066400000000000000000000145271360774374000175630ustar00rootroot00000000000000\echo Use "CREATE EXTENSION pg_rational" to load this file. \quit CREATE FUNCTION rational_in(cstring) RETURNS rational AS '$libdir/pg_rational' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION rational_in_float(float8) RETURNS rational AS '$libdir/pg_rational' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION rational_out(rational) RETURNS cstring AS '$libdir/pg_rational' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION rational_out_float(rational) RETURNS float8 AS '$libdir/pg_rational' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION rational_recv(internal) RETURNS rational AS '$libdir/pg_rational' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION rational_send(rational) RETURNS bytea AS '$libdir/pg_rational' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE TYPE rational ( INPUT = rational_in, OUTPUT = rational_out, RECEIVE = rational_recv, SEND = rational_send, INTERNALLENGTH = 8 ); CREATE FUNCTION rational_create(integer, integer) RETURNS rational AS '$libdir/pg_rational' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE TYPE ratt AS (n integer, d integer); CREATE FUNCTION tuple_to_rational(ratt) RETURNS rational AS $$ SELECT rational_create($1.n,$1.d); $$ LANGUAGE SQL; CREATE CAST (ratt AS rational) WITH FUNCTION tuple_to_rational(ratt) AS IMPLICIT; CREATE CAST (float8 AS rational) WITH FUNCTION rational_in_float(float8) AS IMPLICIT; CREATE CAST (rational as float8) WITH FUNCTION rational_out_float(rational) AS IMPLICIT; CREATE FUNCTION rational_embed(integer) RETURNS rational AS '$libdir/pg_rational' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE CAST (integer AS rational) WITH FUNCTION rational_embed(integer) AS IMPLICIT; CREATE FUNCTION rational_add(rational, rational) RETURNS rational AS '$libdir/pg_rational' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE OPERATOR + ( leftarg = rational, rightarg = rational, procedure = rational_add, commutator = + ); CREATE FUNCTION rational_sub(rational, rational) RETURNS rational AS '$libdir/pg_rational' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE OPERATOR - ( leftarg = rational, rightarg = rational, procedure = rational_sub ); CREATE FUNCTION rational_mul(rational, rational) RETURNS rational AS '$libdir/pg_rational' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE OPERATOR * ( leftarg = rational, rightarg = rational, procedure = rational_mul, commutator = * ); CREATE FUNCTION rational_div(rational, rational) RETURNS rational AS '$libdir/pg_rational' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE OPERATOR / ( leftarg = rational, rightarg = rational, procedure = rational_div ); CREATE FUNCTION rational_neg(rational) RETURNS rational AS '$libdir/pg_rational' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE OPERATOR - ( rightarg = rational, procedure = rational_neg ); CREATE FUNCTION rational_simplify(rational) RETURNS rational AS '$libdir/pg_rational' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION rational_intermediate(rational, rational) RETURNS rational AS '$libdir/pg_rational' LANGUAGE C IMMUTABLE; ------------- Comparison ------------- CREATE FUNCTION rational_eq(rational, rational) RETURNS boolean AS '$libdir/pg_rational' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE OPERATOR = ( LEFTARG = rational, RIGHTARG = rational, PROCEDURE = rational_eq, COMMUTATOR = '=', NEGATOR = '<>', RESTRICT = eqsel, JOIN = eqjoinsel, HASHES, MERGES ); CREATE FUNCTION rational_ne(rational, rational) RETURNS boolean AS '$libdir/pg_rational' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE OPERATOR <> ( LEFTARG = rational, RIGHTARG = rational, PROCEDURE = rational_ne, COMMUTATOR = '<>', NEGATOR = '=', RESTRICT = neqsel, JOIN = neqjoinsel ); CREATE FUNCTION rational_lt(rational, rational) RETURNS boolean AS '$libdir/pg_rational' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE OPERATOR < ( LEFTARG = rational, RIGHTARG = rational, PROCEDURE = rational_lt, COMMUTATOR = > , NEGATOR = >= , RESTRICT = scalarltsel, JOIN = scalarltjoinsel ); CREATE FUNCTION rational_le(rational, rational) RETURNS boolean AS '$libdir/pg_rational' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE OPERATOR <= ( LEFTARG = rational, RIGHTARG = rational, PROCEDURE = rational_le, COMMUTATOR = >= , NEGATOR = > , RESTRICT = scalarltsel, JOIN = scalarltjoinsel ); CREATE FUNCTION rational_gt(rational, rational) RETURNS boolean AS '$libdir/pg_rational' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE OPERATOR > ( LEFTARG = rational, RIGHTARG = rational, PROCEDURE = rational_gt, COMMUTATOR = < , NEGATOR = <= , RESTRICT = scalargtsel, JOIN = scalargtjoinsel ); CREATE FUNCTION rational_ge(rational, rational) RETURNS boolean AS '$libdir/pg_rational' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE OPERATOR >= ( LEFTARG = rational, RIGHTARG = rational, PROCEDURE = rational_ge, COMMUTATOR = <= , NEGATOR = < , RESTRICT = scalargtsel, JOIN = scalargtjoinsel ); CREATE FUNCTION rational_cmp(rational, rational) RETURNS integer AS '$libdir/pg_rational' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE OPERATOR CLASS btree_rational_ops DEFAULT FOR TYPE rational USING btree AS OPERATOR 1 < , OPERATOR 2 <= , OPERATOR 3 = , OPERATOR 4 >= , OPERATOR 5 > , FUNCTION 1 rational_cmp(rational, rational); CREATE FUNCTION rational_hash(rational) RETURNS integer AS '$libdir/pg_rational' LANGUAGE C IMMUTABLE STRICT; CREATE OPERATOR CLASS hash_rational_ops DEFAULT FOR TYPE rational USING hash AS OPERATOR 1 = , FUNCTION 1 rational_hash(rational); ------------- Aggregates ------------- CREATE FUNCTION rational_smaller(rational, rational) RETURNS rational AS '$libdir/pg_rational' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION rational_larger(rational, rational) RETURNS rational AS '$libdir/pg_rational' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE AGGREGATE min(rational) ( SFUNC = rational_smaller, STYPE = rational, SORTOP = <, COMBINEFUNC = rational_smaller, PARALLEL = SAFE ); CREATE AGGREGATE max(rational) ( SFUNC = rational_larger, STYPE = rational, SORTOP = >, COMBINEFUNC = rational_larger, PARALLEL = SAFE ); CREATE AGGREGATE sum (rational) ( SFUNC = rational_add, STYPE = rational, COMBINEFUNC = rational_add, PARALLEL = SAFE ); pg_rational-0.0.2/pg_rational.c000066400000000000000000000323611360774374000164730ustar00rootroot00000000000000#include "postgres.h" #include "fmgr.h" #include "access/hash.h" #include "common/int.h" /* portable overflow detection */ #include "libpq/pqformat.h" /* send/recv functions */ #include #include PG_MODULE_MAGIC; typedef struct { int32 numer; int32 denom; } Rational; static int32 gcd(int32, int32); static bool simplify(Rational *); static int32 cmp(Rational *, Rational *); static void neg(Rational *); static Rational * add(Rational *, Rational *); static Rational * mul(Rational *, Rational *); static void mediant(Rational *, Rational *, Rational *); /* ***************** IO ****************** */ PG_FUNCTION_INFO_V1(rational_in); PG_FUNCTION_INFO_V1(rational_in_float); PG_FUNCTION_INFO_V1(rational_out); PG_FUNCTION_INFO_V1(rational_out_float); PG_FUNCTION_INFO_V1(rational_recv); PG_FUNCTION_INFO_V1(rational_create); PG_FUNCTION_INFO_V1(rational_embed); PG_FUNCTION_INFO_V1(rational_send); Datum rational_in(PG_FUNCTION_ARGS) { char *s = PG_GETARG_CSTRING(0), *after; long long n, d; Rational *result = palloc(sizeof(Rational)); if (!isdigit(*s) && *s != '-') ereport(ERROR, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("Missing or invalid numerator"))); n = strtoll(s, &after, 10); if (*after == '\0') { /* if just a number and no slash, interpret as an int */ d = 1; } else { /* otherwise look for denominator */ if (*after != '/') ereport(ERROR, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("Expecting '/' after number but found '%c'", *after))); if (*(++after) == '\0') ereport(ERROR, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("Expecting value after '/' but got '\\0'"))); d = strtoll(after, &after, 10); if (*after != '\0') ereport(ERROR, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("Expecting '\\0' but found '%c'", *after))); if (d == 0) ereport(ERROR, (errcode(ERRCODE_DIVISION_BY_ZERO), errmsg("fraction cannot have zero denominator"))); } if (n < INT32_MIN || n > INT32_MAX || d < INT32_MIN || d > INT32_MAX) ereport(ERROR, (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), errmsg("numerator or denominator outside valid int32 value"))); /* * prevent negative denominator, but do not negate the smallest value -- * that would produce overflow */ if (d >= 0 || n == INT32_MIN || d == INT32_MIN) { result->numer = (int32) n; result->denom = (int32) d; } else { result->numer = (int32) -n; result->denom = (int32) -d; } PG_RETURN_POINTER(result); } /* This function taken from John Kennedy's paper, "Algorithm To Convert a Decimal to a Fraction." Translated from Pascal. */ Datum rational_in_float(PG_FUNCTION_ARGS) { float8 target = PG_GETARG_FLOAT8(0), z, fnumer, fdenom, error; int32 prev_denom, sign; Rational *result = palloc(sizeof(Rational)); if (target == (int32)target) { result->numer = (int32)target; result->denom = 1; PG_RETURN_POINTER(result); } sign = target < 0.0 ? -1 : 1; target = fabs(target); if (!(target <= INT32_MAX)) { // also excludes NaN's ereport(ERROR, (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), errmsg("value too large for rational"))); } z = target; prev_denom = 0; result->numer = (int32)round(target); result->denom = 1; do { z = 1.0 / (z - floor(z)); fdenom = result->denom * floor(z) + prev_denom; fnumer = round(target * fdenom); if (fnumer > INT32_MAX || fdenom > INT32_MAX ) break; prev_denom = result->denom; result->numer = (int32)fnumer; result->denom = (int32)fdenom; error = fabs(target - ((float8) result->numer / (float8) result->denom)); } while (z != floor(z) && error >= 1e-12); result->numer *= sign; PG_RETURN_POINTER(result); } Datum rational_out(PG_FUNCTION_ARGS) { Rational *r = (Rational *) PG_GETARG_POINTER(0); PG_RETURN_CSTRING(psprintf("%d/%d", r->numer, r->denom)); } Datum rational_out_float(PG_FUNCTION_ARGS) { Rational *r = (Rational *) PG_GETARG_POINTER(0); PG_RETURN_FLOAT8((float8) r->numer / (float8) r->denom); } Datum rational_recv(PG_FUNCTION_ARGS) { StringInfo buf = (StringInfo) PG_GETARG_POINTER(0); Rational *result = palloc(sizeof(Rational)); result->numer = pq_getmsgint(buf, sizeof(int32)); result->denom = pq_getmsgint(buf, sizeof(int32)); if (result->denom == 0) ereport(ERROR, (errcode(ERRCODE_DIVISION_BY_ZERO), errmsg("fraction cannot have zero denominator: \"%d/%d\"", result->numer, result->denom))); PG_RETURN_POINTER(result); } Datum rational_create(PG_FUNCTION_ARGS) { int32 n = PG_GETARG_INT32(0), d = PG_GETARG_INT32(1); Rational *result = palloc(sizeof(Rational)); if (d == 0) ereport(ERROR, (errcode(ERRCODE_DIVISION_BY_ZERO), errmsg("fraction cannot have zero denominator: \"%d/%d\"", n, d))); result->numer = n; result->denom = d; PG_RETURN_POINTER(result); } Datum rational_embed(PG_FUNCTION_ARGS) { int32 n = PG_GETARG_INT32(0); Rational *result = palloc(sizeof(Rational)); result->numer = n; result->denom = 1; PG_RETURN_POINTER(result); } Datum rational_send(PG_FUNCTION_ARGS) { Rational *r = (Rational *) PG_GETARG_POINTER(0); StringInfoData buf; pq_begintypsend(&buf); pq_sendint(&buf, r->numer, sizeof(int32)); pq_sendint(&buf, r->denom, sizeof(int32)); PG_RETURN_BYTEA_P(pq_endtypsend(&buf)); } /* ************* ARITHMETIC ************** */ PG_FUNCTION_INFO_V1(rational_simplify); PG_FUNCTION_INFO_V1(rational_add); PG_FUNCTION_INFO_V1(rational_sub); PG_FUNCTION_INFO_V1(rational_mul); PG_FUNCTION_INFO_V1(rational_div); PG_FUNCTION_INFO_V1(rational_neg); Datum rational_simplify(PG_FUNCTION_ARGS) { Rational *in = (Rational *) PG_GETARG_POINTER(0); Rational *out = palloc(sizeof(Rational)); memcpy(out, in, sizeof(Rational)); simplify(out); PG_RETURN_POINTER(out); } Datum rational_add(PG_FUNCTION_ARGS) { Rational x, y; memcpy(&x, PG_GETARG_POINTER(0), sizeof(Rational)); memcpy(&y, PG_GETARG_POINTER(1), sizeof(Rational)); PG_RETURN_POINTER(add(&x, &y)); } Datum rational_sub(PG_FUNCTION_ARGS) { Rational x, y; memcpy(&x, PG_GETARG_POINTER(0), sizeof(Rational)); memcpy(&y, PG_GETARG_POINTER(1), sizeof(Rational)); neg(&y); PG_RETURN_POINTER(add(&x, &y)); } Datum rational_mul(PG_FUNCTION_ARGS) { Rational x, y; memcpy(&x, PG_GETARG_POINTER(0), sizeof(Rational)); memcpy(&y, PG_GETARG_POINTER(1), sizeof(Rational)); PG_RETURN_POINTER(mul(&x, &y)); } Datum rational_div(PG_FUNCTION_ARGS) { Rational x, y; int32 tmp; memcpy(&x, PG_GETARG_POINTER(0), sizeof(Rational)); memcpy(&y, PG_GETARG_POINTER(1), sizeof(Rational)); tmp = y.numer; y.numer = y.denom; y.denom = tmp; PG_RETURN_POINTER(mul(&x, &y)); } Datum rational_neg(PG_FUNCTION_ARGS) { Rational *out = palloc(sizeof(Rational)); memcpy(out, PG_GETARG_POINTER(0), sizeof(Rational)); neg(out); PG_RETURN_POINTER(out); } /* *************** UTILITY *************** */ PG_FUNCTION_INFO_V1(rational_hash); PG_FUNCTION_INFO_V1(rational_intermediate); PG_FUNCTION_INFO_V1(rational_intermediate_float); Datum rational_hash(PG_FUNCTION_ARGS) { Rational x; memcpy(&x, PG_GETARG_POINTER(0), sizeof(Rational)); /* * hash_any works at binary level, so we must simplify fraction */ simplify(&x); return hash_any((const unsigned char *) &x, sizeof(Rational)); } Datum rational_intermediate(PG_FUNCTION_ARGS) { Rational x, y, /* arguments */ lo = {0, 1}, hi = {1, 0}, /* yes, an internal use of 1/0 */ *med = palloc(sizeof(Rational)); /* * x = coalesce(lo, arg[0]) y = coalesce(hi, arg[1]) */ memcpy(&x, PG_ARGISNULL(0) ? &lo : (Rational *) PG_GETARG_POINTER(0), sizeof(Rational)); memcpy(&y, PG_ARGISNULL(1) ? &hi : (Rational *) PG_GETARG_POINTER(1), sizeof(Rational)); if (cmp(&x, &lo) < 0 || cmp(&y, &lo) < 0) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("arguments must be non-negative"))); if (cmp(&x, &y) >= 0) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("first argument must be strictly smaller than second"))); while (true) { mediant(&lo, &hi, med); if (cmp(med, &x) < 1) memcpy(&lo, med, sizeof(Rational)); else if (cmp(med, &y) > -1) memcpy(&hi, med, sizeof(Rational)); else break; } PG_RETURN_POINTER(med); } /* ************* COMPARISON ************** */ PG_FUNCTION_INFO_V1(rational_cmp); PG_FUNCTION_INFO_V1(rational_eq); PG_FUNCTION_INFO_V1(rational_ne); PG_FUNCTION_INFO_V1(rational_lt); PG_FUNCTION_INFO_V1(rational_le); PG_FUNCTION_INFO_V1(rational_gt); PG_FUNCTION_INFO_V1(rational_ge); PG_FUNCTION_INFO_V1(rational_smaller); PG_FUNCTION_INFO_V1(rational_larger); Datum rational_cmp(PG_FUNCTION_ARGS) { PG_RETURN_INT32( cmp((Rational *) PG_GETARG_POINTER(0), (Rational *) PG_GETARG_POINTER(1))); } Datum rational_eq(PG_FUNCTION_ARGS) { PG_RETURN_BOOL( cmp((Rational *) PG_GETARG_POINTER(0), (Rational *) PG_GETARG_POINTER(1)) == 0); } Datum rational_ne(PG_FUNCTION_ARGS) { PG_RETURN_BOOL( cmp((Rational *) PG_GETARG_POINTER(0), (Rational *) PG_GETARG_POINTER(1)) != 0); } Datum rational_lt(PG_FUNCTION_ARGS) { PG_RETURN_BOOL( cmp((Rational *) PG_GETARG_POINTER(0), (Rational *) PG_GETARG_POINTER(1)) < 0); } Datum rational_le(PG_FUNCTION_ARGS) { PG_RETURN_BOOL( cmp((Rational *) PG_GETARG_POINTER(0), (Rational *) PG_GETARG_POINTER(1)) <= 0); } Datum rational_gt(PG_FUNCTION_ARGS) { PG_RETURN_BOOL( cmp((Rational *) PG_GETARG_POINTER(0), (Rational *) PG_GETARG_POINTER(1)) > 0); } Datum rational_ge(PG_FUNCTION_ARGS) { PG_RETURN_BOOL( cmp((Rational *) PG_GETARG_POINTER(0), (Rational *) PG_GETARG_POINTER(1)) >= 0); } Datum rational_smaller(PG_FUNCTION_ARGS) { Rational *a = (Rational *) PG_GETARG_POINTER(0), *b = (Rational *) PG_GETARG_POINTER(1); PG_RETURN_POINTER(cmp(a, b) < 0 ? a : b); } Datum rational_larger(PG_FUNCTION_ARGS) { Rational *a = (Rational *) PG_GETARG_POINTER(0), *b = (Rational *) PG_GETARG_POINTER(1); PG_RETURN_POINTER(cmp(a, b) > 0 ? a : b); } /* ************** INTERNAL *************** */ int32 gcd(int32 a, int32 b) { int32 temp; while (b != 0) { temp = a % b; a = b; b = temp; } return a; } bool simplify(Rational * r) { int32 common = gcd(r->numer, r->denom); /* * tricky: avoid overflow from (INT32_MIN / -1) */ if (common != -1 || (r->numer != INT32_MIN && r->denom != INT32_MIN)) { r->numer /= common; r->denom /= common; } /* * prevent negative denominator, but do not negate the smallest value -- * that would produce overflow */ if (r->denom < 0 && r->numer != INT32_MIN && r->denom != INT32_MIN) { r->numer *= -1; r->denom *= -1; } return (common != 1) && (common != -1); } int32 cmp(Rational * a, Rational * b) { /* * Overflow is not an option, we need a total order so that btree indices * do not die. Hence do the arithmetic in 64 bits. */ int64 cross1 = (int64) a->numer * (int64) b->denom, cross2 = (int64) a->denom * (int64) b->numer; return (cross1 > cross2) - (cross1 < cross2); } void neg(Rational * r) { if (r->numer == INT32_MIN) { simplify(r); /* * check again */ if (r->numer == INT32_MIN) { /* * denom can't be MIN too or fraction would have previously * simplified to 1/1 */ r->denom *= -1; return; } } r->numer *= -1; } Rational * add(Rational * x, Rational * y) { int32 xnyd, ynxd, numer, denom; bool nxyd_bad, ynxd_bad, numer_bad, denom_bad; Rational *result; retry_add: nxyd_bad = pg_mul_s32_overflow(x->numer, y->denom, &xnyd); ynxd_bad = pg_mul_s32_overflow(y->numer, x->denom, &ynxd); numer_bad = pg_add_s32_overflow(xnyd, ynxd, &numer); denom_bad = pg_mul_s32_overflow(x->denom, y->denom, &denom); if (nxyd_bad || ynxd_bad || numer_bad || denom_bad) { /* overflow in intermediate value */ if (!simplify(x) && !simplify(y)) { /* neither fraction could reduce, cannot proceed */ ereport(ERROR, ( errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), errmsg("intermediate value overflow in rational addition") )); } /* the fraction(s) reduced, good for one more retry */ goto retry_add; } result = palloc(sizeof(Rational)); result->numer = numer; result->denom = denom; return result; } Rational * mul(Rational * x, Rational * y) { int32 numer, denom; bool numer_bad, denom_bad; Rational *result; retry_mul: numer_bad = pg_mul_s32_overflow(x->numer, y->numer, &numer); denom_bad = pg_mul_s32_overflow(x->denom, y->denom, &denom); if (numer_bad || denom_bad) { /* overflow in intermediate value */ if (!simplify(x) && !simplify(y)) { /* neither fraction could reduce, cannot proceed */ ereport(ERROR, (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), errmsg("intermediate value overflow in rational multiplication"))); } /* the fraction(s) reduced, good for one more retry */ goto retry_mul; } result = palloc(sizeof(Rational)); result->numer = numer; result->denom = denom; return result; } void mediant(Rational * x, Rational * y, Rational * m) { /* * Rational_intermediate sends fractions with small numers and denoms, and * slowly builds up. The search will take forever before we ever get close * to arithmetic overflow in this function, so I don't guard it here. */ m->numer = x->numer + y->numer; m->denom = x->denom + y->denom; } pg_rational-0.0.2/pg_rational.control000066400000000000000000000000671360774374000177270ustar00rootroot00000000000000comment = 'bigint fractions' default_version = '0.0.2' pg_rational-0.0.2/sql/000077500000000000000000000000001360774374000146225ustar00rootroot00000000000000pg_rational-0.0.2/sql/pg_rational_test.sql000066400000000000000000000143641360774374000207110ustar00rootroot00000000000000create extension pg_rational; set client_min_messages to error; -- I/O -- can parse a simple fraction select '1/3'::rational; -- can parse negatives select '-1/3'::rational; select '1/-3'::rational; -- SEND works select rational_send('1/3'); -- casting -- tuple helper select (1,2)::ratt = '1/2'::rational; -- int select 42 = '42/1'::rational; -- from float select 0.263157894737::float::rational; select 3.141592625359::float::rational; select 0.606557377049::float::rational; select -0.5::float::rational; select 1.000001::float::rational; select 1.0000001::float::rational; select 1.00000001::float::rational; select 1.000000001::float::rational; select 1.0000000001::float::rational; select 2147483647::float::rational; select 2147483647.1::float::rational; select 'NAN'::float::rational; -- to float select '1/2'::rational::float; set extra_float_digits = 0; -- default changed in PG12 select '1/3'::rational::float; reset extra_float_digits; select '-1/2'::rational::float; -- too big select '2147483648/2147483647'::rational; -- no spaces select '1 /3'::rational; -- no zero denominator select '1/0'::rational; -- quoted number treated as int select '1'::rational; select '-1'::rational; -- no garbage select ''::rational; select '/'::rational; select '2/'::rational; select '/2'::rational; select 'sdfkjsdfj34984538'::rational; -- simplification -- double negative becomes positive select rational_simplify('-1/-3'); -- works with negative value select rational_simplify('-3/12'); -- dodge the INT32_MIN/-1 mistake select rational_simplify('-2147483648/2147483647'); -- don't move negative if it would overflow select rational_simplify('1/-2147483648'); -- biggest value reduces select rational_simplify('2147483647/2147483647'); -- smallest value reduces select rational_simplify('-2147483648/-2147483648'); -- idempotent on simplified expression select rational_simplify('1/1'); -- addition -- additive identity select '0/1'::rational + '1/2'; -- additive inverse select '1/2'::rational + '-1/2'; -- just regular select '1/2'::rational + '1/2'; -- forcing intermediate simplification select '2147483647/2147483647'::rational + '1/1'; -- overflow (sqrt(max)+1)/1 + 1/sqrt(max) select '46342/1'::rational + '1/46341'; -- multiplication -- multiplicative identity select '1/1'::rational * '1/2'; -- multiplicative inverse select '2/1'::rational * '1/2'; -- just regular select '5/8'::rational * '3/5'; -- forcing intermediate simplification select '2147483647/2147483647'::rational * '2/2'; -- overflow select '46342/46341'::rational * '46341/46342'; -- division select 1::rational / 3; select '2/3'::rational / '2/3'; -- negation -- flips sign of numerator select -('1/2'::rational); -- flips back select -('-1/2'::rational); -- overflow not possible select -('-2147483648/1'::rational); select -('1/-2147483648'::rational); select -('-2147483648/-2147483648'::rational); -- subtraction -- just regular select '1/2'::rational - '1/2'; -- can go negative select '1/2'::rational - '1/1'; -- forcing intermediate simplification select '2147483647/2147483647'::rational - '100/100'; -- overflow (sqrt(max)+1)/1 - 1/sqrt(max) select '46342/1'::rational - '1/46341'; -- comparison -- equal in every way select '1/1'::rational = '1/1'; -- same equivalence class select '20/40'::rational = '22/44'; -- negatives work too select '-20/40'::rational = '-22/44'; -- overflow not possible select '46342/46341'::rational = '46342/46341'; -- high precision select '1/2147483647'::rational = '1/2147483646'; select '1/3'::rational * 3 = 1; select 1.0/3.0 = 1.0; -- not everything is equal select '2/3'::rational = '8/5'; -- negates equality select '1/1'::rational <> '1/1'; -- overflow not possible select '46342/46341'::rational <> '46342/46341'; -- not equal select '2/3'::rational <> '8/5'; -- lt anti-reflexive select '1/2'::rational < '1/2'; -- gt anti-reflexive select '1/2'::rational > '1/2'; -- overflow not possible select '1/2147483647'::rational < '2/2147483647'; -- lte select r from unnest(ARRAY[ '303700050/303700050', '-2/1', '0/9999999', '-11/17', '100/1', '3/4', '-1/2', '-1/1', '5/8', '6/9', '5/8' ]::rational[]) as r order by r asc; -- gte select r from unnest(ARRAY[ '303700050/303700050', '-2/1', '0/9999999', '-11/17', '100/1', '3/4', '-1/2', '-1/1', '5/8', '6/9', '5/8' ]::rational[]) as r order by r desc; -- btree create table rs ( r rational ); create index rs_r_btree on rs using btree(r); insert into rs values ('0/7'), ('1/7'), ('2/7'), ('3/7'), ('4/7'), ('5/7'), ('6/7'); set enable_seqscan=false; explain select * from rs where r > '1/7' and r <= '10/14'; select * from rs where r > '1/7' and r <= '10/14'; set enable_seqscan=true; drop table rs cascade; -- hash create table rs ( r rational ); create index rs_r_hash on rs using hash(r); insert into rs values ('0/7'), ('1/7'); set enable_seqscan=false; explain select * from rs where r = '0/1'; select * from rs where r = '0/1'; select * from rs where r = '2/7'; set enable_seqscan=true; drop table rs cascade; -- aggregates select min(r) from unnest(ARRAY[ '100/1', NULL, '-11/17', '-1/1' ]::rational[]) as r; select max(r) from unnest(ARRAY[ '100/1', NULL, '-11/17', '-1/1' ]::rational[]) as r; select max(r) from unnest(ARRAY[ NULL, NULL, NULL ]::rational[]) as r; select rational_simplify(sum(r)) from unnest(ARRAY[ '1/1', '1/2', NULL, '-3/2', '1/16' ]::rational[]) as r; select sum(r) from unnest(ARRAY[ NULL, NULL, NULL ]::rational[]) as r; -- stern-brocot intermediates -- intermediates start at 1 -- between 0 and Infinity select rational_intermediate(NULL, NULL); -- random example select rational_intermediate('15/16', 1); select rational_intermediate('15/16', 1) between '15/16'::rational and 1; select rational_intermediate('44320/39365', '77200/12184'); select rational_intermediate('44320/39365', '77200/12184') between '44320/39365'::rational and '77200/12184'; -- unbounded upper limit produces least greater integer select rational_intermediate('1/3', NULL); select rational_intermediate('3/2', NULL); -- though not the other direction select rational_intermediate(NULL, '15/16');