vcard: read a parameter by name off the property
Which TYPE a phone meant, and that a name matches without case,
is the grammar's own rule, kept in a caller's private place.
The property answers for its parameters now, the first value returned,
and what stays upstairs is the downcase, the one part Contact earned.

Assisted-by: Claude Opus 5 via Claude Code

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015WjrPJFvetad8vvCHqjujh
change
commit ef7277acff11da4ab377d71d7c9fe35d87073ccd
author Claude <noreply@anthropic.com>
date
parent qxrqmmpv
diff --git a/lib/pro_tacts/contact.rb b/lib/pro_tacts/contact.rb
index 3dd5797..3181354 100644
--- a/lib/pro_tacts/contact.rb
+++ b/lib/pro_tacts/contact.rb
@@ -396,12 +396,15 @@ module ProTacts
       Address.new(po_box:, extended:, street:, locality:, region:, postal_code:, country:, type: type_of(property), line:)
     end
 
-    # RFC 2426 section 3.3.1: TYPE can repeat (TYPE=work;TYPE=voice) or
-    # comma-list (TYPE=work,voice) — either arrives here as one
-    # parameter per value, so the first is the one this carries.
+    # A row's type parameter (RFC 2426 section 3.3.1), the first of
+    # them where a card lists several — which one that is, and that a
+    # name matches without case, is the grammar's and so
+    # Property#parameter's. What is this layer's is the downcase: the
+    # spelling is the card's, `WORK` and `work` label the same row,
+    # and a screen shows one of them.
     #: (VCard::Parser::Property property) -> String?
     def type_of(property)
-      property.parameters.find { |name, _| name.casecmp?("TYPE") }&.last&.downcase
+      property.parameter("TYPE")&.downcase
     end
   end
 end
diff --git a/lib/pro_tacts/vcard/parser.rb b/lib/pro_tacts/vcard/parser.rb
index c2e15ee..9d6010e 100644
--- a/lib/pro_tacts/vcard/parser.rb
+++ b/lib/pro_tacts/vcard/parser.rb
@@ -89,6 +89,25 @@ module ProTacts
         # component instead of ending it.
         #: () -> Array[String]
         def components = VCard.split_components(value)
+
+        # The named parameter's first value, or nil when the property
+        # carries none. Names compare without case, RFC 2426 section 4
+        # making them case-insensitive, and the first value is the
+        # answer because a parameter can repeat: section 3.3.1's
+        # `TYPE=work;TYPE=voice` and `TYPE=work,voice` are the same
+        # thing and both arrive as two pairs, so a caller wanting all
+        # of them reads `parameters` itself.
+        #
+        # Lookup, not meaning: that a parameter name is
+        # case-insensitive and that a repeat is a second value are
+        # the grammar's rules, which is what keeps this on the shallow
+        # side of the line the class comment draws. Which parameters a
+        # property may carry, and what any of them says, stays the
+        # caller's.
+        #: (String name) -> String?
+        def parameter(name)
+          parameters.find { |parameter, _| parameter.casecmp?(name) }&.last
+        end
       end
 
       # One logical line of a card, parsed beside the exact bytes that
diff --git a/test/pro_tacts/vcard/test_parser.rb b/test/pro_tacts/vcard/test_parser.rb
index 3a629a5..23f3ba2 100644
--- a/test/pro_tacts/vcard/test_parser.rb
+++ b/test/pro_tacts/vcard/test_parser.rb
@@ -367,4 +367,23 @@ class VCardParserTest < Minitest::Test
     assert_equal "Smith; Jr.;John;;;", property.text
     assert_equal ["Smith; Jr.", "John", "", "", ""], property.components
   end
+
+  ## Parameters
+
+  # A name matches without case (RFC 2426 section 4), and a parameter
+  # a card lists several values for answers with the first of them —
+  # both spellings of the repeat reaching here as the same two pairs.
+  def test_a_parameter_reads_by_name_without_case
+    property = properties(card("TEL;type=WORK,voice:+1-555-1234")).fetch(0)
+
+    assert_equal "WORK", property.parameter("TYPE")
+    assert_equal "WORK", property.parameter("type")
+    assert_equal ["WORK", "voice"], property.parameters.map(&:last)
+  end
+
+  def test_a_parameter_the_property_does_not_carry_reads_as_nil
+    property = properties(card("TEL:+1-555-1234")).fetch(0)
+
+    assert_nil property.parameter("TYPE")
+  end
 end