CrackedRuby logo

CrackedRuby

Array Compaction

Array compaction in Ruby removes nil and optionally blank values from arrays using built-in filtering methods.

Core Built-in Classes Array Class
2.4.8

Overview

Array compaction removes unwanted values from arrays through several core methods. Ruby provides Array#compact and Array#compact! for removing nil values, while Rails extends this functionality with compact_blank methods that remove nil, empty strings, and other blank values.

The compact method creates a new array with nil values removed, while compact! modifies the original array in place. Both methods preserve the order of remaining elements and handle nested arrays by only examining top-level elements.

# Basic nil removal
[1, nil, 2, nil, 3].compact
# => [1, 2, 3]

# In-place modification
arr = [1, nil, 2, nil, 3]
arr.compact!
# => [1, 2, 3]
# arr is now [1, 2, 3]

Rails applications have access to additional compaction methods through ActiveSupport. The compact_blank method removes nil values, empty strings, empty arrays, empty hashes, and other objects that respond to blank? with true.

# Rails/ActiveSupport compact_blank
[1, nil, "", [], {}, " ", 2].compact_blank
# => [1, " ", 2]

Array compaction operates at the top level only - nested nil values within sub-arrays remain untouched. The methods maintain the original array's class when creating new instances, preserving subclass behavior.

Basic Usage

The compact method handles the most common compaction scenario by removing nil values without modifying the original array. This approach works well when the original array must remain unchanged or when chaining multiple operations.

user_ids = [1, 2, nil, 4, nil, 6]
valid_ids = user_ids.compact

# Original array unchanged
puts user_ids
# => [1, 2, nil, 4, nil, 6]

puts valid_ids  
# => [1, 2, 4, 6]

# Chaining with other methods
User.where(id: user_ids.compact.first(3))

The compact! method modifies arrays in place, reducing memory usage when the original array is no longer needed. This method returns the modified array or nil if no changes occurred.

data = fetch_user_data()  # Returns [1, nil, 2, nil, 3]
result = data.compact!

# data is now modified
puts data
# => [1, 2, 3]

puts result
# => [1, 2, 3]

# Returns nil when no nils present
clean_data = [1, 2, 3]
result = clean_data.compact!
# => nil (no changes made)

When working with form data or user input, compaction often combines with other array methods to clean and process data streams.

# Processing form submission with multiple optional fields
form_values = params.values_at(:field1, :field2, :field3, :field4)
# => ["John", nil, "", "Doe"]

# Remove nils but keep empty strings for validation
cleaned = form_values.compact
# => ["John", "", "Doe"]

# Further processing with reject for empty strings
final_values = cleaned.reject(&:empty?)
# => ["John", "Doe"]

Rails applications can use compact_blank for more comprehensive cleaning when blank values should be treated equivalently to nil values.

# User profile data with various blank values
profile_data = [
  user.name,           # "John"
  user.middle_name,    # nil
  user.nickname,       # ""
  user.bio,           # "   "
  user.website,       # "https://example.com"
  user.tags           # []
]

# Standard compact keeps empty strings and whitespace
profile_data.compact
# => ["John", "", "   ", "https://example.com", []]

# compact_blank removes all blank values
profile_data.compact_blank  
# => ["John", "https://example.com"]

Performance & Memory

Array compaction performance scales linearly with array size, but memory behavior differs between compact and compact!. The compact method allocates a new array, potentially doubling memory usage temporarily, while compact! modifies in place with minimal memory overhead.

For large arrays, compact! provides better memory efficiency when the original array can be modified:

# Memory-efficient approach for large datasets
large_dataset = load_million_records()  # Array with 1M elements, ~20% nil

# Inefficient - creates duplicate array
cleaned_copy = large_dataset.compact  # Uses ~2x memory temporarily

# Efficient - modifies in place  
large_dataset.compact!  # Minimal additional memory usage

Benchmark comparisons show compact! performing 15-20% faster than compact for arrays over 10,000 elements due to reduced memory allocation and garbage collection pressure.

require 'benchmark'

