Introduction

Before you start

This page features many of the most popular calculations, but it barely scratches the surface of what’s possible. Feel free to copy any of these over to your own forms as a starting place, then add to them, combine them, experiment, and reach out to the support team if you have any trouble.

Our cheat sheet is more of a reference page with examples, as opposed to a step-by-step guide. It assumes that you’re already familiar with the following information:

  • The Calculations Guide has all of the info you need about how these fields actually work—concepts like variables, functions, and general syntax are all covered there.
  • The Answer Piping Guide is a good resource as well. If you see anything inside of curly brackets that you don’t recognize (such as {{ id.total }} or {{ id || date d/m/y }}), check out the sections on dot notation and transformations.
  • Your best resource for learning each individual function is actually going to be found in the configuration for a calculation field in the Paperform editor. To learn how a certain function works, search for it under “How to use calculations.”
Screenshot of the "How to use calculations" menu.

Question IDs

Each of these example calculations uses id to represent the actual ID for a relevant question on your form. In your version, you will need to replace any of these with the appropriate question IDs by selecting the question with the + menu to the right.

Animated GIF of the configuration for a calculation. An example calculation is pasted into the window, the text "{{ id }}" is deleted, and then an answer is selected from the + menu to take its place.

If you see any dot notation or a transformation, make sure to include that after the question ID. For example, you might see {{ id.postcode }} in an example calculation. If your question’s ID is 12345, then you would need to use {{ 12345.postcode }}.

If the notation mentions SKU, then a specific product’s SKU must also be inserted. In the example {{ id.quantities.sku }}, your version might end up looking something like {{ 12345.quantities.ABCD }}.

Using this guide

Feel free to skip around using the table of contents to the left, or you can search for specific keywords using the “Find” shortcut in your browser (Ctrl-F on Windows or ⌘-F on Mac). You’ll also find some general tips at the very end.

Basic Calculations

Arithmetic

Simple math is often all you need to get the job done. Though not necessary in every case, NUMBERVALUE can help to ensure that you're working with numbers instead of strings.

length = NUMBERVALUE( {{ id-1 }} );
width = NUMBERVALUE( {{ id-2 }} );
height = NUMBERVALUE( {{ id-3 }} );

length * width * height;

Concatenation

Combining two or more strings of text is called concatenation.

"Hello, " || {{ id }} || "!";

Concatenation can be done with either the shortcut || or the full function. Both are used in this guide, depending on which one is easier to read in context.

CONCATENATE( "Hello, ", {{ id }}, "!" );

Don't forget to account for spaces inside the quoted strings.

Find a matching value

The SWITCH function is useful for a variety of applications. This example checks which location is selected from a Dropdown question, then returns an associated employee's email address. The result could then be piped into the "To" field of a custom email.

SWITCH( {{ id }},
  "Location A", "employee-1@example.com",
  "Location B", "employee-2@example.com",
  "Location C", "employee-3@example.com",
  "fallback@example.com"
);

Count number of answers

This could be used to find the number of choices selected in a Multiple Choice or Dropdown field, or to count the number of files uploaded to a File or Image Upload question.

COUNT( {{ id.array }} );

Wording and formatting

Names

  • Combine first and last name fields into one full name with proper capitalization.

    full_name = {{ first-name-id }} || " " || {{ last-name-id }};
    
    PROPER( full_name );
    
  • Combine required first and last name fields with an optional middle name.

    first_name = {{ first-name-id }};
    middle_name = {{ middle-name-id }};
    last_name = {{ last-name-id }};
    
    full_name = CONCATENATE(
      first_name, " ",
      IF( middle_name != "", middle_name || " ", "" ),
      last_name
    );
    
    PROPER( full_name );
    

Address

