diff --git a/ext/pg_text_decoder.c b/ext/pg_text_decoder.c index 6d4151e9a..d0ca3d418 100644 --- a/ext/pg_text_decoder.c +++ b/ext/pg_text_decoder.c @@ -49,6 +49,8 @@ static ID s_id_utc; static ID s_id_getlocal; static ID s_id_BigDecimal; +static VALUE s_Date; +static VALUE s_Date_GREGORIAN; /* Date::GREGORIAN */ static VALUE s_IPAddr; static VALUE s_vmasks4; static VALUE s_vmasks6; @@ -937,6 +939,51 @@ pg_text_dec_inet(t_pg_coder *conv, const char *val, int len, int tuple, int fiel return ip; } +/* + * Document-class: PG::TextDecoder::Date < PG::SimpleDecoder + * + * This is a decoder class for conversion of PostgreSQL date type to Ruby Date values. + * + * Dates are expected in ISO format (+YYYY-MM-DD+), which is PostgreSQL's default DateStyle. + * Other formats, BC dates, years beyond 9999 and the special values +infinity+ and +-infinity+ + * are returned as String. + * + * As soon as this class is used, it requires the ruby standard library 'date'. + */ +static VALUE +pg_text_dec_date(t_pg_coder *conv, const char *val, int len, int tuple, int field, int enc_idx) +{ + if (len == 10 + && isdigit(val[0]) && isdigit(val[1]) && isdigit(val[2]) && isdigit(val[3]) + && val[4] == '-' && isdigit(val[5]) && isdigit(val[6]) + && val[7] == '-' && isdigit(val[8]) && isdigit(val[9]) + ) { + int year = str2_to_int(val) * 100 + str2_to_int(val + 2); + int mon = str2_to_int(val + 5); + int day = str2_to_int(val + 8); + + return rb_funcall(s_Date, s_id_new, 4, INT2NUM(year), INT2NUM(mon), INT2NUM(day), s_Date_GREGORIAN); + } + + return pg_text_dec_string(conv, val, len, tuple, field, enc_idx); +} + +/* called per autoload when TextDecoder::Date is used */ +static VALUE +init_pg_text_decoder_date(VALUE rb_mPG_TextDecoder) +{ + rb_require("date"); + rb_gc_register_address(&s_Date); + rb_gc_register_address(&s_Date_GREGORIAN); + s_Date = rb_const_get(rb_cObject, rb_intern("Date")); + s_Date_GREGORIAN = rb_const_get(s_Date, rb_intern("GREGORIAN")); + + /* dummy = rb_define_class_under( rb_mPG_TextDecoder, "Date", rb_cPG_SimpleDecoder ); */ + pg_define_coder( "Date", pg_text_dec_date, rb_cPG_SimpleDecoder, rb_mPG_TextDecoder ); + + return Qnil; +} + /* called per autoload when TextDecoder::Inet is used */ static VALUE init_pg_text_decoder_inet(VALUE rb_mPG_TextDecoder) @@ -982,6 +1029,7 @@ init_pg_text_decoder(void) /* This module encapsulates all decoder classes with text input format */ rb_mPG_TextDecoder = rb_define_module_under( rb_mPG, "TextDecoder" ); + rb_define_private_method(rb_singleton_class(rb_mPG_TextDecoder), "init_date", init_pg_text_decoder_date, 0); rb_define_private_method(rb_singleton_class(rb_mPG_TextDecoder), "init_inet", init_pg_text_decoder_inet, 0); rb_define_private_method(rb_singleton_class(rb_mPG_TextDecoder), "init_numeric", init_pg_text_decoder_numeric, 0); diff --git a/lib/pg/text_decoder/date.rb b/lib/pg/text_decoder/date.rb index 6fc4cf4a0..652fbac52 100644 --- a/lib/pg/text_decoder/date.rb +++ b/lib/pg/text_decoder/date.rb @@ -1,21 +1,9 @@ # -*- ruby -*- # frozen_string_literal: true -require 'date' - module PG module TextDecoder - # This is a decoder class for conversion of PostgreSQL date type to Ruby Date values. - # - # As soon as this class is used, it requires the ruby standard library 'date'. - class Date < SimpleDecoder - def decode(string, tuple=nil, field=nil) - if string =~ /\A(\d{4})-(\d\d)-(\d\d)\z/ - ::Date.new $1.to_i, $2.to_i, $3.to_i, ::Date::GREGORIAN - else - string - end - end - end + # Init C part of the decoder + init_date end end # module PG diff --git a/spec/pg/type_spec.rb b/spec/pg/type_spec.rb index 91336be6a..e8d952838 100644 --- a/spec/pg/type_spec.rb +++ b/spec/pg/type_spec.rb @@ -234,6 +234,35 @@ def textdec_timestamptz_decode_should_fail(str) end end + context 'dates' do + let(:textdec_date) { PG::TextDecoder::Date.new } + + it 'decodes ISO dates' do + expect( textdec_date.decode('2016-01-02') ).to eq( Date.new(2016, 1, 2) ) + expect( textdec_date.decode('0001-12-31') ).to eq( Date.new(1, 12, 31, Date::GREGORIAN) ) + end + it 'decodes dates in the proleptic gregorian calendar' do + expect( textdec_date.decode('1581-12-15').start ).to eq( Date::GREGORIAN ) + end + it 'returns special values, BC dates and years beyond 9999 as string' do + %w[infinity -infinity].each do |value| + expect( textdec_date.decode(value) ).to eq( value ) + end + expect( textdec_date.decode('0044-03-15 BC') ).to eq( '0044-03-15 BC' ) + expect( textdec_date.decode('10000-01-01') ).to eq( '10000-01-01' ) + end + it 'returns dates in other formats as string' do + %w[01/02/2016 02.01.2016 2016-1-02 2016-01-2 2016/01/02 2016-01ยช0].each do |value| + expect( textdec_date.decode(value) ).to eq( value ) + end + expect( textdec_date.decode(' 2016-01-02') ).to eq( ' 2016-01-02' ) + expect( textdec_date.decode('2016-01-02 ') ).to eq( '2016-01-02 ' ) + end + it 'raises on invalid dates' do + expect{ textdec_date.decode('2016-02-30') }.to raise_error( Date::Error ) + end + end + context 'identifier quotation' do it 'should build an array out of an quoted identifier string' do quoted_type = PG::TextDecoder::Identifier.new