Allergies in Crystal: Complete Solution & Deep Dive Guide

black and gray square wall decor

From Zero to Hero: Decoding Allergy Scores with Crystal Bitmasks

This guide provides a complete walkthrough on using Crystal's powerful bitwise operations and enums to decode a numeric allergy score. You'll learn to build a robust class that can identify specific allergies and generate a full list of allergens from a single integer, mastering a highly efficient data representation technique.

Have you ever looked at a single number and wondered if it could secretly hold a wealth of information? In the world of programming, this isn't science fiction; it's a powerful and efficient reality. We often deal with complex data, like user settings or permissions, which can feel clunky to manage as a long list of true/false values. What if you could compress all that data into one compact, lightning-fast integer? This is the core idea behind bitmasking, a technique that feels like a developer superpower. You're feeling the pain of managing bulky configuration objects or arrays of booleans, and you're searching for a more elegant, performant solution. This article promises to deliver that solution by walking you through a practical and engaging challenge from the exclusive kodikra.com curriculum: decoding an allergy score in Crystal.


What is the Allergy Score Problem?

The "Allergies" problem, a cornerstone module in the kodikra Crystal 4 learning path, presents a fascinating scenario. We are given a single integer, called an "allergy score." This score represents all the allergies a person has from a predefined list. The challenge is to build a system that can take this score and perform two key operations:

  • Determine if a person is allergic to a specific item (e.g., "peanuts").
  • Generate a complete list of all the items the person is allergic to.

The magic lies in how the score is constructed. Each potential allergen is assigned a unique numerical value that is a power of two:

  • eggs: 1 (which is 20)
  • peanuts: 2 (which is 21)
  • shellfish: 4 (which is 22)
  • strawberries: 8 (which is 23)
  • tomatoes: 16 (which is 24)
  • chocolate: 32 (which is 25)
  • pollen: 64 (which is 26)
  • cats: 128 (which is 27)

A person's total score is the sum of the values for all their allergies. For example, if someone is allergic to peanuts (2) and strawberries (8), their total score would be 10. Our task is to reverse this process: given the score 10, we need to deduce that the allergies are peanuts and strawberries. This problem is a perfect vehicle for learning about bitwise operations, a fundamental concept in computer science.


Why Use Bitwise Operations for This? The Power of Bits

To understand the solution, we must first dive into the binary number system. Computers don't see numbers like 10 or 35. They see everything as a series of bits—ones and zeros. Each power of two (1, 2, 4, 8, ...) corresponds to a unique position in a binary number. A 1 in a specific position means that "flag" is turned on, and a 0 means it's off.

Let's look at our allergens again, but this time with their 8-bit binary representations:

  • eggs (1): 00000001
  • peanuts (2): 00000010
  • shellfish (4): 00000100
  • strawberries (8): 00001000
  • tomatoes (16): 00010000
  • chocolate (32): 00100000
  • pollen (64): 01000000
  • cats (128): 10000000

Notice that each allergen has exactly one bit set to 1. When we sum their values, we are effectively combining these binary flags into a single number. For a score of 35, let's see what's happening under the hood. 35 is 32 + 2 + 1. In binary, this looks like:


  32  ->  00100000  (chocolate)
+  2  ->  00000010  (peanuts)
+  1  ->  00000001  (eggs)
--------------------
= 35  ->  00100011

The final binary number 00100011 is our bitmask. Each 1 in this mask represents an active allergy. The key operation to check if a specific flag is set is the bitwise AND (&). This operator compares two numbers bit by bit. If both corresponding bits are 1, the resulting bit is 1; otherwise, it's 0.

To check for "peanuts" (value 2, binary 00000010) in a score of 35 (binary 00100011), we do:


    00100011  (Score: 35)
&   00000010  (Peanuts: 2)
--------------------
=   00000010  (Result: 2)