The string "\n" can be used to add a line break.

  • Split the address into multiple lines and set the punctuation.

    street = {{ id.street }};
    city = {{ id.suburb }};
    state = {{ id.state }};
    zip = {{ id.postcode }};
    country = {{ id.country }};
    
    CONCATENATE(
      street, "\n",
      city, ", ", state, " ", zip, "\n",
      country
    );
    
  • Automatically abbreviate the state when formatting an address, if the matching state is found (not case-sensitive). If a match isn't found, display the state as it was typed in.

    street = {{ id.street }};
    suburb = {{ id.suburb }};
    state = {{ id.state }};
    postcode = {{ id.postcode }};
    
    stateAbbr = SWITCH( LOWER( state ),
      "new south wales", "NSW",
      "northern territory", "NT",
      "queensland", "QLD",
      "south australia", "SA",
      "tasmania", "TAS",
      "victoria", "VIC",
      "western australia", "WA",
      state
    );
    
    CONCATENATE(
      street, "\n",
      suburb, " ", stateAbbr, " ", postcode
    );
    

Appointments

  • Reformat the result of an Appointment question with the mode set to “Days.”

    Your reservation is from Mon, Feb 13th to Fri, Feb 17th.

    check_in = DATEFORMAT( {{ id.start }}, "ddd, MMM Do" );
    check_out = DATEFORMAT( {{ id.stop }}, "ddd, MMM Do" );
    
    CONCATENATE(
      "Your reservation is from ", check_in, " to ", check_out, "."
    );
    
  • Reformat the result of an Appointment question with the mode set to “Minutes.”

    Your appointment is from 10:00 am to 10:30 am on January 20, 2023.

    date = DATEFORMAT( {{ id.start }}, "MMMM D, YYYY" );
    start = DATEFORMAT( {{ id.start }}, "h:mm a" );
    end = DATEFORMAT( {{ id.stop }}, "h:mm a");
    
    CONCATENATE(
      "Your appointment is from ", start, " to ", end, " on ", date, "."
    );
    

Currency

  • Add a currency symbol and round to two fixed decimal places (e.g. $12.50).

    total = NUMBERVALUE( {{ id }} );
    
    "$" || FIXED( total, 2 );
    
  • Round to a whole number, then add a currency symbol and thousands separator (e.g. $1,234).

    total = NUMBERVALUE( {{ id }} );
    rounded = ROUND( total );
    
    NUMBERFORMAT( rounded, ",", ".", "$", "" );
    
  • Combine a thousands separator with two fixed decimal places (e.g. $1,234.50). This one requires a bit of regex, because FIXED and NUMBERFORMAT don’t play nice with one another.

    total = NUMBERVALUE( {{ id }} );
    rounded = ROUND( total, 2 );
    formatted = NUMBERFORMAT( rounded, ",", ".", "$", "" );
    
    IF(
      REGEXMATCH( formatted, "\\.[0-9]{2}" ),
      formatted,
      IF(
        REGEXMATCH( formatted, "\\.[0-9]{1}"),
        formatted || "0",
        formatted || ".00"
      )
    );
    

Payments

Working with Custom Pricing Rules

Custom Pricing Rules can be used to dynamically alter the total amount charged by the form at checkout.

Screenshot of a Custom Pricing Rule: "When # of Attendees is more than 4 then multiply the answer by 0.8"

You can also combine them with calculations for more complex pricing structures. For example, this rule would add the result of a calculation field to the grand total:

Screenshot of a Custom Pricing Rule: "When Example Calculation Field is answered then + answer"

Please note that if you use Coupons, you may run into issues when combining them with Custom Pricing Rules (or when using them with calculations in general). These features are usually compatible with one another, but it's worth a little extra testing to make sure that all of your fees and/or discounts are being applied in the correct order.

Find total price

When working with prices, sometimes you’ll need to reference the total charged by your form. The value {{ total }} is disabled in calculations, so you will need recalculate it using the individual totals from each Product and Price field. Save the result to a variable, so that you can reference it later.

total = {{ id-1.total }} + {{ id-2.total }} + {{ id-3.total }};

// ...

If you have any preexisting Custom Pricing Rules, these may need to be recreated as well.

services = {{ id-1.total }} + {{ id-2.total }} + {{ id-3.total }};
start_up_fee = IF( {{ id-4 }} == "I am a new customer.", 30, 0 );

total = services + start_up_fee;

// ...

Discount based on quantity

If you'd like to offer discounts at certain quantities, you can start by using a calculation to find out how many products are selected.

  • Find the quantity of a specific product.

    {{ id.quantities.sku }};
    
  • Find the total quantity from multiple fields.

    {{ id-1.quantity }} + {{ id-2.quantity }};
    

