Ruby Count Duplicate Labels: Hash and Block Example
Use Ruby tally to count strings, normalize labels, remove blanks, and select duplicates. Compare the result with an explicit counting hash.
By Hardeep · · Updated

Ruby tally counts occurrences and returns a hash. For imported labels, decide how to handle spaces, case, and invalid values before counting. Otherwise, Billing and billing become separate entries even when your application treats them as the same label.
A Ruby tally example with normalized labels
def tally_labels(labels)
unless labels.is_a?(Array) && labels.all? { |label| label.is_a?(String) }
raise ArgumentError, "labels must be an array of strings"
end
labels.map { |label| label.strip.downcase }
.reject { |label| label.empty? }
.tally
end
labels = [" Billing ", "billing", "Access", " "]
counts = tally_labels(labels)
p counts # {"billing"=>2, "access"=>1}
p counts.select { |_label, count| count > 1 } # {"billing"=>2}
Enumerable#tally counts equal elements and returns their counts in a hash. Normalizing before tallying is an application decision; omit downcase if capitalization distinguishes valid labels. The example rejects nil rather than converting it to an empty string. Its non-mutating string methods leave the original labels intact.
Check the empty array, a single label, repeated labels, and an invalid entry. The explicit hash example below is useful when you need more control over what contributes to each count.
Ruby the language is not Rails the framework
Ruby is a dynamically typed, object-oriented language. You work with objects by calling methods on them. An expression can fail at runtime if the object does not support the requested operation. That flexibility can make small transformations concise, but it also makes representative tests and clear method boundaries important.
Rails is a web framework written in Ruby. You can use the language for scripts and other applications without adopting Rails. Conversely, learning framework conventions does not replace understanding arrays, hashes, exceptions, and object behavior. Keep these layers separate when a tutorial says a feature is provided automatically.
The official Ruby quickstart introduces the interactive interpreter, IRB. Use it to inspect a short expression, then move a repeatable exercise into a file. Check ruby --version and run the file with ruby ticket_labels.rb. If the executable cannot be found, resolve installation and path issues before changing the code.
A small label-counting example
Imagine that ticket labels arrive as an array of strings. The rule for this exercise is explicit: remove surrounding whitespace, convert letters to lowercase, ignore empty labels, and count the remaining values. It is a simple rule for a learning tool, not a universal policy for multilingual identifiers or customer data.
def count_labels(labels)
unless labels.is_a?(Array) && labels.all? { |label| label.is_a?(String) }
raise ArgumentError, "Expected an array of strings"
end
labels
.map { |label| label.strip.downcase }
.reject { |label| label.empty? }
.each_with_object(Hash.new(0)) do |label, counts|
counts[label] += 1
end
end
p count_labels([" Billing ", "bug", "billing", " "])
The expected counts are two for billing and one for bug. The input validation rejects a list containing a missing value rather than silently treating it as text. In another application you might choose to skip invalid entries and report them; the important part is to state that policy and test it.
Read the transformation in stages. The first block normalizes each string. The second removes empty results. The final stage accumulates counts in a hash. Breaking it into named intermediate variables is also acceptable, particularly when you want to inspect a failure. Shorter code is not automatically clearer code.
Blocks express work, not magic
A block is a piece of code passed to a method. In the example, collection methods control how often their blocks run and what happens to each result. The Enumerable reference documents common iteration and transformation methods. Its development-version URL can include newer APIs; check your installed version's reference before copying unfamiliar methods.
Use a transformation when you want a transformed collection, and an iteration when you want a side effect such as printing. Mixing the two can make an expression surprising. For example, a loop that logs every record and returns an unrelated counter is harder to reuse in a reporting function that should only calculate a value.
Mutation deserves particular attention. Some methods change their receiver; others return a new object. Do not infer behavior solely from a method's punctuation. Read its contract and check whether callers still need the original value. In the example, keeping the input labels intact makes it possible to compare the normalized output with the source when debugging.
Make edge cases part of the explanation
A demonstration with clean input tells you little about a real import. Decide what happens to whitespace, missing fields, mixed types, and duplicate labels before building a larger pipeline around the function. You can capture those decisions in a small table and then turn the table into automated checks.
| Input | Expected behavior | Reason |
|---|---|---|
| An empty array | An empty count hash | No labels to count |
| Two strings differing only in case | One key with count two | Case normalization |
| A whitespace-only string | No added key | Empty labels are ignored |
| An array containing nil | ArgumentError | Invalid input is visible |
One plain-language test is: given two equivalent labels, the result contains one normalized key with a count of two. That statement stays useful even if the implementation changes. Avoid tests that depend on incidental formatting of printed hashes when the actual requirement concerns the keys and counts.
For a quick local exercise, compare the returned hash with an expected hash and raise an error if they differ. As the project grows, put those assertions in its test framework. Keep sample tickets fictional; debugging examples rarely need real customer messages.
Dependencies are part of the program
Ruby libraries are commonly packaged as gems. A project using several gems needs a reproducible dependency setup, not a list of packages somebody installed globally months ago. Bundler records dependency requirements and resolved versions. The official dependency-sharing guide explains the relationship between the Gemfile and lockfile.
For an application, keep those project files under version control and use the documented installation process. Run project commands in the bundle's context where appropriate; bundle exec is the reference for that behavior. A lockfile supports reproducibility, but it does not prove dependencies are secure or maintained.
Review a dependency before adding it for a small convenience. Ask what problem it solves, which transitive packages it adds, how updates are tested, and whether deployment supports its native requirements. Avoid a broad update immediately before release unless you have time to investigate changed behavior.
Readable code needs boundaries
Dynamic dispatch makes it possible to accept objects with the behavior a method needs. It also makes undocumented assumptions easy to spread. A method that expects a string should either validate at an external boundary or clearly document that contract for internal callers. Repeatedly rescuing errors deep inside the calculation usually obscures the actual source of invalid data.
Keep error handling narrow. If a file cannot be read, report the file operation's failure; do not return an empty result that looks like a successful import with no tickets. If a single row is rejected, preserve enough non-sensitive context to locate it. A row number and reason often help more than dumping the entire input.
Likewise, separate calculation from external work. The counting function should not send email, modify the source file, or update a database. A caller can choose those actions after validating the result. That separation makes the same logic easier to test and reuse.
Performance and memory: measure the actual workload
The example creates intermediate arrays. That is reasonable for a small exercise, but a large import might need incremental processing. Before optimizing, measure realistic input sizes and identify whether time is spent parsing, allocating, accessing the network, or querying storage. Replacing readable code with a dense expression does not answer that question.
Garbage collection does not prevent retained data from growing. An application can keep every processed record in a long-lived object or cache. Concurrency behavior also depends on the Ruby implementation and the work being done. Avoid universal claims that adding threads will speed up all calculations.
What to build next
Extend the exercise by reading a small fictional file and reporting rejected rows separately from valid counts. Add a test for each stated rule. Then run it from a clean checkout using the recorded runtime and dependencies. This gives you a more useful foundation than a larger demo that only works inside one configured editor.
If the tool later stores tickets, evaluate that database work separately. The SQL indexing guide offers a measurement-based starting point. Ruby can make an application pleasant to express, but reliability comes from explicit rules, reproducible environments, and checks that expose mistakes.
Check that normalization leaves the input intact
Store the labels in a variable, call count_labels, and inspect both the returned hash and the original array. For this exercise the source strings should remain unchanged. Then add nil to the input and confirm that validation fails instead of creating a misleading count.
Related reading
Send a correction with this article’s link and a supporting source.