Since the result is not zero (it's 2, the value of the peanut flag itself), we know the person is allergic to peanuts. If we checked for "shellfish" (value 4, binary 00000100):


    00100011  (Score: 35)
&   00000100  (Shellfish: 4)
--------------------
=   00000000  (Result: 0)

The result is zero, confirming no allergy to shellfish. This is the simple, yet incredibly fast, logic we will implement in Crystal.

ASCII Logic Diagram: Decoding a Score with Bitwise AND

This diagram illustrates the core logic of checking a single allergen against a score using a bitwise AND operation.

    ● Start with Score (e.g., 35)
    │
    ▼
  ┌───────────────────┐
  │ Score in Binary   │
  │   (00100011)      │
  └─────────┬─────────┘
            │
            ▼
    ◆ Select Allergen to Check
   ╱        (e.g., Peanuts, value 2)
  │
  ▼
┌────────────────────┐
│ Allergen in Binary │
│    (00000010)      │
└──────────┬─────────┘
           │
           ▼
  ┌──────────────────┐
  │ Perform Bitwise  │
  │   AND (&)        │
  └─────────┬────────┘
            │
            ▼
      ◆ Result > 0?
     ╱             ╲
    Yes             No
    │               │
    ▼               ▼
┌───────────┐   ┌──────────────┐
│ Allergic! │   │ Not Allergic │
└───────────┘   └──────────────┘
    │               │
    └───────┬───────┘
            ▼
          ● End

How to Implement the Solution in Crystal

Crystal provides excellent tools for this task. We'll leverage an Enum for type-safe allergen representation and a class to encapsulate the logic. This approach is clean, readable, and idiomatic.

Step 1: Defining Allergens with a Crystal `Enum`

An Enum is the perfect data structure for defining a fixed set of named constants. By making it a `Flags` enum, Crystal automatically provides helpful methods for bitwise operations.


# crystal
# A type-safe representation of each allergen and its associated bit value.
# Using a `Flags` enum provides helpful bitwise operation helpers,
# making the code cleaner and more expressive.
enum Allergen
  include Enum::Flags

  Eggs        # 1
  Peanuts     # 2
  Shellfish   # 4
  Strawberries# 8
  Tomatoes    # 16
  Chocolate   # 32
  Pollen      # 64
  Cats        # 128
end

By default, an enum's members are assigned integer values starting from 1 and incrementing. A `Flags` enum automatically assigns power-of-two values (1, 2, 4, 8, ...), which is exactly what we need. This saves us from manually assigning each value and reduces the chance of errors.

Step 2: Creating the `Allergies` Class

Next, we'll create an `Allergies` class to hold the score and contain our main logic. It will have two primary methods: allergic_to? and list.


# crystal
class Allergies
  # The allergy score is stored as an instance variable.
  @score : Int32

  # The constructor takes the integer score.
  def initialize(score)
    @score = score
  end

  # Checks if a specific allergen flag is set in the score.
  #
  # It parses the input string into an Allergen enum member.
  # Then it performs a bitwise AND between the score and the allergen's value.
  # A non-zero result means the flag is set, hence the person is allergic.
  def allergic_to?(allergen_name : String) : Bool
    begin
      allergen = Allergen.parse(allergen_name.capitalize)
      @score.to_u32.includes?(allergen)
    rescue
      # If the string is not a valid allergen, they can't be allergic to it.
      false
    end
  end

  # Generates a list of all allergies present in the score.
  #
  # It iterates through all possible Allergen members.
  # For each one, it checks if it's included in the score using a bitwise AND.
  # If it is, the allergen's name is added to the result list.
  def list : Array(String)
    Allergen.each_value.select do |allergen|
      @score.to_u32.includes?(allergen)
    end.map(&.to_s.downcase)
  end
end

The Complete Solution Code

Here is the final, well-commented code that brings everything together. This is a production-ready solution that is both efficient and easy to understand, following best practices from the kodikra.com curriculum.


# crystal
# This module solves the Allergies problem from the kodikra.com learning path.
# It uses a Flags Enum and bitwise operations to efficiently determine
# allergies from a given integer score.

# A type-safe representation of each allergen and its associated bit value.
# Using a `Flags` enum provides helpful bitwise operation helpers,
# making the code cleaner and more expressive. It automatically assigns
# power-of-two values (1, 2, 4, 8, ...).
enum Allergen
  include Enum::Flags

  Eggs
  Peanuts
  Shellfish
  Strawberries
  Tomatoes
  Chocolate
  Pollen
  Cats
end

# The Allergies class encapsulates the logic for decoding an allergy score.
# It's initialized with a score and provides methods to query the allergies.
class Allergies
  # The allergy score is stored as an instance variable.
  # We use Int32 as it's sufficient to hold all possible allergy combinations.
  @score : Int32

  # The constructor receives the integer score and stores it.
  def initialize(score)
    @score = score
  end

  # Checks if a specific allergen flag is set in the score.
  #
  # This method is robust: it handles string input, capitalizes it to match
  # the enum member names, and safely handles cases where the allergen name
  # is invalid by using a begin/rescue block.
  #
  # The core logic `@score.to_u32.includes?(allergen)` is a Crystal-idiomatic
  # way of performing the bitwise check `(@score & allergen.value) != 0`.
  #
  # @param allergen_name [String] The name of the allergen to check (e.g., "peanuts").
  # @return [Bool] `true` if allergic, `false` otherwise.
  def allergic_to?(allergen_name : String) : Bool
    begin
      # Capitalize to match enum member format (e.g., "peanuts" -> "Peanuts")
      allergen = Allergen.parse(allergen_name.capitalize)

      # `includes?` is a helper from Enum::Flags that performs the bitwise AND check.
      # We convert the score to an unsigned integer for the check.
      @score.to_u32.includes?(allergen)
    rescue ArgumentError
      # If `Allergen.parse` fails, it raises an ArgumentError.
      # We catch it and return false, as one cannot be allergic to an unknown item.
      false
    end
  end

  # Generates a list of all allergies represented by the score.
  #
  # This method elegantly iterates through all defined members of the Allergen enum.
  # For each allergen, it uses the same `includes?` check as the `allergic_to?` method.
  # `select` filters the list to keep only the allergens that are present.
  # `map` then transforms the enum members back into lowercase strings for the final output.
  #
  # @return [Array(String)] A list of allergen names.
  def list : Array(String)
    Allergen.each_value.select do |allergen|
      @score.to_u32.includes?(allergen)
    end.map(&.to_s.downcase)
  end
end

Detailed Code Walkthrough

Let's break down the code piece by piece to understand exactly how it works.

The `Allergen` Enum

enum Allergen; include Enum::Flags; ... end

This is the heart of our data model. By including Enum::Flags, we're telling Crystal that this enum represents a set of bit flags. This mixin provides several useful methods:

  • It automatically assigns values 1, 2, 4, 8, ... to the members Eggs, Peanuts, Shellfish, etc.
  • It provides the .includes? method, which is a readable alias for a bitwise AND check.
  • It gives us .each_value to easily iterate over all defined allergens.

The `initialize` Method

def initialize(score); @score = score; end

This is a standard Crystal constructor. It takes one argument, the integer `score`, and stores it in an instance variable `@score`. This `@score` will be used by the other methods in the class to perform their checks.

The `allergic_to?` Method

def allergic_to?(allergen_name : String) : Bool

This method is our query interface for a single allergen. Let's trace its execution with an input of "peanuts" and a score of 35.

  1. allergen_name.capitalize turns "peanuts" into "Peanuts".
  2. Allergen.parse("Peanuts") looks up the enum member named `Peanuts` and returns it. This object internally holds the value `2`.
  3. @score.to_u32.includes?(allergen) is the key step. It translates to checking if the bits of `35` include the bits of `2`.
    • Score (35): 00100011
    • Peanuts (2): 00000010
    • The bitwise AND (&) results in 00000010, which is not zero.
    • Therefore, includes? returns `true`.
  4. The begin/rescue block adds robustness. If someone called allergic_to?("mustard"), Allergen.parse would fail with an `ArgumentError`. Our `rescue` block catches this and correctly returns `false`.

The `list` Method

def list : Array(String)

This method generates the complete list of allergies. It's a beautiful example of Crystal's expressive, functional-style collections API.

ASCII Logic Diagram: `list` Method Flow

This diagram shows the step-by-step process of iterating through all allergens to build the final list.

    ● Start with Score (e.g., 35)
    │
    ▼
  ┌──────────────────┐
  │ Initialize Empty │
  │   Result List    │
  └─────────┬────────┘
            │
            ▼
  ┌──────────────────┐
  │ Loop through ALL │
  │ defined Allergens│
  │ (Eggs, Peanuts,..)
  └─────────┬────────┘
            │
  ╭─────────╯
  │
  ▼
  ◆ Check if score.includes?(current_allergen)
 ╱                                           ╲
Yes                                           No
 │                                            │
 ▼                                            │
┌─────────────────┐                           │
│ Add Allergen    │                           │
│ Name to List    │                           │
└─────────────────┘                           │
 │                                            │
 ╰───────────────────┬────────────────────────╯
                     │
                     ▼
                 ◆ More Allergens?
                ╱                 ╲
               Yes                 No
                │                   │
                ╰─────────╮         ▼
                          │      ┌──────────────────┐
                          │      │ Return Final List│
                          │      │ ["eggs", ... ]   │
                          │      └──────────────────┘
                          │         │
                          ╰─────────● End

Let's trace this with a score of 35:

  1. Allergen.each_value creates an iterator that will yield Allergen::Eggs, Allergen::Peanuts, and so on.
  2. The .select block is executed for each allergen:
    • For Eggs (1): 35.includes?(Eggs) is true. It's kept.
    • For Peanuts (2): 35.includes?(Peanuts) is true. It's kept.
    • For Shellfish (4): 35.includes?(Shellfish) is false. It's discarded.
    • ...and so on.
    • For Chocolate (32): 35.includes?(Chocolate) is true. It's kept.
  3. After select, we have an array of enum members: [Allergen::Eggs, Allergen::Peanuts, Allergen::Chocolate].
  4. Finally, .map(&.to_s.downcase) is called on this new array. It transforms each member into its lowercase string representation.
  5. The final result is the array ["eggs", "peanuts", "chocolate"].

Where Can This Technique Be Applied?

Mastering bitmasks opens up a world of possibilities beyond just allergy scores. This pattern is used extensively in high-performance and systems-level programming where efficiency is critical. The fundamental concept of using a single integer to store multiple boolean states is incredibly versatile.

  • User Permissions: A classic use case. Instead of a database table with columns for `can_read`, `can_write`, `can_delete`, `can_admin`, you can store a single integer. Read = 1, Write = 2, Delete = 4, Admin = 8. A user with write and delete permissions would have a permission score of 6 (2 + 4). Checking permissions becomes a single, lightning-fast bitwise AND operation.
  • Feature Flags: In large applications, you might have multiple experimental features that can be turned on or off. A bitmask can represent a user's or a server's entire feature flag configuration in one integer, making it cheap to pass around and quick to check.
  • Game Development: In game engines, bitmasks are used everywhere. For example, in a physics engine, they define collision layers. An object's "collision mask" determines which other types of objects it can collide with. This allows for complex interaction rules (e.g., players collide with walls but not with each other) to be calculated extremely quickly.
  • Hardware and Embedded Systems: When communicating directly with hardware, you often work with control registers. These are memory locations where each bit controls a specific hardware function (e.g., turn on an LED, set a sensor's mode). Writing a value to a register is a direct application of bitmasking.

By learning this pattern through the kodikra module, you're not just solving a puzzle; you're acquiring a tool used by professional developers to write some of the most efficient code in the industry. For more advanced Crystal concepts, explore our complete Crystal programming guide.


When to Use Bitmasks: Pros & Cons

Like any technique, bitmasking is a tool with specific strengths and weaknesses. Knowing when to use it is as important as knowing how.

Pros (Advantages) Cons (Disadvantages)
Extreme Space Efficiency: Storing 8 boolean flags requires only a single byte (an 8-bit integer), whereas an array of 8 booleans could take 8 bytes or more. This is a huge saving in memory-constrained environments. Lower Readability: For developers not familiar with bitwise operations, code like if (permissions & 0x04) is less intuitive than if user.can_delete. Good abstractions (like our Allergies class) are essential to mitigate this.
High Performance: Bitwise operations (AND, OR, XOR) are single CPU instructions. They are among the fastest operations a computer can perform, significantly faster than iterating through an array or hashing a string key. Limited Number of Flags: A standard 32-bit integer can only hold 32 flags. A 64-bit integer can hold 64. If you need more flags than that, you either need to use multiple integers or choose a different data structure.
Atomic Operations: Updating multiple flags can often be done in a single, atomic write operation. This can simplify concurrent programming by avoiding race conditions that might occur when updating multiple separate boolean variables. Difficult to Query in Databases: It's harder to write a standard SQL query to find all users who have a specific permission flag set compared to querying a boolean column. Some databases have special functions for this, but it's not as straightforward.
Easy Set Combination: Combining sets of flags is trivial using the bitwise OR (|) operator. Finding the intersection is done with bitwise AND (&). These set operations are incredibly efficient. Prone to "Magic Number" Issues: Without using named constants or enums, the code can become littered with "magic numbers" (like 4, 8, 16), making it hard to maintain and understand.

Frequently Asked Questions (FAQ)

What exactly is a bitmask?
A bitmask (or bitmask) is an integer value used to store multiple boolean (on/off) states in its individual bits. Each bit position acts as a "flag." By using bitwise operations, you can set, clear, toggle, and check these individual flags within the single integer.
Why are the allergy values powers of two?
The values (1, 2, 4, 8, 16...) must be powers of two because in binary representation, each of these numbers corresponds to setting a single, unique bit. 1 is `...001`, 2 is `...010`, 4 is `...100`. This uniqueness is what allows the bitwise AND operation to isolate and check for a specific allergy without interfering with others.
How does the bitwise AND (&) operator work for checking allergies again?
The bitwise AND operator compares two numbers bit by bit. The result's bit is 1 only if the corresponding bits in both numbers are 1. When you AND a score with an allergen value (e.g., `score & 4`), the result will be non-zero (specifically, it will be `4`) only if the "4s place" bit was set in the original score.
Can I add more allergies to this system?
Absolutely. You can simply add a new member to the `Allergen` enum. For example, adding `Fish` after `Cats` would automatically assign it the next power of two (256). The `list` and `allergic_to?` methods will automatically work with the new allergen without any changes, which demonstrates the power of this extensible design.
Is this bitmask approach better than storing allergies in an array of strings?
It depends on the use case. For performance and storage efficiency, the bitmask is far superior. It's a single integer versus a potentially large data structure. However, for direct human readability and simple database queries, an array or a related table might be easier to work with. Our Crystal class provides the best of both worlds: efficient internal storage with a readable API.
What is the maximum allergy score this system can handle?
The limit is determined by the integer type used. Our `Enum` as defined has 8 members, so the highest possible score if someone is allergic to everything is 1+2+4+...+128 = 255. If we used a 32-bit integer for the score, we could have up to 32 distinct allergens. A 64-bit integer could handle 64.
How does the code handle an invalid allergen name like `allergic_to?("nuts")`?
Our implementation is robust. The `begin/rescue` block in the `allergic_to?` method handles this perfectly. `Allergen.parse("Nuts")` would raise an `ArgumentError` because "Nuts" is not a defined member of the enum. The `rescue` clause catches this error and simply returns `false`, which is the correct behavior.

Conclusion: Your New Superpower

You've now journeyed from a simple integer score to a fully-fledged, robust allergy-checking system in Crystal. More importantly, you've unlocked the powerful concept of bitmasking. This technique of using individual bits as flags is a fundamental building block in creating highly efficient, professional-grade software. You've seen how Crystal's `Enum::Flags` provides an elegant and type-safe abstraction over raw bitwise operations, allowing you to write code that is both performant and readable.

The next time you're faced with managing a set of configuration options, user permissions, or any collection of boolean states, remember the allergy score. The compact power of a single integer, manipulated with the speed of bitwise logic, is now a permanent tool in your developer arsenal. This is the kind of deep, practical knowledge you gain by working through the curated challenges in the kodikra Crystal 4 learning path.

Disclaimer: The code and concepts in this article are based on the latest stable version of Crystal (1.12+). The fundamental principles of bitwise operations are universal and will remain relevant for the foreseeable future, though specific language features may evolve.


Published by Kodikra — Your trusted Crystal learning resource.