In some cases, this will be enough info from the calculation, and your Custom Pricing Rules can take care of the rest. For example, the following rule applies a $25 discount (or 25 of your currency) when the "# Selected" calculation shows that at least 10 products have been ordered:

Screenshot of a Custom Pricing Rule: "When # Selected is more than 9 then x 0.8"

Depending on your pricing structure, you may want to use a calculation to determine the specific amount of the discount instead.

quantity = {{ id.quantity }};

discount_each =
  IF( quantity < 10, 0.00,
    IF( quantity < 20, 1.50,
      IF( quantity < 30, 3.00,
        IF( quantity >= 30, 4.50 )
      )
    )
  )
;

quantity * discount_each;

The tips at the end of this guide include two alternative ways to configure this calculation.

In this case, simply subtract the calculation's answer from the total using your Custom Pricing Rule.

Screenshot of a Custom Pricing Rule: "When [Calculation] is answered then - answer"

Pay now or later

This calculation is a part of our method for optionally accepting payments at a later time. It finds the amount that should still be owed after the form is submitted: the total price if paying later, or 0 if paying now.

total = {{ id-1.total }} + {{ id-2.total }} + {{ id-3.total }};
is_paying_later = {{ multiple-choice-id }} == "Pay later";

IF( isPayingLater, total, 0 );

The result should be subtracted from the form’s current total using a Custom Pricing Rule.

Screenshot of a Custom Pricing Rule: "When [Calculation] is answered then - answer"

This method accomplishes two things. If paying later, the form’s total will be brought down to zero, allowing submission without payment. In that case, the calculation field’s answer (included in the submission results) will also let you know how much the customer still owes.

Charge by appointment length

Used for daily/nightly fees (like a hotel) or hourly fees (like a contractor), these calculations determine the price of a service based on the amount of time selected in an Appointment field.

You can use a Custom Pricing Rule to add the calculation’s result to the total amount charged by the form.

Screenshot of a Custom Pricing Rule: "When [Calculation] is answered then + answer"
  • $50 per hour

    hours = {{ id.minuteLength }} / 60;
    
    50 * hours;
    
  • $60 flat fee + $35 per hour

    hours = {{ id.minuteLength }} / 60;
    
    60 + ( 35 * hours );
    
  • $100 per night

    100 * {{ id.dayLength }};
    
  • $100/night for the first week, then $75/night after that

    nights = {{ id.dayLength }};
    
    short_stay = nights * 100;
    long_stay = 700 + ( ( nights - 7 ) * 75 );
    
    IF( nights > 7, long_stay, short_stay );
    

The notation .dayLength counts the "difference" between dates, so a Monday through Friday would return 4. To include both the first and last day, use {{ id.dayLength }} + 1 instead.

Show selected product's name only

When appearing after submission (e.g. in an email), the piped answer to a Products question includes the name, SKU, price of the selected item. If you only want to display a product's name by itself, you can run the answer through a calculation.

selected = GET( {{ id.selectedProducts }}, 1 );

SWITCH( selected,
  "sku-A", {{ id.names.sku-A }},
  "sku-B", {{ id.names.sku-B }},
  "sku-C", {{ id.names.sku-C }},
  "None"
);

Please note that the above calculation only works if the question only allows a single product to be selected. If "can choose more than one product" is toggled on, then you'll likely want to include more information, like the quantity. In that case, try the order summary below and simplify it as needed.

Custom order summary

If you accept online payments, an itemized summary is automatically shown on the checkout screen. For quote/estimate forms that don’t take payment, or if you'd like to display these details earlier in the form, you may want to create your own order summary.

// --- enter all ID(s) & SKUs ---

SKUs = ARRAY( "sku-A", "sku-B", "sku-C" ); 

names = ARRAY(
  {{ id.names.sku-A }},
  {{ id.names.sku-B }},
  {{ id.names.sku-C }}
);

quantities = ARRAY(
  {{ id.quantities.sku-A }},
  {{ id.quantities.sku-B }},
  {{ id.quantities.sku-C }}
);

prices = MAP(
  ARRAY(
    {{ id.prices.sku-A }},
    {{ id.prices.sku-B }},
    {{ id.prices.sku-C }}
  ),
  "= FIXED( item, 2 )"
);