# Test with 100k element array containing 20k nils
test_array = Array.new(100_000) { rand(5) == 0 ? nil : rand(1000) }

Benchmark.bmbm do |x|
  x.report("compact") do 
    1000.times { test_array.dup.compact }
  end
  
  x.report("compact!") do
    1000.times { test_array.dup.compact! }
  end
end

# Typical results:
#                user     system      total        real
# compact     0.891000   0.052000   0.943000 (  0.947621)
# compact!    0.743000   0.031000   0.774000 (  0.778394)

When processing data in batches, compact operations can be strategically placed to minimize memory pressure:

# Processing large CSV file with missing values
def process_large_csv(filename)
  results = []
  
  CSV.foreach(filename, headers: true).each_slice(1000) do |batch|
    # Process batch and compact to remove failed records
    processed = batch.map { |row| process_record(row) }.compact!
    
    # Accumulate results in smaller chunks
    results.concat(processed)
    
    # Periodic compaction of main results array
    results.compact! if results.size % 10_000 == 0
  end
  
  results.compact!  # Final cleanup
end

Memory profiling shows that compact_blank carries additional overhead due to the more complex blank value detection, making it 2-3x slower than standard compact but still linear in complexity.

Production Patterns

Production applications frequently use array compaction in data processing pipelines, API response cleaning, and batch operations. Database query results often contain nil values from LEFT JOINs or optional associations that require filtering before further processing.

class ReportGenerator
  def generate_monthly_stats
    # Fetch data with potential nils from outer joins
    user_stats = User.joins("LEFT JOIN subscriptions ON users.id = subscriptions.user_id")
                     .pluck(:name, :subscription_id, :subscription_type)
    
    # Compact subscription data while preserving user info
    active_users = user_stats.map do |name, sub_id, sub_type|
      {
        name: name,
        subscription: [sub_id, sub_type].compact.join('-')
      }
    end.reject { |user| user[:subscription].empty? }
    
    generate_report(active_users)
  end
end

API controllers commonly compact arrays before JSON serialization to reduce payload size and eliminate undefined values that cause client-side issues:

class Api::UsersController < ApplicationController
  def index
    users = User.includes(:profile, :settings).limit(50)
    
    render json: users.map do |user|
      {
        id: user.id,
        name: user.name,
        profile_data: [
          user.profile&.bio,
          user.profile&.website,
          user.profile&.location
        ].compact,
        preferences: user.settings&.preferences&.compact || []
      }
    end
  end
end

Background job processing uses compaction for error handling and result aggregation, particularly when processing arrays of items where some operations may fail:

class BatchProcessingJob < ApplicationJob
  def perform(item_ids)
    results = item_ids.map do |id|
      begin
        process_item(id)
      rescue ProcessingError => e
        Rails.logger.warn "Failed to process item #{id}: #{e.message}"
        nil  # Return nil for failed items
      end
    end
    
    # Compact removes failed items, leaving only successful results
    successful_results = results.compact
    
    # Update batch status with success count
    update_batch_status(
      total: item_ids.size,
      successful: successful_results.size,
      failed: item_ids.size - successful_results.size
    )
    
    # Process successful results further
    aggregate_results(successful_results) if successful_results.any?
  end
end

Monitoring compaction operations in production helps identify data quality issues and processing bottlenecks:

class CompactionMetrics
  def self.track_compaction(array, context)
    original_size = array.size
    compacted = array.compact
    nil_count = original_size - compacted.size
    
    if nil_count > 0
      StatsD.increment('array.compaction.performed', 
                      tags: ["context:#{context}"])
      StatsD.gauge('array.compaction.nil_ratio', 
                  nil_count.to_f / original_size,
                  tags: ["context:#{context}"])
    end
    
    compacted
  end
end

# Usage in data processing
processed_records = CompactionMetrics.track_compaction(
  raw_records.map(&:process), 
  'user_import'
)

Common Pitfalls

Array compaction only removes top-level nil values, leaving nested nils untouched. This behavior surprises developers who expect deep compaction across nested structures.

nested_array = [1, [2, nil, 3], nil, 4]
result = nested_array.compact
# => [1, [2, nil, 3], 4]  # Nested nil remains

