vcard: build hashes and unfolds with plain enumerables
to_h over each_with_object for the component hashes;
slice_when over an accumulating loop for unfolding.

Assisted-by: GLM-5.3 via pi
change owskroqvplpwnqpnulvwuootvktyzzyw
commit 7f713d1936c769eac8786712b01d95013ef481b2
author Alpha Chen <alpha@kejadlen.dev>
date
parent qqmlmpnu
diff --git a/lib/pro_tacts/vcard.rb b/lib/pro_tacts/vcard.rb
index f903679..c205abe 100644
--- a/lib/pro_tacts/vcard.rb
+++ b/lib/pro_tacts/vcard.rb
@@ -39,10 +39,11 @@ module ProTacts
         *typed_property_lines(contact, "email", "EMAIL"),
         *address_lines(contact),
         "UID:#{escape(uid)}",
-        "END:VCARD"
+        "END:VCARD",
+        "", # trailing newline
       ]
 
-      "#{lines.map { |line| fold(line) }.join("\r\n")}\r\n"
+      lines.map { fold(it) }.join("\r\n")
     end
 
     # `name "John Smith"` derives N:Smith;John;;; (last token family, the
@@ -50,11 +51,9 @@ module ProTacts
     # when any of them is present, N is built from exactly those, and
     # every missing component renders empty.
     def structured_name(name)
-      overrides = name.children.each.with_object({}) do |child, acc|
-        next unless NAME_COMPONENTS.include?(child.name)
-
-        acc[child.name] = string_argument(child)
-      end
+      overrides = name.children
+        .select { |child| NAME_COMPONENTS.include?(child.name) }
+        .to_h { |child| [child.name, string_argument(child)] }
 
       return components(NAME_COMPONENTS.map { |component| overrides.fetch(component, "") }) unless overrides.empty?
 
@@ -78,11 +77,9 @@ module ProTacts
     # counterpart and stay empty.
     def address_lines(contact)
       contact.children.select { |node| node.name == "address" }.map do |node|
-        parts = node.children.each.with_object({}) do |child, acc|
-          next unless ADDRESS_PARTS.include?(child.name)
-
-          acc[child.name] = string_argument(child)
-        end
+        parts = node.children
+          .select { |child| ADDRESS_PARTS.include?(child.name) }
+          .to_h { |child| [child.name, string_argument(child)] }
 
         components = ["", "", *ADDRESS_PARTS.map { |part| parts.fetch(part, "") }]
         type = node.properties["type"]&.value
diff --git a/test/pro_tacts/test_vcard.rb b/test/pro_tacts/test_vcard.rb
index dab986b..de8d354 100644
--- a/test/pro_tacts/test_vcard.rb
+++ b/test/pro_tacts/test_vcard.rb
@@ -111,9 +111,7 @@ class VCardTest < Minitest::Test
     assert_operator physical.length, :>, 1, "expected folding"
     physical.each { |line| assert_operator line.bytesize, :<=, 75 }
 
-    logical = physical.each.with_object([]) do |line, acc|
-      line.start_with?(" ") ? acc.last << line[1..] : acc << line.dup
-    end
+    logical = unfold(physical)
     assert_equal "FN:#{"x" * 30}#{("é" * 60)}", logical.find { |l| l.start_with?("FN:") }
   end
 
@@ -217,12 +215,16 @@ class VCardTest < Minitest::Test
     "\"#{escaped}\""
   end
 
-  # Joins physical lines back into logical ones by removing the folding
-  # break: CRLF followed by a single space.
+  # Joins physical lines back into logical ones: a line starting with a
+  # single space continues the previous one. slice_when starts a new
+  # group wherever the next line is not a continuation.
   def logical_lines(vcard)
-    vcard.split("\r\n").each.with_object([]) do |line, acc|
-      line.start_with?(" ") ? acc.last << line[1..] : acc << line.dup
-    end
+    unfold(vcard.split("\r\n"))
+  end
+
+  def unfold(lines)
+    lines.slice_when { |_line, next_line| !next_line.start_with?(" ") }
+      .map { |group| group.first + group.drop(1).map { |line| line[1..] }.join }
   end
 
   # Reverses RFC 2426 section 2.4.2 escaping.