selected = {{ id.selectedProducts }};

// --- build the summary ---

totals = MAP( prices,
  "= FIXED( NUMBERVALUE( item ) * GET( quantities, index ), 2 )"
);

summary = MAP( selected,
  "= CONCATENATE(
    GET( names, MATCH( SKUs, item ) ),
    ' ($',
    GET( prices, MATCH( SKUs, item ) ),
    ') x ',
    GET( quantities, MATCH( SKUs, item ) ),
    ' = $',
    GET( totals, MATCH( SKUs, item ) )
  )"
);

JOIN( summary, "\n" );

To include information from more than one Products question in the same summary, use the ARRAY function in the selected variable.

selected = ARRAY(
  {{ id-1.selectedProducts }},
  {{ id-2.selectedProducts }},
  {{ id-3.selectedProducts }}
);

Error messages

Overview

When the ERROR function returns a message in a visible calculation field, the pagination and submit buttons will be disabled. There are a couple of different ways to utilize this to add custom error messages to your forms.

Success and error messages

One option is to set up a calculation to display either a success message or an error message, depending on whether a certain condition is met.

date_of_birth = {{ id }};
age = DATEDIFF( NOW(), date_of_birth, "years" );

IF( age >= 18,
  "Please continue.",
  ERROR( "You must be at least 18 to submit this form." )
);

Remember, this calculation field would need to be visible in order for the error to work.

Screenshot of a form, including a question labeled "Date of Birth" with the answer "05/05/2010" entered. The visible calculation field below shows an error message, "You must be at least 18 to submit this form."

Only show error message

Another popular choice is to set a calculation up so that it always returns an error. Then, you can rely entirely on visibility logic to determine when it appears on the form. Using this method, no message appears unless there is an issue.

Screenshot of a calculation field, set to ERROR( "You must be at least 18 to submit." ). The question visibility logic is set such that it only appears when "Date of Birth" is answered and "Age" is less than 18.

If the condition for your error is too complicated to be set up using the basic visibility logic options, consider adding a second calculation, just for the condition itself. Then, refer to the result of that calculation in the visibility logic for the one containing the error message.

In the example above, there would need to be another hidden calculation on the form called "Age," which returns the submitter's age from the Date of Birth question.

date_of_birth = {{ id }};

DATEDIFF( NOW(), date_of_birth, "years" );

If the Date of Birth question is answered, and the result of "Age" is less than 18, then the error message appears.

Validation

Overview

The following calculations return either true or false. They are often used as conditions for error messages, referenced by logic, or saved to a variable as one piece of a larger calculation.

Checking for a specific answer

  • Case-sensitive:

    {{ id }} == "Accepted answer";
    
  • Not case-sensitive:

    LOWER( {{ id }} ) == "accepted answer";
    

Answer must be in approved list

This calculation covers a broad range of use cases—you can use it to check if a postcode matches those in your delivery area, or to find out if an email belongs to one of your employees.

  • Case-sensitive:

    answer = {{ id }};
    accepted = ARRAY( "Accepted 1", "Accepted 2", "Accepted 3" );
    
    INCLUDES( accepted, answer );
    
  • Not case-sensitive:

    answer = LOWER( {{ id }} );
    accepted = ARRAY( "accepted 1", "accepted 2", "accepted 3" );
    
    INCLUDES( accepted, answer );
    

    Note that in the second version, the values in the accepted array are all written in lowercase. The answer given on the form will first be transformed to lowercase, and then it will be checked against your list of accepted, lowercase values.

Answer must match specific criteria

This example checks the postcode property of an Address field to make sure that the answer is in US ZIP code format (a five-digit code between “00001” and “99951”).

zip = {{ id.postcode }};

AND(
  LEN( zip ) == 5,
  REGEXMATCH( zip, "[0-9]{5}" ),
  NUMBERVALUE( zip ) >= 1,
  NUMBERVALUE( zip ) <= 99951
);

If you'd like to validate the answer to a standalone question instead, remove .postcode from the first line. When requesting numeric codes like postcodes or ID numbers, we actually recommend using Text fields (as opposed to Number fields, which automatically remove leading zeroes).

For an alternative without calculations, you could use a custom format for a Phone Number question for different types of numeric codes. If you go that route, you might also want to change the wording for its error message, which is normally "Please enter a valid phone number."

