TkEntry validation?

Hi.

I wrote the following to make some of my input validation from
TkEntry's a bit easier. I thought some others might be interested
(still has some parts that could use work)

···

#
# Usage:
# num = TkEntry.new(...)
# num.filter.integer.nosign # 123, 625, 86434, etc
#
# year = TkEntry.new(...)
# year.filter.integer.nosign.left_digit(4) # any number up to four
digits - no sign
#
# float = TkEntry.new(...)
# float.filter.real.right_digit(3) # any real number with at most 3
places past the decimal
#
# month = TkEntry.new(...)
# month.filter.month.letters(3) # A month - can be either 1-12 or
names (up to 3 letters)
#

require 'tk'

class TkEntry
  def acceptable_keys
    # This part is probably what needs more info in it...
    %w{Tab ISO_Left_Tab Left Right Shift Shift_L Shift_R}
  end

  def removal_keys
    %w{BackSpace Delete}
  end

  def month_names
    %w{january february march april may june july august september
october november december}
  end

  def generic_remove(key)
    s = self.value.dup
    if self.selection_present then
      s[(self.index('sel.first'))...(self.index('sel.last'))] = ""
    elsif key == "BackSpace" && self.cursor > 0 then
      s[self.cursor - 1, 1] = ""
    elsif key == "Delete" && self.cursor < s.length
      s[self.cursor, 1] = ""
    end
    s
  end

  def generic_insert(key)
    return generic_remove(key) if removal_keys.include?(key)
    s = self.value.dup
    if self.selection_present then
      s[(self.index('sel.first'))...(self.index('sel.last'))] = key
    else
      s.insert(self.cursor, key)
    end
    s
  end

  def generic_number_insert(key)
    return generic_remove(key) if removal_keys.include?(key)
    s = self.value.dup
    if key =~ /^\d$/ then
      if self.selection_present then
        s[(self.index('sel.first'))...(self.index('sel.last'))] = key
      else
        s.insert(self.cursor, key)
      end
    elsif %w{minus plus}.include?(key) then
      v = {'minus' => '-', 'plus' => '+'}[key]
      if self.selection_present then
        s[(self.index('sel.first'))...(self.index('sel.last'))] = v
      else
        s.insert(self.cursor, v)
      end
    elsif key == 'period'
      if self.selection_present then
        s[(self.index('sel.first'))...(self.index('sel.last'))] = '.'
      else
        s.insert(self.cursor, '.')
      end
    else
      Tk.callback_break # Not something to be used with numbers?
    end
    s
  end

  def filter
    me, f = self, Object.new

    f.instance_eval {
      @filter_target = me;
    }

    def f.method_missing(sym, *args, &block)
      if [:positive, :negative, :nosign, :integer, :real, :left_digit,
:right_digit, :range, :month, :letters, :alphanumeric].include?(sym)
then
        @filter_target.key_filter([sym, *args])
        self
      else
        super(sym, *args, &block)
      end
    end

    f
  end

  def key_filter_proc(type, *args)
    case type
      when :positive
        Proc.new {|key|
          next if self.acceptable_keys.include?(key)
          s = self.generic_number_insert(key)
          Tk.callback_break if s =~ /^.+\+/ || s =~ /\+.*\+/
          Tk.callback_break if s =~ /-/
          Tk.callback_break if s =~ /^\+?0/
        }
      when :negative
        Proc.new {|key|
          next if self.acceptable_keys.include?(key)
          s = self.generic_number_insert(key)
          Tk.callback_break if s =~ /^.+-/ || s =~ /-.*-/
          Tk.callback_break if s =~ /\+/
          Tk.callback_break unless s.length == 0 || s =~ /^-/
          Tk.callback_break if s =~ /^-0/
        }
      when :nosign
        Proc.new {|key|
          next if self.acceptable_keys.include?(key)
          Tk.callback_break if %w{plus minus}.include?(key)
        }
      when :integer
        Proc.new {|key|
          next if self.acceptable_keys.include?(key)
          s = self.generic_number_insert(key)
          Tk.callback_break if s =~ /^(-|\+)?0\d/
          Tk.callback_break if s =~ /^.+(-|\+)/
          Tk.callback_break if s =~ /\./
        }
      when :real
        Proc.new {|key|
          next if self.acceptable_keys.include?(key)
          s = self.generic_number_insert(key)
          Tk.callback_break if s =~ /^(-|\+)?0\d/
          Tk.callback_break if s =~ /^.+(-|\+)/
          Tk.callback_break if s.scan(/\./).length > 1
        }
      when :left_digit
        raise(ArgumentError, "Need a number of digits to allow on the
left of the decimal") if args.empty?
        n = args[0].to_i
        Proc.new {|key|
          next if acceptable_keys.include?(key)
          s = self.generic_number_insert(key)
          Tk.callback_break if s =~ /^(-|\+)?(\d*)/ && $2.length > n
        }
      when :right_digit
        raise(ArgumentError, "Need a number of digits to allow on the
left of the decimal") if args.empty?
        n = args[0].to_i
        Proc.new {|key|
          next if acceptable_keys.include?(key)
          s = self.generic_number_insert(key)
          Tk.callback_break if s =~ /\.(\d+)$/ && $1.length > n
        }
      when :range
        raise(ArgumentError, "Need a range to test against") if args.length < 1
        r = (args[0].to_i)..(args[1].to_i)
        Proc.new {|key|
          next if acceptable_keys.include?(key)
          s = self.generic_number_insert(key)
          Tk.callback_break if s.to_f < r.begin || s.to_f > r.end
        }
      when :month
        Proc.new {|key|
          next if acceptable_keys.include?(key)
          Tk.callback_break unless key =~ /^(\d|\w)$/
          s = self.generic_insert(key)
          Tk.callback_break if s =~ /\d/ && s =~ /[A-Za-z]/

          if s =~ /^\d+$/ then
            Tk.callback_break unless (1..12).include?(s.to_i)
          else
            match = Regexp.compile("^#{s}", true)
            Tk.callback_break unless self.month_names.any? {|i| i =~ match}
          end
        }
      when :letters
        if args.empty? then
          Proc.new {|key|
            next if acceptable_keys.include?(key)
            Tk.callback_break unless key =~ /^[A-Za-z]$/
          }
        else
          n = args[0].to_i
          Proc.new {|key|
            next if acceptable_keys.include?(key)
            Tk.callback_break unless key =~ /^[A-Za-z]$/ &&
generic_insert(key).length <= n
          }
        end
      when :alphanumeric
        if args.empty? then
          Proc.new {|key|
            next if acceptable_keys.include?(key)
            Tk.callback_break unless key =~ /^\w$/
          }
        else
          Proc.new {|key|
            next if acceptable_keys.include?(key)
            next if key =~ /^\w$/
            next if args.include?(key)
            Tk.callback_break
          }
        end
      else
        raise(ArgumentError, "Could not determine type of filter.")
    end
  end

  def key_filter(*constraints)
    constraints.each {|c|
      self.bind_append('Key', case c
        # Allow only positive numbers (but only checks that we have
only plus sign and only one in the front
        when Symbol then
          key_filter_proc(c)
        when Array # Special Cases
          raise(ArgumentError, "Array based filters cannot be empty.")
if c.empty?
          raise(ArgumentError, "Array based filters must start with a
symbol.") unless Symbol === c[0]
          key_filter_proc(*c)
      end, '%K')
    }
  end
end

def Tk.datetime_filter
  [fyr, fdy, fhr, fmi, fsc].each {|i|
    i.key_filter(:integer, :nosign)
  }
  fyr.key_filter([:left_digit, 4])
  fmo.key_filter(:month, [:letters, 3])
  fdy.key_filter([:range, 1, 31])
  fhr.key_filter([:range, 0, 23])
  [fmi, fsc].each {|i|
    i.key_filter([:range, 0, 59])
  }
  nil
end

actually, update ~ line 154 from

    Tk.callback_break unless key =~ /^(\d|\w)$/
to
    Tk.callback_break unless removal_keys.include?(key) || key =~ /^(\d|\w)$/

(Sorry, I just added the functionality of dealing with backspace /
delete correctly, and forgot that part).

~Matthew Maycock!

···

On 11/11/06, Matt Maycock <ummaycoc@gmail.com> wrote:

Hi.

I wrote the following to make some of my input validation from
TkEntry's a bit easier. I thought some others might be interested
(still has some parts that could use work)

#
# Usage:
# num = TkEntry.new(...)
# num.filter.integer.nosign # 123, 625, 86434, etc
#
# year = TkEntry.new(...)
# year.filter.integer.nosign.left_digit(4) # any number up to four
digits - no sign
#
# float = TkEntry.new(...)
# float.filter.real.right_digit(3) # any real number with at most 3
places past the decimal
#
# month = TkEntry.new(...)
# month.filter.month.letters(3) # A month - can be either 1-12 or
names (up to 3 letters)
#

require 'tk'

class TkEntry
  def acceptable_keys
    # This part is probably what needs more info in it...
    %w{Tab ISO_Left_Tab Left Right Shift Shift_L Shift_R}
  end

  def removal_keys
    %w{BackSpace Delete}
  end

  def month_names
    %w{january february march april may june july august september
october november december}
  end

  def generic_remove(key)
    s = self.value.dup
    if self.selection_present then
      s[(self.index('sel.first'))...(self.index('sel.last'))] = ""
    elsif key == "BackSpace" && self.cursor > 0 then
      s[self.cursor - 1, 1] = ""
    elsif key == "Delete" && self.cursor < s.length
      s[self.cursor, 1] = ""
    end
    s
  end

  def generic_insert(key)
    return generic_remove(key) if removal_keys.include?(key)
    s = self.value.dup
    if self.selection_present then
      s[(self.index('sel.first'))...(self.index('sel.last'))] = key
    else
      s.insert(self.cursor, key)
    end
    s
  end

  def generic_number_insert(key)
    return generic_remove(key) if removal_keys.include?(key)
    s = self.value.dup
    if key =~ /^\d$/ then
      if self.selection_present then
        s[(self.index('sel.first'))...(self.index('sel.last'))] = key
      else
        s.insert(self.cursor, key)
      end
    elsif %w{minus plus}.include?(key) then
      v = {'minus' => '-', 'plus' => '+'}[key]
      if self.selection_present then
        s[(self.index('sel.first'))...(self.index('sel.last'))] = v
      else
        s.insert(self.cursor, v)
      end
    elsif key == 'period'
      if self.selection_present then
        s[(self.index('sel.first'))...(self.index('sel.last'))] = '.'
      else
        s.insert(self.cursor, '.')
      end
    else
      Tk.callback_break # Not something to be used with numbers?
    end
    s
  end

  def filter
    me, f = self, Object.new

    f.instance_eval {
      @filter_target = me;
    }

    def f.method_missing(sym, *args, &block)
      if [:positive, :negative, :nosign, :integer, :real, :left_digit,
:right_digit, :range, :month, :letters, :alphanumeric].include?(sym)
then
        @filter_target.key_filter([sym, *args])
        self
      else
        super(sym, *args, &block)
      end
    end

    f
  end

  def key_filter_proc(type, *args)
    case type
      when :positive
        Proc.new {|key|
          next if self.acceptable_keys.include?(key)
          s = self.generic_number_insert(key)
          Tk.callback_break if s =~ /^.+\+/ || s =~ /\+.*\+/
          Tk.callback_break if s =~ /-/
          Tk.callback_break if s =~ /^\+?0/
        }
      when :negative
        Proc.new {|key|
          next if self.acceptable_keys.include?(key)
          s = self.generic_number_insert(key)
          Tk.callback_break if s =~ /^.+-/ || s =~ /-.*-/
          Tk.callback_break if s =~ /\+/
          Tk.callback_break unless s.length == 0 || s =~ /^-/
          Tk.callback_break if s =~ /^-0/
        }
      when :nosign
        Proc.new {|key|
          next if self.acceptable_keys.include?(key)
          Tk.callback_break if %w{plus minus}.include?(key)
        }
      when :integer
        Proc.new {|key|
          next if self.acceptable_keys.include?(key)
          s = self.generic_number_insert(key)
          Tk.callback_break if s =~ /^(-|\+)?0\d/
          Tk.callback_break if s =~ /^.+(-|\+)/
          Tk.callback_break if s =~ /\./
        }
      when :real
        Proc.new {|key|
          next if self.acceptable_keys.include?(key)
          s = self.generic_number_insert(key)
          Tk.callback_break if s =~ /^(-|\+)?0\d/
          Tk.callback_break if s =~ /^.+(-|\+)/
          Tk.callback_break if s.scan(/\./).length > 1
        }
      when :left_digit
        raise(ArgumentError, "Need a number of digits to allow on the
left of the decimal") if args.empty?
        n = args[0].to_i
        Proc.new {|key|
          next if acceptable_keys.include?(key)
          s = self.generic_number_insert(key)
          Tk.callback_break if s =~ /^(-|\+)?(\d*)/ && $2.length > n
        }
      when :right_digit
        raise(ArgumentError, "Need a number of digits to allow on the
left of the decimal") if args.empty?
        n = args[0].to_i
        Proc.new {|key|
          next if acceptable_keys.include?(key)
          s = self.generic_number_insert(key)
          Tk.callback_break if s =~ /\.(\d+)$/ && $1.length > n
        }
      when :range
        raise(ArgumentError, "Need a range to test against") if args.length < 1
        r = (args[0].to_i)..(args[1].to_i)
        Proc.new {|key|
          next if acceptable_keys.include?(key)
          s = self.generic_number_insert(key)
          Tk.callback_break if s.to_f < r.begin || s.to_f > r.end
        }
      when :month
        Proc.new {|key|
          next if acceptable_keys.include?(key)
          Tk.callback_break unless key =~ /^(\d|\w)$/
          s = self.generic_insert(key)
          Tk.callback_break if s =~ /\d/ && s =~ /[A-Za-z]/

          if s =~ /^\d+$/ then
            Tk.callback_break unless (1..12).include?(s.to_i)
          else
            match = Regexp.compile("^#{s}", true)
            Tk.callback_break unless self.month_names.any? {|i| i =~ match}
          end
        }
      when :letters
        if args.empty? then
          Proc.new {|key|
            next if acceptable_keys.include?(key)
            Tk.callback_break unless key =~ /^[A-Za-z]$/
          }
        else
          n = args[0].to_i
          Proc.new {|key|
            next if acceptable_keys.include?(key)
            Tk.callback_break unless key =~ /^[A-Za-z]$/ &&
generic_insert(key).length <= n
          }
        end
      when :alphanumeric
        if args.empty? then
          Proc.new {|key|
            next if acceptable_keys.include?(key)
            Tk.callback_break unless key =~ /^\w$/
          }
        else
          Proc.new {|key|
            next if acceptable_keys.include?(key)
            next if key =~ /^\w$/
            next if args.include?(key)
            Tk.callback_break
          }
        end
      else
        raise(ArgumentError, "Could not determine type of filter.")
    end
  end

  def key_filter(*constraints)
    constraints.each {|c|
      self.bind_append('Key', case c
        # Allow only positive numbers (but only checks that we have
only plus sign and only one in the front
        when Symbol then
          key_filter_proc(c)
        when Array # Special Cases
          raise(ArgumentError, "Array based filters cannot be empty.")
if c.empty?
          raise(ArgumentError, "Array based filters must start with a
symbol.") unless Symbol === c[0]
          key_filter_proc(*c)
      end, '%K')
    }
  end
end

def Tk.datetime_filter
  [fyr, fdy, fhr, fmi, fsc].each {|i|
    i.key_filter(:integer, :nosign)
  }
  fyr.key_filter([:left_digit, 4])
  fmo.key_filter(:month, [:letters, 3])
  fdy.key_filter([:range, 1, 31])
  fhr.key_filter([:range, 0, 23])
  [fmi, fsc].each {|i|
    i.key_filter([:range, 0, 59])
  }
  nil
end

--
There's no word in the English language for what you do to a dead
thing to make it stop chasing you.

Message-ID: <e86cebfb0611111120g21691ccbtf7ab981355d95175@mail.gmail.com>

I wrote the following to make some of my input validation from
TkEntry's a bit easier. I thought some others might be interested
(still has some parts that could use work)

Why don't you use 'validatecommand' (or 'vcmd') option and on?
Of course I know that API of validation of entry widgets is very
basic. But I think that you can make some part of your code simple.

If you don't see the option, please read 'VALIDATION' part of the
Tcl/Tk's manual of entry widgets. You'll be able to see examples of
its usage in "ext/tk/sample/demos-en/entry3.rb" on Ruby's source tree.

···

From: "Matt Maycock" <ummaycoc@gmail.com>
Subject: TkEntry validation?
Date: Sun, 12 Nov 2006 04:20:49 +0900
--
Hidetoshi NAGAI (nagai@ai.kyutech.ac.jp)

and..

I also forgot to add that part back into the letters / alphanumerics
(before this, it just passively ignored backspace/delete as an
`acceptable_key' instead of testing the new result, sorry for posting
buggyness).

`semi-final' version of the code

require 'tk'

class TkEntry
  def acceptable_keys
    %w{Tab ISO_Left_Tab Left Right Shift Shift_L Shift_R}
  end

  def removal_keys
    %w{BackSpace Delete}
  end

  def month_names
    %w{january february march april may june july august september
october november december}
  end

  def generic_remove(key)
    s = self.value.dup
    if self.selection_present then
      s[(self.index('sel.first'))...(self.index('sel.last'))] = ""
    elsif key == "BackSpace" && self.cursor > 0 then
      s[self.cursor - 1, 1] = ""
    elsif key == "Delete" && self.cursor < s.length
      s[self.cursor, 1] = ""
    end
    s
  end

  def generic_insert(key)
    return generic_remove(key) if removal_keys.include?(key)
    s = self.value.dup
    if self.selection_present then
      s[(self.index('sel.first'))...(self.index('sel.last'))] = key
    else
      s.insert(self.cursor, key)
    end
    s
  end

  def generic_number_insert(key)
    return generic_remove(key) if removal_keys.include?(key)
    s = self.value.dup
    if key =~ /^\d$/ then
      if self.selection_present then
        s[(self.index('sel.first'))...(self.index('sel.last'))] = key
      else
        s.insert(self.cursor, key)
      end
    elsif %w{minus plus}.include?(key) then
      v = {'minus' => '-', 'plus' => '+'}[key]
      if self.selection_present then
        s[(self.index('sel.first'))...(self.index('sel.last'))] = v
      else
        s.insert(self.cursor, v)
      end
    elsif key == 'period'
      if self.selection_present then
        s[(self.index('sel.first'))...(self.index('sel.last'))] = '.'
      else
        s.insert(self.cursor, '.')
      end
    else
      Tk.callback_break # Not something to be used with numbers?
    end
    s
  end

  def filter
    me, f = self, Object.new

    f.instance_eval {
      @filter_target = me;
    }

    def f.method_missing(sym, *args, &block)
      if [:positive, :negative, :nosign, :integer, :real, :left_digit,
:right_digit, :range, :month, :letters, :alphanumeric].include?(sym)
then
        @filter_target.key_filter([sym, *args])
        self
      else
        super(sym, *args, &block)
      end
    end

    f
  end

  def key_filter_proc(type, *args)
    case type
      when :positive
        Proc.new {|key|
          next if self.acceptable_keys.include?(key)
          s = self.generic_number_insert(key)
          Tk.callback_break if s =~ /^.+\+/ || s =~ /\+.*\+/
          Tk.callback_break if s =~ /-/
          Tk.callback_break if s =~ /^\+?0/
        }
      when :negative
        Proc.new {|key|
          next if self.acceptable_keys.include?(key)
          s = self.generic_number_insert(key)
          Tk.callback_break if s =~ /^.+-/ || s =~ /-.*-/
          Tk.callback_break if s =~ /\+/
          Tk.callback_break unless s.length == 0 || s =~ /^-/
          Tk.callback_break if s =~ /^-0/
        }
      when :nosign
        Proc.new {|key|
          next if self.acceptable_keys.include?(key)
          Tk.callback_break if %w{plus minus}.include?(key)
        }
      when :integer
        Proc.new {|key|
          next if self.acceptable_keys.include?(key)
          s = self.generic_number_insert(key)
          Tk.callback_break if s =~ /^(-|\+)?0\d/
          Tk.callback_break if s =~ /^.+(-|\+)/
          Tk.callback_break if s =~ /\./
        }
      when :real
        Proc.new {|key|
          next if self.acceptable_keys.include?(key)
          s = self.generic_number_insert(key)
          Tk.callback_break if s =~ /^(-|\+)?0\d/
          Tk.callback_break if s =~ /^.+(-|\+)/
          Tk.callback_break if s.scan(/\./).length > 1
        }
      when :left_digit
        raise(ArgumentError, "Need a number of digits to allow on the
left of the decimal") if args.empty?
        n = args[0].to_i
        Proc.new {|key|
          next if acceptable_keys.include?(key)
          s = self.generic_number_insert(key)
          Tk.callback_break if s =~ /^(-|\+)?(\d*)/ && $2.length > n
        }
      when :right_digit
        raise(ArgumentError, "Need a number of digits to allow on the
left of the decimal") if args.empty?
        n = args[0].to_i
        Proc.new {|key|
          next if acceptable_keys.include?(key)
          s = self.generic_number_insert(key)
          Tk.callback_break if s =~ /\.(\d+)$/ && $1.length > n
        }
      when :range
        raise(ArgumentError, "Need a range to test against") if args.length < 1
        r = (args[0].to_i)..(args[1].to_i)
        Proc.new {|key|
          next if acceptable_keys.include?(key)
          s = self.generic_number_insert(key)
          Tk.callback_break if s.to_f < r.begin || s.to_f > r.end
        }
      when :month
        Proc.new {|key|
          next if acceptable_keys.include?(key)
          Tk.callback_break unless removal_keys.include?(key) || key
=~ /^(\d|\w)$/
          s = self.generic_insert(key)
          Tk.callback_break if s =~ /\d/ && s =~ /[A-Za-z]/

          if s =~ /^\d+$/ then
            Tk.callback_break unless (1..12).include?(s.to_i)
          else
            match = Regexp.compile("^#{s}", true)
            Tk.callback_break unless self.month_names.any? {|i| i =~ match}
          end
        }
      when :letters
        if args.empty? then
          Proc.new {|key|
            next if acceptable_keys.include?(key)
            next if removal_keys.include?(key)
            Tk.callback_break unless key =~ /^[A-Za-z]$/
          }
        else
          n = args[0].to_i
          Proc.new {|key|
            next if acceptable_keys.include?(key)
            next if removal_keys.include?(key)
            Tk.callback_break unless key =~ /^[A-Za-z]$/ &&
generic_insert(key).length <= n
          }
        end
      when :alphanumeric
        if args.empty? then
          Proc.new {|key|
            next if acceptable_keys.include?(key) || removal_keys.include?(key)
            Tk.callback_break unless key =~ /^\w$/
          }
        else
          Proc.new {|key|
            next if acceptable_keys.include?(key)
            next if key =~ /^\w$/
            next if args.include?(key)
            Tk.callback_break
          }
        end
      else
        raise(ArgumentError, "Could not determine type of filter.")
    end
  end

  def key_filter(*constraints)
    constraints.each {|c|
      self.bind_append('Key', case c
        # Allow only positive numbers (but only checks that we have
only plus sign and only one in the front
        when Symbol then
          key_filter_proc(c)
        when Array # Special Cases
          raise(ArgumentError, "Array based filters cannot be empty.")
if c.empty?
          raise(ArgumentError, "Array based filters must start with a
symbol.") unless Symbol === c[0]
          key_filter_proc(*c)
      end, '%K')
    }
  end
end

def Tk.datetime_filter
  [fyr, fdy, fhr, fmi, fsc].each {|i|
    i.key_filter(:integer, :nosign)
  }
  fyr.key_filter([:left_digit, 4])
  fmo.key_filter(:month, [:letters, 3])
  fdy.key_filter([:range, 1, 31])
  fhr.key_filter([:range, 0, 23])
  [fmi, fsc].each {|i|
    i.key_filter([:range, 0, 59])
  }
  nil
end

# Note - unless other people post, this is going to look really
pathetic with me having 3 posts in a row, 2 showing how trigger happy
I am to post... :slight_smile:

Why don't you use 'validatecommand' (or 'vcmd') option and on?
Of course I know that API of validation of entry widgets is very
basic. But I think that you can make some part of your code simple.

If you don't see the option, please read 'VALIDATION' part of the
Tcl/Tk's manual of entry widgets. You'll be able to see examples of
its usage in "ext/tk/sample/demos-en/entry3.rb" on Ruby's source tree.

When I originally went looking into validate, I found it hard to find
documentation - but that example code was very helpful. Thank you.

~Matthew Maycock!