Coverage Summary for Class: TransactionTags (com.georgeci.moneysurfer.domain.model)

Class Class, % Method, % Branch, % Line, % Instruction, %
TransactionTags 100% (1/1) 100% (2/2) 100% (2/2) 100% (8/8) 100% (58/58)


 package com.georgeci.moneysurfer.domain.model
 
 /**
  * Normalization for [Transaction.tags].
  *
  * Tags are an embedded list, not a join table: they are free-form labels with no identity
  * of their own — nothing renames, merges or counts them across transactions yet. A single
  * column costs one migration now; a join table would cost one anyway once a tag manager
  * exists, and buys nothing before then.
  *
  * That storage choice is why normalization is mandatory rather than cosmetic. Room keeps the
  * list as one CSV column, so a comma inside a tag would silently split it in two on the next
  * read. Stripping the separator is the only round-trip-safe option — rejecting the input
  * would surface a storage detail in the UI, and escaping would make the column unreadable
  * in a SQL browser for no gain.
  */
 object TransactionTags {
 
     /**
      * Upper bound on tags per transaction. Bounds both the CSV column and the Firestore doc,
      * and no design surface shows more than a handful.
      */
     const val MAX_TAGS: Int = 10
 
     private const val SEPARATOR = ','
     private val WHITESPACE_RUN = Regex("\\s+")
 
     /**
      * Strips separators, trims, drops blanks, removes case-insensitive duplicates keeping the
      * first spelling the user typed, and caps at [MAX_TAGS].
      */
     fun normalize(raw: List<String>): List<String> {
         val seen = mutableSetOf<String>()
         return raw.asSequence()
             .map { it.replace(SEPARATOR, ' ').replace(WHITESPACE_RUN, " ").trim() }
             .filter { it.isNotEmpty() }
             .filter { seen.add(it.lowercase()) }
             .take(MAX_TAGS)
             .toList()
     }
 }