Email

Combine these with custom error messages to disable submission when certain email addresses are entered.

  • Restrict specific email domains. Returns true if forbidden (matching provider found).

    email = LOWER( {{ id }} );
    prohibited = ARRAY( "gmail", "yahoo", "aol" );
    
    provider = REGEXEXTRACT( email, "@([a-zA-Z0-9-]*)\\." );
    
    INCLUDES( prohibited, provider );
    
  • Only allow company domain. Returns true if allowed (matching domain found).

    email = LOWER( {{ id }} );
    
    domain = REGEXEXTRACT( email, "@([a-zA-Z0-9-\\.]*)" );
    
    domain == "paperform.co";
    

Scoring

Overview

Our built-in Scoring feature is mostly used for keeping track of correct and incorrect answers. For more complex scoring systems, you might want to use a calculation instead.

Likert scale quiz

This type of quiz or survey gives weighted points based on answers along a range, like a Likert or semantic differential scale.

If each question's answer choices are worded and scored the same way, then you can tally the scores for all questions at once using this shortcut with MAP:

answers = ARRAY( {{ id-1 }}, {{ id-2 }}, {{ id-3 }} );

scores = MAP( 
  answers,
  "= SWITCH( item,
    'Never', 1,
    'Rarely', 2,
    'Sometimes', 3,
    'Often', 4,
    'Always', 5,
    0
  )"
);

SUMIF( scores );

This type of quiz sometimes includes specific questions with “inverted” scoring. In other words, the highest-scoring choice for some questions could be “Always,” while the best answer for others might be “Never.”

standard_answers = ARRAY( {{ id-1 }}, {{ id-2 }}, {{ id-3 }} );
inverted_answers = ARRAY( {{ id-4 }}, {{ id-5 }}, {{ id-6 }} );

standard_scores = MAP( 
  standard_answers,
  "= SWITCH( item,
    'Never', 1,
    'Rarely', 2,
    'Sometimes', 3,
    'Often', 4,
    'Always', 5,
    0
  )"
);

inverted_scores = MAP( 
  inverted_answers,
  "= SWITCH( item,
    'Never', 5,
    'Rarely', 4,
    'Sometimes', 3,
    'Often', 2,
    'Always', 1,
    0
  )"
);

SUMIF( standard_scores ) + SUMIF( inverted_scores );

Finally, if the answers to each question are worded differently, you would need to split them up into separate SWITCH functions instead.

q1_score = SWITCH( {{ id-1 }},
  "Never", 1,
  "Rarely", 2,
  "Sometimes", 3,
  "Often", 4,
  "Always", 5,
  0
);

q2_score = SWITCH( {{ id-2 }},
  "Strongly Disagree", 1,
  "Disagree", 2,
  "Indifferent", 3,
  "Agree", 4,
  "Strongly agree", 5,
  0
);

q3_score = SWITCH( {{ id-3 }},
  "Poor", 1,
  "Needs improvement", 2,
  "Good", 3,
  "Great", 4,
  "Excellent", 5,
  0
);

q1_score + q2_score + q3_score;

Personality type quiz

This calculation keeps track of a separate score for each possible result (e.g. personality type), then returns the name of the highest-scoring result. This answer can be used for the logic in Dynamic Success Pages to display the page for the matching result after the form is submitted.

Statements like variable == "Answer" resolve to Boolean values (either true or false). When Booleans are used in math, true is treated as 1 and false becomes 0. So, true + true + false would equal 2. This is used to tally the scores for each result.

Make sure that the text of each answer matches the option in the original question exactly. These are case-sensitive and any extra spaces or characters will also affect the score.

// store each selected answer

q1 = {{ id-1 }};
q2 = {{ id-2 }};
q3 = {{ id-3 }};
q4 = {{ id-4 }};
q5 = {{ id-5 }};

// tally the score for each category by matching the answers

result_A =
  ( q1 == "Answer for result A in question 1." ) +
  ( q2 == "Answer for result A in question 2." ) +
  ( q3 == "Answer for result A in question 3." ) +
  ( q4 == "Answer for result A in question 4." ) +
  ( q5 == "Answer for result A in question 5." )
