class Kiss # This class is used to validate and parse strings into Sequel-ready values. class Format # used to raise exceptions when values don't validate to format regexp class ValidateError < Exception; end @_regexp = nil @_error = 'invalid format' @_legend = nil @_input_width = nil class << self _attr_accessor :regexp, :error, :legend, :input_width # copy Format class instance variables to their subclasses def inherited(subclass) self.instance_variables.each do |var| subclass.instance_variable_set(var, instance_variable_get(var)) end end def parse(value, context = {}) value end def value_to_s(value, context = {}) value.to_s end def validate(value, context = {}) if !value.blank? && @_regexp && value.to_s !~ @_regexp raise Kiss::Format::ValidateError, @_error end parse(value, context) end end class Integer < self @_regexp = /\A\-?\d+\Z/ @_error = 'must be an integer' class << self def parse(value, context = {}) value.to_i end def validate(value, context = {}) # remove commas for thousands value.blank? ? nil : super(value.gsub(/[\$,]/, ''), context) end def value_to_s(value, context = {}) # add commas for thousands value end end end class PositiveInteger < Integer @_regexp = /\A\d*[1-9]\d*\Z/ @_error = 'must be a positive integer' end class UnsignedInteger < Integer @_regexp = /\A\d+\Z/ @_error = 'must be a positive integer or zero' end class NegativeInteger < Integer @_regexp = /\A\-\d*[1-9]\d*\Z/ @_error = 'must be a negative integer' end class Decimal < Integer @_regexp = /\A\-?(\d+(\.\d*)?|\.\d+)\Z/ @_error = 'must be a decimal number' class << self def parse(value, context = {}) value.to_f end def value_to_s(value, context = {}) # add commas for thousands, to integer part only value.format_thousands end end end class AlphaNum < self @_regexp = /\A[a-z0-9]+\Z/i @_error = 'only letters and numbers allowed' end class Word < self @_regexp = /\A\w+\Z/ @_error = 'only letters, numbers, and underscores allowed' end class EmailAddress < self @_regexp = /\A[A-Z0-9._%+-]+\@([A-Z0-9\-]+\.)+[A-Z]{2,4}\Z/i @_error = 'must be a valid email address' end class URL < self @_regexp = /\Ahttps?:\/\/([A-Z0-9\-]+\.)+[A-Z]{2,4}[^\"\']*\Z/i @_error = 'must be a valid web address, starting with http://' end class DateTime < self @_regexp = /\A\d+\D\d+(\D\d+)?\s+\d{1,2}(\:\d{2})?\s*[ap]m\Z/i @_error = 'must be a valid date and time' @_legend = 'm/d/yyyy h:mm a/pm' @_input_width = 140 class << self def parse(value, context = {}) return nil unless value =~ /\S/ relative_time = context[:year] ? ::Time.parse("1/1/#{context[:year]}") : ::Time.now convert_value_local_to_utc(::Time.parse(value.gsub(/[-\.]/, '/'), relative_time), context) end def convert_value_local_to_utc(value, context) if value && !value.zero? && (timezone = context[:timezone]) timezone = TZInfo::Timezone.get(timezone) if timezone.is_a?(String) timezone.local_to_utc(value) else value end end def convert_value_utc_to_local(value, context) if value && !value.zero? && (timezone = context[:timezone]) timezone = TZInfo::Timezone.get(timezone) if timezone.is_a?(String) timezone.utc_to_local(value) else value end end def value_to_s(value, context = {}) convert_value_utc_to_local(value, context).strftime("%m/%d/%Y %I:%M %p").gsub(/0(\d[\:\/])/, '\1') end def validate(value, context = {}) value.blank? ? nil : super(value, context) end end end class Date < DateTime @_regexp = /\A\d+\D\d+(\D\d+)?\Z/ @_error = 'must be a valid date' @_legend = 'm/d/yyyy' @_input_width = 80 def self.value_to_s(value, context = {}) value.strftime("%m/%d/%Y") end end class Time < DateTime @_regexp = /\A\d+\:\d+\s*[ap]m\Z/i, @_error = 'must be a valid time' @_legend = 'h:mm a/pm' @_input_width = 80 end class MonthYear < Date @_regexp = /\A\d+\D\d+\Z/ @_error = 'must be a valid month and year (m/yyyy)' @_legend = 'm/yyyy' @_input_width = 80 class << self def parse(value, context = {}) month, year = value.sub(/\A\s*/, '').sub(/\s*\Z/, '').split(/\D+/) # convert two-digit years to four-digit years year = year.to_i if year < 100 year += 1900 year += 100 if year < ::Time.now.year - 95 end begin ::Time.parse("#{month}/#{year}") rescue ArgumentError => e raise Kiss::Format::ValidateError, e.message end end def value_to_s(value, context = {}) value.strftime("%m/%Y") end end end @@symbols = { :integer => Integer, :integer_positive => PositiveInteger, :positive_integer => PositiveInteger, :id => PositiveInteger, :integer_unsigned => UnsignedInteger, :unsigned_integer => UnsignedInteger, :id_or_zero => UnsignedInteger, :id_zero => UnsignedInteger, :integer_negative => NegativeInteger, :negative_integer => NegativeInteger, :decimal => Decimal, :word => Word, :alphanum => AlphaNum, :email => EmailAddress, :email_address => EmailAddress, :url => URL, :datetime => DateTime, :date => Date, :time => Time, :month_year => MonthYear } def self.lookup(format) format = format.to_sym if format.is_a?(String) format.is_a?(Symbol) ? # if symbol, lookup format by symbol @@symbols[format] || self : # else return format, or Kiss::Format if format not defined format || self end end end