What You’ll Learn in This Module
This module walks through the CustomerSince snippet — one of Cast’s most popular real-world patterns. It calculates how long a customer has been with your company and expresses it in natural language: “2 years and 3 months,” “6 months and 1 week,” or “2 weeks and 3 days.” Along the way, you’ll see how every concept from previous modules — date math, string-to-number conversion, conditional logic, capture blocks, pluralization, and defensive coding — comes together in a single, practical snippet.
📖 https://school.cast.app/liquid/liquid-library.html#customer-since-snippet
Why This Pattern Matters
A welcome slide that says “Thank you for being a customer” is generic. One that says “Thank you for being a customer for 2 years and 3 months” is personal and acknowledges loyalty. The CustomerSince snippet makes this level of personalization automatic — it reads the contract start date, does the math, and produces the natural-language string.
How It Works — Conceptual Overview
The logic follows these steps:
- Read the contract start date from CRM data
- Validate that the date is real (not blank, not a garbage value)
- Convert both the start date and today’s date to Unix timestamps
- Subtract to get the difference in seconds
- Convert seconds to days
- Cascade through units: years → months → weeks → days
- Pick the two most meaningful units for natural output
- Pluralize correctly (“1 year” vs. “2 years”)
- Handle edge cases: future dates, invalid dates, very recent customers
Usage in a Narration
Once the snippet is created in Cast as CustomerSince, using it is simple:
Thank you for being a customer for {{ CustomerSince }}.
Example outputs:
| Contract Start Date | Today | Output |
|---|---|---|
| 2021-04-17 | 2026-03-23 | 4 years and 11 months |
| 2023-06-15 | 2026-03-23 | 2 years and 9 months |
| 2025-09-01 | 2026-03-23 | 6 months and 3 weeks |
| 2026-01-01 | 2026-03-23 | 2 months and 3 weeks |
| 2026-03-10 | 2026-03-23 | 1 week and 6 days |
| 2026-03-23 | 2026-03-23 | 0 days |
| 2026-12-31 | 2026-03-23 | 283 days in the future |
| (empty) | 2026-03-23 | Invalid Date Value |
The Snippet — Section by Section
Let’s walk through the full snippet, explaining each section. The complete code is at the end for copy-paste.
Section 1: Input and Timestamp Conversion
{% assign dateStart = contract_start_date %}
{% assign dateStart = dateStart | date: '%s' %}
This takes the contract_start_date field and converts it to a Unix timestamp (seconds since 1970). If the date is valid, dateStart becomes a large number like "1618617600". If invalid, it may become "-2208988800" (a sentinel value for blank dates) or an empty string.
Section 2: Validation
{% if dateStart != '-2208988800' and dateStart != '' and dateStart != null %}
This checks three failure cases:
'-2208988800'— the timestamp for December 30, 1899, which many systems produce for blank dates''— an empty stringnull— the field doesn’t exist
Only if the date passes all three checks does the calculation proceed.
Section 3: Calculate Difference in Days
{% assign nowTimestamp = 'now' | date: '%s' %}
{% assign diffSeconds = nowTimestamp | minus: dateStart %}
{% assign diffDays = diffSeconds | divided_by: 3600 | divided_by: 24 %}
- Get today’s timestamp
- Subtract the start date from today (result: seconds of tenure)
- Divide by 3600 (seconds → hours) then by 24 (hours → days)
This gives total days of customer tenure. A negative result means the date is in the future.
Section 4: Cascade Through Units (Years → Months → Weeks → Days)
The snippet uses a cascading if/elsif structure. If the tenure is 365+ days, it starts with years. If 30–364 days, it starts with months. If 7–29 days, weeks. Otherwise, days.
For each starting unit, it calculates the remainder and expresses it in the next smaller unit, giving output like “2 years and 3 months” (two units, not three or four).
Year tier example:
{% if diffDays >= 365 %}
{% assign years = diffDays | divided_by: 365 %}
{% assign yearsRemainder = diffDays | modulo: 365 %}
divided_by: 365 gives whole years. modulo: 365 gives remaining days. The remaining days are then checked: if ≥30, convert to months; if ≥7, convert to weeks; otherwise, report as days.
Section 5: Pluralization
Each unit is wrapped in a capture that handles singular vs. plural:
{% if years > 1 %}{% capture years %}{{ years }} years{% endcapture %}
{% else %}{% capture years %}{{ years }} year{% endcapture %}{% endif %}
This reassigns years from a number (like 2) to a formatted string (like "2 years").
Section 6: Combining Two Units
{% capture timeFrame %}{{ years }} and {{ months }}{% endcapture %}
The and between the two units produces natural output: “2 years and 3 months.”
Section 7: Future Dates and Zero Days
The snippet gracefully handles edge cases:
- Negative
diffDays(future date): outputs “X days in the future” - Zero days: outputs “0 days”
- Very small values: “1 day,” “1 week and 2 days”
Techniques to Study in This Snippet
| Technique | Where You Learned It | How It’s Used Here |
|---|---|---|
| Date to Unix timestamp | Module 7 | date: '%s' converts start date |
| String-to-number | Module 5 | minus:, divided_by:, modulo: on timestamps |
modulo | Module 5 | Calculating remainders (days after years, etc.) |
divided_by (integer) | Module 5 | Converting days to years, months, weeks |
capture for text assembly | Module 15 | Building “2 years” and “2 years and 3 months” |
| Conditional pluralization | Module 15 | “1 year” vs. “2 years” |
Cascading if/elsif | Module 12 | Choosing the right starting unit |
abs | Module 5 | Making negative days positive for future dates |
| Input validation | Module 7 | Checking for sentinel timestamps and nil |
Defensive default | Module 4 | Handling missing contract dates |
Full Snippet Code
{% comment %} Snippet Name: CustomerSince {% endcomment %}
{% comment %} Input: contract_start_date field mapped to dateStart {% endcomment %}
{% comment %} Output: e.g. "1 year and 6 days" {% endcomment %}
{% assign dateStart = contract_start_date %}
{% assign dateStart = dateStart | date: '%s' %}
{% if dateStart != '-2208988800' and dateStart != '' and dateStart != null %}
{% assign nowTimestamp = 'now' | date: '%s' %}
{% assign diffSeconds = nowTimestamp | minus: dateStart %}
{% assign diffDays = diffSeconds | divided_by: 3600 | divided_by: 24 %}
{% if diffDays >= 365 %}
{% assign years = diffDays | divided_by: 365 %}
{% assign yearsRemainder = diffDays | modulo: 365 %}
{% if years > 1 %}{% capture years %}{{ years }} years{% endcapture %}
{% else %}{% capture years %}{{ years }} year{% endcapture %}{% endif %}
{% assign timeFrame = years %}
{% if yearsRemainder >= 30 %}
{% assign months = yearsRemainder | divided_by: 30 %}
{% if months > 1 %}{% capture months %}{{ months }} months{% endcapture %}
{% else %}{% capture months %}{{ months }} month{% endcapture %}{% endif %}
{% capture timeFrame %}{{ years }} and {{ months }}{% endcapture %}
{% elsif yearsRemainder >= 7 %}
{% assign weeks = yearsRemainder | divided_by: 7 %}
{% if weeks > 1 %}{% capture weeks %}{{ weeks }} weeks{% endcapture %}
{% else %}{% capture weeks %}{{ weeks }} week{% endcapture %}{% endif %}
{% capture timeFrame %}{{ years }} and {{ weeks }}{% endcapture %}
{% elsif yearsRemainder > 0 %}
{% assign days = yearsRemainder %}
{% if days > 1 %}{% capture days %}{{ days }} days{% endcapture %}
{% else %}{% capture days %}{{ days }} day{% endcapture %}{% endif %}
{% capture timeFrame %}{{ years }} and {{ days }}{% endcapture %}
{% endif %}
{% elsif diffDays >= 30 %}
{% assign months = diffDays | divided_by: 30 %}
{% assign monthsRemainder = diffDays | modulo: 30 %}
{% if months > 1 %}{% capture months %}{{ months }} months{% endcapture %}
{% else %}{% capture months %}{{ months }} month{% endcapture %}{% endif %}
{% assign timeFrame = months %}
{% if monthsRemainder >= 7 %}
{% assign weeks = monthsRemainder | divided_by: 7 %}
{% if weeks > 1 %}{% capture weeks %}{{ weeks }} weeks{% endcapture %}
{% else %}{% capture weeks %}{{ weeks }} week{% endcapture %}{% endif %}
{% capture timeFrame %}{{ months }} and {{ weeks }}{% endcapture %}
{% elsif monthsRemainder > 0 %}
{% assign days = monthsRemainder %}
{% if days > 1 %}{% capture days %}{{ days }} days{% endcapture %}
{% else %}{% capture days %}{{ days }} day{% endcapture %}{% endif %}
{% capture timeFrame %}{{ months }} and {{ days }}{% endcapture %}
{% endif %}
{% elsif diffDays >= 7 %}
{% assign weeks = diffDays | divided_by: 7 %}
{% assign weeksRemainder = diffDays | modulo: 7 %}
{% if weeks > 1 %}{% capture weeks %}{{ weeks }} weeks{% endcapture %}
{% else %}{% capture weeks %}{{ weeks }} week{% endcapture %}{% endif %}
{% assign timeFrame = weeks %}
{% if weeksRemainder > 0 %}
{% if weeksRemainder > 1 %}{% capture days %}{{ weeksRemainder }} days{% endcapture %}
{% else %}{% capture days %}{{ weeksRemainder }} day{% endcapture %}{% endif %}
{% capture timeFrame %}{{ weeks }} and {{ days }}{% endcapture %}
{% endif %}
{% elsif diffDays >= 0 %}
{% if diffDays > 1 or diffDays == 0 %}
{% capture timeFrame %}{{ diffDays }} days{% endcapture %}
{% else %}
{% capture timeFrame %}{{ diffDays }} day{% endcapture %}
{% endif %}
{% else %}
{% assign days = diffDays | abs %}
{% if days > 1 or days == 0 %}
{% capture timeFrame %}{{ days }} days in the future{% endcapture %}
{% else %}
{% capture timeFrame %}{{ days }} day in the future{% endcapture %}
{% endif %}
{% endif %}
{% else %}
{% assign timeFrame = 'Invalid Date Value' %}
{% endif %}
{{ timeFrame }}
Try It Yourself
Exercise: Without looking at the snippet code, answer these questions:
- What would the snippet output if
contract_start_dateis empty? - What would it output if the contract started exactly 45 days ago?
- Why does the snippet use
divided_by: 3600 | divided_by: 24instead ofdivided_by: 86400?
Click to reveal the answers
1. **Empty date:** The validation check catches it (`dateStart != ''`), and the snippet outputs `"Invalid Date Value"`. 2. **45 days ago:** 45 days is ≥30, so it enters the "months" tier. `45 / 30 = 1` month, `45 % 30 = 15` days remaining. 15 ≥ 7, so `15 / 7 = 2` weeks. Output: `"1 month and 2 weeks"`. 3. **Two divisions vs. one:** Both approaches produce the same mathematical result. Using two steps (`/ 3600 / 24`) is arguably more readable — you can see "seconds to hours, then hours to days" as separate conceptual steps. Using `/ 86400` is more concise. In Liquid's integer division, both yield the same result.What’s Next
In Module 18, you’ll learn the Avatar Name Generator pattern — a clever use of modulo and arrays to produce deterministic, human-sounding names for demos and persona-based presentations.
📖 Official documentation:
- Snippet Library: https://school.cast.app/liquid/liquid-library.html#customer-since-snippet