;

result_B =
  ( q1 == "Answer for result B in question 1." ) +
  ( q2 == "Answer for result B in question 2." ) +
  ( q3 == "Answer for result B in question 3." ) +
  ( q4 == "Answer for result B in question 4." ) +
  ( q5 == "Answer for result B in question 5." )
;

result_C =
  ( q1 == "Answer for result C in question 1." ) +
  ( q2 == "Answer for result C in question 2." ) +
  ( q3 == "Answer for result C in question 3." ) +
  ( q4 == "Answer for result C in question 4." ) +
  ( q5 == "Answer for result C in question 5." )
;

// find and return the highest-scoring category

highest_score = MAX( result_A, result_B, result_C );

SWITCH( highest_score,
  result_A, "Result A",
  result_B, "Result B",
  result_C, "Result C",
  "Error"
);

But what if you'd like to make changes to the quiz answers later, without it messing up your calculation? You can score the quiz based on the position (or "index") of each answer, rather than matching the full text.

The dot notation .selectedIndex pulls the index of the selected answer to a multiple-choice question. For example, the index of the second choice in the option list would be the number 2.

This saves you from copying and pasting full answers, as well as making it easier to make changes to the quiz.

// store the index of each answer

q1_index = {{ id-1.selectedIndex }};
q2_index = {{ id-2.selectedIndex }};
q3_index = {{ id-3.selectedIndex }};
q4_index = {{ id-4.selectedIndex }};
q5_index = {{ id-5.selectedIndex }};

// tally the score for each category by matching the answers' indexes

blue =
  ( q1_index == 2 ) +
  ( q2_index == 1 ) +
  ( q3_index == 2 ) +
  ( q4_index == 3 ) +
  ( q5_index == 3 )
;

green =
  ( q1_index == 1 ) +
  ( q2_index == 2 ) +
  ( q3_index == 1 ) +
  ( q4_index == 2 ) +
  ( q5_index == 2 )
;

red =
  ( q1_index == 3 ) +
  ( q2_index == 2 ) +
  ( q3_index == 3 ) +
  ( q4_index == 1 ) +
  ( q5_index == 1 )
;

// find and return the highest-scoring category

highest_score = MAX( blue, green, red );

SWITCH( highest_score,
  blue, "Blue",
  green, "Green",
  red, "Red",
  "Error"
);

For a shortcut, consider setting up the original questions such that the answers are always in the same order. Place all of the answers associated with a certain result first, another result second, and so on. Then, toggle on the "randomize option order" setting for each question.

The index of each answer will correspond to a specific result, but they won't appear in the same order for the quiz taker. This allows you to simplify the calculation:

// store the index of each answer into a single array

answers = ARRAY(
  {{ id-1.selectedIndex }},
  {{ id-2.selectedIndex }},
  {{ id-3.selectedIndex }},
  {{ id-4.selectedIndex }},
  {{ id-5.selectedIndex }}
);

// count the number of times each category's index number appears

blue = COUNTIF( answers, 1 );
green = COUNTIF( answers, 2 );
red = COUNTIF( answers, 3 );

// find and return the highest-scoring category

highest_score = MAX( blue, green, red );

SWITCH( highest_score,
  blue, "Blue",
  green, "Green",
  red, "Red",
  "Error"
);

Assign values based on dropdown options

This calculation leverages options selected by a respondent on the form to assign a given value, providing a value alongside the question title corresponding with the specified option.

For the purposes of this demonstration, it is assumed your form uses Dropdown fields and extracts the selected options using the {{ id.array }} dot notation.

// Define the individual answer arrays for each question
q1 = {{ id-1.array }};
q2 = {{ id-2.array }};
q3 = {{ id-3.array }};

// Initialise the summary array (where we'll keep the results)
final_summary = ARRAY();