# For deep compaction, use recursive approach
def deep_compact(array)
  array.map do |element|
    element.is_a?(Array) ? deep_compact(element) : element
  end.compact
end

deep_compact(nested_array)
# => [1, [2, 3], 4]

The compact! method returns nil when no modifications occur, not the array itself. This trips up developers who chain operations or check return values:

clean_array = [1, 2, 3]
result = clean_array.compact!
# => nil (no nils to remove)

# Incorrect chaining - fails because result is nil
result.first(2)  # NoMethodError: undefined method `first' for nil

# Correct approaches
clean_array.compact! || clean_array  # Return original if no changes
# or
(clean_array.compact!; clean_array)  # Always return the array

Rails compact_blank method removes more values than expected. Empty strings, whitespace-only strings, empty arrays, and empty hashes are all considered blank:

mixed_data = ["John", "", "  ", [], {}, false, 0]

# Standard compact only removes nil
mixed_data.compact
# => ["John", "", "  ", [], {}, false, 0]

# compact_blank removes empty/whitespace strings and containers
mixed_data.compact_blank  
# => ["John", false, 0]  # Note: false and 0 remain

Performance degrades when repeatedly compacting the same array without checking if compaction is needed. This commonly occurs in iterative processing:

# Inefficient - compacts on every iteration
data = load_initial_data()
10.times do |i|
  data = process_iteration(data)
  data.compact!  # May do nothing if no nils added
end

# Efficient - track if compaction needed
data = load_initial_data()
needs_compaction = false

10.times do |i|
  old_size = data.size
  data = process_iteration(data)
  needs_compaction ||= data.size != old_size || data.any?(&:nil?)
end

data.compact! if needs_compaction

Type assumptions after compaction can cause errors when the array becomes empty or when non-nil values don't match expected types:

user_ids = [nil, nil, nil]
valid_ids = user_ids.compact  # => []

# Dangerous assumption - may fail with empty array
first_user = User.find(valid_ids.first)  # Error: can't find nil

# Safe approach with presence check
if valid_ids.any?
  first_user = User.find(valid_ids.first)
end

# Or use safe navigation
first_user = User.find(valid_ids.first) if valid_ids.first

Reference

Core Methods

Method Parameters Returns Description
Array#compact None Array Returns new array with nil values removed
Array#compact! None Array or nil Removes nil values in place, returns modified array or nil if unchanged

ActiveSupport Extensions

Method Parameters Returns Description
Array#compact_blank None Array Returns new array with blank values removed (nil, "", [], {}, etc.)
Array#compact_blank! None Array or nil Removes blank values in place, returns modified array or nil if unchanged

Blank Value Detection

Values considered blank by compact_blank:

Value Type Example Blank?
nil nil Yes
Empty String "" Yes
Whitespace String " " Yes
Empty Array [] Yes
Empty Hash {} Yes
False false No
Zero 0 No
Empty Set Set.new Yes

Return Value Patterns

Method Has Changes No Changes Array State
compact New filtered array New array (same content) Original unchanged
compact! Modified array nil Original modified or unchanged
compact_blank New filtered array New array (same content) Original unchanged
compact_blank! Modified array nil Original modified or unchanged

Performance Characteristics

Operation Time Complexity Space Complexity Notes
compact O(n) O(k) where k = non-nil elements Creates new array
compact! O(n) O(1) Modifies in place
compact_blank O(n) O(k) where k = non-blank elements More expensive blank checking
compact_blank! O(n) O(1) Most expensive due to in-place blank checking

Common Usage Patterns

# Safe chaining with compact!
array.compact! || array

# Conditional compaction
array.compact! if array.any?(&:nil?)

# Deep compaction
def deep_compact(obj)
  case obj
  when Array
    obj.map { |item| deep_compact(item) }.compact
  when Hash  
    obj.transform_values { |value| deep_compact(value) }
  else
    obj
  end
end

# Batch processing with compaction
items.each_slice(100) do |batch|
  results = batch.map(&:process).compact!
  save_results(results) if results&.any?
end