// Process question 1
final_summary = PUSH(
    final_summary, 
    ARGS2ARRAY(
        "Question 1",
        MAP(q1, "
            // Compare each index key for `q1` with the pre-determined values
            SWITCH(item,
            'Choice 1', '0',
            'Choice 2', '1',
            'Choice 3', '2',
            '')
        ")
    )
);

// Process question 2
final_summary = PUSH(
    final_summary, 
    ARGS2ARRAY(
        "Question 2",
        MAP(q2, "
            // Compare each index key for `q2` with the pre-determined values
            SWITCH(item,
            'Choice 1', '3',
            'Choice 2', '4',
            'Choice 3', '5',
            '')
        ")
    )
);

// Process question 3
final_summary = PUSH(
    final_summary, 
    ARGS2ARRAY(
        "Question 3",
        MAP(q3, "
            // Compare each index key for `q3` with the pre-determined values
            SWITCH(item,
            'Choice 1', '6',
            'Choice 2', '7',
            'Choice 3', '8',
            '')
        ")
    )
);

JOIN(final_summary, "\n");

Current date and time

The function NOW returns the current date and time. As a form is being filled out, the calculation runs in the browser, and NOW is automatically adjusted for the visitor's timezone setting. However, when the form is actually submitted, the calculation runs again on the server. This causes NOW to always return its final answer in UTC.

This can cause a discrepancy between a calculation's visible result on the live form and in the submission results. For this reason, we recommend using DATEFORMATUTC to normalize NOW to UTC.

current_utc = DATEFORMATUTC( NOW(), "YYYY-MM-DD HH:mm:ss" );
current_pst = DATESUB( current_utc, 8, "hours" );

hour = DATEFORMAT( current_pst, "H" );

IF( NUMBERVALUE( hour ) < 14,
  "Orders placed before 2:00 pm will be available within four hours.",
  "Orders placed after 2:00 pm will be available by 9:00 am on the following day."
);

As you can see in the example above, you can manually add or subtract hours from NOW's UTC result if you'd like to use a specific timezone.

Adjusting this for daylight saving time can be tricky, because in many regions, the dates for DST change every year. One solution is to set yourself up in advance by manually inserting the dates for the next few years or more.

// --- time adjustment ---

utc = DATEFORMATUTC ( NOW(), "YYYY-MM-DD HH:mm:ss" );
pst = DATESUB( utc, 8, "hours" );
pdt = DATESUB( utc, 7, "hours" );

dst = OR(
  AND(
    DATEISAFTER( pst, "2023-03-12 02:00" ),
    DATEISBEFORE( pdt, "2023-11-05 02:00")
  ),
  AND(
    DATEISAFTER( pst, "2024-03-10 02:00" ),
    DATEISBEFORE( pdt, "2024-11-03 02:00")
  ),
  AND(
    DATEISAFTER( pst, "2025-03-09 02:00" ),
    DATEISBEFORE( pdt, "2025-11-02 02:00")
  )
);

current = IF( dst, pdt, pst );

// --- continue calculation ---

hour = DATEFORMAT( current, "H" );

IF( NUMBERVALUE( hour ) < 14,
  "Orders placed before 2:00 pm will be available within four hours.",
  "Orders placed after 2:00 pm will be available by 9:00 am on the following day."
);

If you only need to pipe the submission time into an email or integration, you can use {{ submitted_at }} directly instead of finding the time with a calculation. The timezone shown will depend on your account settings. For a different date format, you can also transform it with {{ submitted_at || date d/m/y }}.

Pseudo-random numbers

Overview

Paperform's calculations don't support "truly" random number generation. However, if you need a bit of randomness, you can get around this by applying some math to the current time, in seconds.

DATEFORMATUTC( NOW(), "X" ) % size_of_set;

The variable size_of_set represents the size of the range of numbers you would like to return, including the number 0. For example, you can enter 5 to return a pseudo-random number between 0 and 4:

DATEFORMATUTC( NOW(), "X" ) % 5;

This can be used in a number of ways.

Select a random item from a list

Let's say you'd like to assign each customer who submits your form to a random employee on your team. You could use this calculation to return one of your agents' emails at random, then pipe the result into the "To" field of a custom email.

agents = ARRAY(
  "agent-1@example.com",
  "agent-2@example.com",
  "agent-3@example.com"
);

position = DATEFORMATUTC( NOW(), "X" ) % COUNT( agents ) + 1;

GET( agents, position );

A/B testing

A simpler application would be a workaround for some basic A/B testing. While we don't fully support built-in A/B testing (e.g. switching between two forms that share a single slug), you can reference this calculation in your conditional logic to randomize some part of it, like a section, page, or outgoing email.

is_even = ISEVEN( DATEFORMATUTC( NOW(), "X" ) );

IF( is_even, "A", "B" );

Final tips

Regex

Some of the examples on this page use regex, which is a type of expression used for text validation in many coding languages. Regex is supported in three Paperform calculation functions: REGEXEXTRACT, REGEXMATCH, and REGEXREPLACE. The internet has some great free resources for learning how to write regex, such as RegexOne and RegExr.

Please note that the escape character for Paperform’s calculations is a double backslash \\. If you’ve used regex in other languages (e.g. Javascript), you might be familiar with a single backslash \, which will not work in this case.

REGEXEXTRACT( {{ id }}, "@([\\w-\\.]*)" );

Testing calculations

The Live Preview found in calculations can help tremendously, but it sometimes thinks that there’s a problem when there isn’t one. One reason for this is that the answers used for the preview don’t always match the format of those that you will receive from your submitters.

To make testing easier, try commenting out any lines that have piped answers and temporarily replacing them with new lines using static values.

// nights = {{ id.dayLength }};
nights = 5;

short_stay = nights * 100;
long_stay = 700 + ( ( nights - 7 ) * 75 );

IF( nights > 7, long_stay, short_stay );

The double forward slash // creates a comment. This tells the calculation to ignore the whole line, but you can still see it for your reference.

Using the above calculation, you would know that the answer in the Live Preview should return the number 500. If the wrong result or an error message appears, this tells you that something might be up with the math or syntax. You can also try out different values for the nights variable this way, then check the Live Preview to make sure the correct answer comes out every time.

When you’re all set, it’s easy to un-comment the original line so that the calculation works as normal again.

nights = {{ id.dayLength }};

short_stay = nights * 100;
long_stay = 700 + ( ( nights - 7 ) * 75 );

IF( nights > 7, long_stay, short_stay );

The next step is to test using the live form. To do this, toggle off “Hide this question” so that the calculation appears on the page. As you change your answers on the live form, you can watch as the result of your calculation changes in real-time. After you’re done testing, you can hide the field again if you like.

You have options!

Lastly, it’s important to remember that there are often many different ways to accomplish the same goal. For example, you could use any of the following calculations to return different discount amounts at different price points:

  1. Nested IF statements.

    quantity = {{ id.quantity }};
    
    discount_each =
      IF( quantity < 10, 0.00,
        IF( quantity < 20, 1.50,
          IF( quantity < 30, 3.00,
            IF( quantity >= 30, 4.50 )
          )
        )
      )
    ;
    
    quantity * discount_each;
    
  2. Replacing a variable.

    quantity = {{ id.quantity }};
    
    discount_each = 0.00;
    discount_each = IF( quantity >= 10, 1.50, discount_each );
    discount_each = IF( quantity >= 20, 3.00, discount_each );
    discount_each = IF( quantity >= 30, 4.50, discount_each );
    
    quantity * discount_each;
    
  3. Using SWITCH and true.

    quantity = {{ id.quantity }};
    
    no_disc = quantity < 10;
    disc_10 = quantity >= 10 and quantity < 20;
    disc_20 = quantity >= 20 and quantity < 30;
    disc_30 = quantity >= 30;
    
    discount_each = SWITCH( true,
      no_disc, 0.00,
      disc_10, 1.50,
      disc_20, 3.00,
      disc_30, 4.50
    );
    
    quantity * discount_each;
    

Each of these solutions would give you the exact same result, so the “best” one is simply the one that makes the most intuitive sense to you.

We do recommend using lots of line breaks, descriptive variable names, and adding comments for internal notes and reminders. Even if you’re the only one who edits the form, making your calculations more readable means that they will be easier to understand and edit when you need to make changes to them later.

Feel free to ask for help

If you’re having trouble with your calculations, or if you just have questions about how anything works, please do feel free to reach out and ask the support team. Rather than providing a complete calculation for you, we’ll work together to find the best method for your use case, then help you understand everything you need to do in order to build the full solution out yourself. By keeping you involved in the process, you’ll be empowered to make changes to the calculation in the future, making the solution scalable and sustainable in the long term.

Support is available via the chat icon in the bottom-right corner of the page, or you can email us at support@paperform.co.