# Is it an Ordered Word?

(inspired by this post over on Puzzling. CAUTION: SPOILERS FOR THAT PUZZLE ARE BELOW.)

The standard telephone keypad correlates letters to numbers as follows:

1 ->
2 -> ABC
3 -> DEF
4 -> GHI
5 -> JKL
6 -> MNO
7 -> PQRS
8 -> TUV
9 -> WXYZ
0 ->


A given input word is defined to be an Ordered Word if, when translated to keypad presses using the above, the resulting number is either non-decreasing or non-increasing. In other words, the resultant number cannot both increase and decrease.

For example, the word CAT translates to 228, which is non-decreasing, and thus an Ordered Word. However, the word DOG is 364, which both increases and decreases, and thus is not an Ordered Word.

## The challenge

Given a word, output whether or not it's Ordered.

## Input

• A word (not necessarily a dictionary word) consisting of ASCII alphabet ([A-Z] or [a-z]) letters only, in any suitable format.
• Your choice if the input is all uppercase or all lowercase, but it must be consistent.
• The word will be at least 3 characters in length.

## Output

A consistent truthy/falsey value for whether the input word is Ordered (truthy) or not Ordered (falsey).

## Rules

• Either a full program or a function are acceptable. If a function, you can return the output rather than printing it.
• If possible, please include a link to an online testing environment so other people can try out your code!
• Standard loopholes are forbidden.
• This is so all usual golfing rules apply, and the shortest code (in bytes) wins.

## Examples

Here are some Ordered Words (i.e., truthy), and there are more on the linked Puzzling puzzle.

CAT
TAC
AAA
DEMONS
SKID
LKJONMSRQP
ABCDEFGHIJKLMNOPQRSTUVWXYZ


Here are some non-Ordered Words (i.e., falsey)

DOG
GOD
ROSE
COFFEE
JKLMNOGHI

• Related and Related I'm not sure this isn't a dupe, the only change between abc->t9 and this challenge is checking monotonicity? – nmjcman101 Jul 31 '17 at 15:49
• @nmjcman101 Yes, those are related, but there could be other (better?) ways than strictly abc->t9. – AdmBorkBork Jul 31 '17 at 15:57
• That makes sense, I do hope to see something beat that method – nmjcman101 Jul 31 '17 at 15:58
• Also slightly related. – totallyhuman Jul 31 '17 at 16:08
• Test case request: AAA – Business Cat Jul 31 '17 at 16:12

# Python 2, 164148132 77 bytes

-16 bytes thanks to Rod's suggestion elsewhere. A frickin' -55 bytes thanks to Arnold Palmer.

n=[min(int((ord(i)-58)/3.13),9)for i in input()]
print sorted(n)in[n,n[::-1]]


Try it online!

Input must be uppercase. Outputs True or False based on its orderedness.

## Explanation

The first line maps each letter to a number.

                               for i in input()   # iterate over the input string
ord(i)                                # take the ASCII ordinal
-58                             # subtract 58
(         )/3.13                       # divide by 3.13
int(                )                      # chop off the fractional part
min(                     ,9)                   # choose the minimum between the number and 9
n=[                                            ]  # assign the resulting list to n


This works based on:

          | A   B   C  | D   E   F  | G   H   I  | J   K   L  | M   N   O  | P   Q   R   S  | T   U   V  | W   X   Y   Z
----------+------------+------------+------------+------------+------------+----------------+------------+-----------------
ord(x)    | 65  66  67 | 68  69  70 | 71  72  73 | 74  75  76 | 77  78  79 | 80  81  82  83 | 84  85  86 | 87  88  89  90
----------+------------+------------+------------+------------+------------+----------------+------------+-----------------
x - 58    | 7   8   9  | 10  11  12 | 13  14  15 | 16  17  18 | 19  20  21 | 22  23  24  25 | 26  27  28 | 29  30  31  32
----------+------------+------------+------------+------------+------------+----------------+------------+-----------------
x ÷ 3.13* | 2.2 2.6 2.9| 3.2 3.5 3.8| 4.2 4.5 4.8| 5.1 5.4 5.8| 6.1 6.4 6.7| 7.0 7.3 7.7 7.9| 8.3 8.6 8.9| 9.3 9.6 9.9 10.2
----------+------------+------------+------------+------------+------------+----------------+------------+-----------------
int(x)    | 2   2   2  | 3   3   3  | 4   4   4  | 5   5   5  | 6   6   6  | 7   7   7   7  | 8   8   8  | 9   9   9   10
----------+------------+------------+------------+------------+------------+----------------+------------+-----------------
min(x, 9) | 2   2   2  | 3   3   3  | 4   4   4  | 5   5   5  | 6   6   6  | 7   7   7   7  | 8   8   8  | 9   9   9   9


*Values are rounded. :P

The second line outputs if the list of numbers is in ascending or descending order.

print                                             # print whether...
sorted(n)                                   # n sorted...
in[n,n[::-1]]                      # is equivalent to n or n reversed

• 84 bytes – Arnold Palmer Jul 31 '17 at 16:44
• Holy cow, that's awesome. Thanks! – totallyhuman Jul 31 '17 at 16:45
• I was going to post it as an answer cause I wrote it before everyone flooded in with answers, but you sniped me :) – Arnold Palmer Jul 31 '17 at 16:47

## JavaScript (ES6),  83 ... 71  70 bytes

Returns a boolean.

x=>[...x].every(c=>v&=~(k=x,x=parseInt(c,35)*.32|0||10,x<k?2:x>k),v=3)


### Test cases

let f =

x=>[...x].every(c=>v&=~(k=x,x=parseInt(c,35)*.32|0||10,x<k?2:x>k),v=3)

test = a => a.forEach(s => console.log(f(s) + " for '" + s + "'"))

console.log('[Truthy]')
test(["AAA", "CAT", "TAC", "DEMONS", "SKID", "LKJONMSRQP", "ABCDEFGHIJKLMNOPQRSTUVWXYZ"])

console.log('[Falsy]')
test(["DOG", "GOD", "ROSE", "COFFEE", "JKLMNOGHI"])

### How?

Letter conversion

We use parseInt(c, 35) to convert each letter of the input string to some number in [10 .. 34]. Because it's base-35, "Z" is converted to NaN instead.

The expression * .32 | 0 maps this number into the interval [3 .. 10], leading to 8 correct groups of letters for "A" to "Y". We need || 10 to get the correct value for "Z".

           | A  B  C| D  E  F| G  H  I| J  K  L| M  N  O| P  Q  R  S| T  U  V| W  X  Y   Z
-----------+--------+--------+--------+--------+--------+-----------+--------+------------
parseInt   |10 11 12|13 14 15|16 17 18|19 20 21|22 23 24|25 26 27 28|29 30 31|32 33 34 NaN
-----------+--------+--------+--------+--------+--------+-----------+--------+------------
*.32|0||10 | 3  3  3| 4  4  4| 5  5  5| 6  6  6| 7  7  7| 8  8  8  8| 9  9  9|10 10 10  10


Order test

We keep track of the signs of differences between consecutive numbers into the bitmask v, initially set to 3 (0b11):

• bit #0: cleared when new_value > previous_value
• bit #1: cleared when new_value < previous_value

The previous value is stored in the same variable x as the input. This ensures that the first iteration -- where no previous value actually exists -- will not clear any bit, because a string containing only letters is neither greater nor less than any number:

('CAT' > 5) === false
('CAT' < 5) === false


A word is ordered unless both signs are encountered, which leads to v = 0 and makes every() fail.

• Oh, nice trick to get the number of each letter :) I'm torn as to whether I should borrow it or not, as it would mean I'd tie with you, which doesn't seem right. – Shaggy Jul 31 '17 at 16:44

# Jelly, 28, 27, 25, 23, 22, 21, 19, 18 bytes

_>
O‘ç82ç88:3IṠḟ0E


Try it online!

This was a lot of fun to write!

Explanation:

                # Define a helper link, decrement a if a > b
_               # Subtract
>              # Boolean greater than
O               # The ordinals (ASCII points) of the input
‘              # Minus one
ç82           # Decrement if greater than 82
ç88        # Decrement if greater than 88
:3      # Divide each number by 3
I     # Consecutive differences
Ṡ    # Sign (-1 if < 0, 0 if == 0, and 1 if > 0)
ḟ0  # Remove all 0's
E # All elements are equal?


Thanks to @ErikTheOutgolfer, @leakynun and @BusinessCat for all saving bytes. :)

# 05AB1E, 36 bytes

v.•1нJ©½èÇHø¹á₂N¸°…ÈáÀ•#Dʒyå}k}¥0‹Ë


Try it online!

• You can replace .•1нJ©½èÇHø¹á₂N¸°…ÈáÀ•# by A•22ā₂•S£. – Adnan Jul 31 '17 at 19:22
• @Adnan rip formatting – Leaky Nun Jul 31 '17 at 19:22
• @LeakyNun <s>rip indeed</s> fixed – Adnan Jul 31 '17 at 19:22
• @Adnan rip formatting – Leaky Nun Jul 31 '17 at 19:23
• @LeakyNun rip indeed – Adnan Jul 31 '17 at 19:24

# MATL, 26 25 bytes

1Y21K250B-Y{c&m8\dZSu|s2<


Input is in upper-case letters. Output is 1 or 0.

Try it online!

### Explanation

1Y2      % Push 'ABC...XYZ'
1        % Push 1
K        % Push 4
250B     % Push 250 in binary, that is, [1 1 1 1 1 0 1 0]
-        % Subtract (from 4, element-wise): gives [3 3 3 3 3 4 1 4]
Y{       % Convert to cell array, splitting into chunks of those lengths
c        % Convert to char matrix. Gives a 4-column matrix. Chunks of length 3
% are right-padded with a space
&m       % Implicit input. Push (linear) index of membership in char matrix
8\       % Modulo 8. Converts linear index into 0-based row index
d        % Consecutive differences
ZS       % Sign
u        % Unique
|        % Absolute value
s        % Sum
2<       % Less than 2? Implicit display

• Appropriate score for an alphabetic challenge :P – James Jul 31 '17 at 16:33
• @DJMcMayhem Not anymore :-D – Luis Mendo Jul 31 '17 at 16:37

## Husk, 22 21 19 18 bytes

±S€Ẋ▲Ṫo±≤"DGJMPTW


Returns 1 for truthy inputs, 0 for falsy ones. Inputs must be in uppercase. Passes all test cases. Try it online!

## Explanation

±S€Ẋ▲Ṫo±≤"DGJMPTW  Implicit input x, e.g. "CAT"
Ṫo±≤"DGJMPTW  This part transforms x into a "canonical form" corresponding to the numpad digits
Ṫ             Table with flipped arguments
o±≤          on sign of less-than-or-equal
(In Husk, ≤ returns extra information we don't want, so we take sign of the result to get 0 or 1.)
"DGJMPTW  of this string and x.
This gives, for each char in x, a bit array of comparisons with the chars in the string:
y = [[0,0,0,0,0,0,0],[0,0,0,0,0,0,0],[1,1,1,1,1,1,0]]
Ẋ▲               Maxima of adjacent pairs: [[0,0,0,0,0,0,0],[1,1,1,1,1,1,0]]
S€                 1-based index in y as sublist: 2
±                   Sign: 1


# Python 2, 60 bytes

a=[3681/ord(c)for c in input()]
print sorted(a)in[a,a[::-1]]


Try it online!

Accepts input in lowercase.

### How it works

⌊3681/x⌋ decreases from

• 38 to 37 at x ≈ 96.8684210526, before a;
• 37 to 36 at x ≈ 99.4864864865, between c and d;
• 36 to 35 at x ≈ 102.25, between f and g;
• 35 to 34 at x ≈ 105.171428571, between i and j;
• 34 to 33 at x ≈ 108.264705882, between l and m;
• 33 to 32 at x ≈ 111.545454545, between o and p;
• 32 to 31 at x ≈ 115.03125, between s and t;
• 31 to 30 at x ≈ 118.741935484, between v and w;
• 30 to 29 at x ≈ 122.7, after z.

## C++, 375199195 194 bytes

Thanks to Shaggy's JavaScript answer :
-5 bytes thanks to Zacharý

#include<string>
int o(std::string a){std::string m="22233344455566677778889999";for(auto&b:a)b=m[b-65];int j=1,i=0,d=0;for(;j<a.size();++j){if(a[j]>a[j-1])++i;if(a[j]<a[j-1])++d;}return!(i*d);}

• Can you move the int j=1,i=0,d=0 to the for loop? – Zacharý Jul 31 '17 at 22:57
• @Zacharý Since i and d are used outside of the loop block, i can't – HatsuPointerKun Jul 31 '17 at 23:17
• i==0||d==0 ==> i*d==0. – Zacharý Jul 31 '17 at 23:20
• Would !(i*d) work? (removing the space after return) – Zacharý Jul 31 '17 at 23:25
• @Zacharý Yes, it works – HatsuPointerKun Jul 31 '17 at 23:28

# 05AB1E, 30 bytes

A3 8×Ƶ0+S£¹δåā>‚øε*}.«+¥0K0‹Ë


Try it online!

-1 thanks to Magic Octopus Urn.

• Did you use ¥0K0.SË because ¥0‹Ë isn't correct? I can't tell if the 0.S is needed. – Magic Octopus Urn Jul 31 '17 at 15:52
• @MagicOctopusUrn In fact ¥0K0‹Ë seems to work. – Erik the Outgolfer Jul 31 '17 at 15:54
• Yeah, if you're removing the 0's it should; in my answer I'm not sure it works. – Magic Octopus Urn Jul 31 '17 at 15:55
• @MagicOctopusUrn I'm removing the 0s since otherwise there will be false negatives. Your answer might behave differently though. – Erik the Outgolfer Jul 31 '17 at 15:55

# Retina, 65 bytes

T_ADGJMPTWd
}TL_L
(.)\1*
$1$*1<
(1+)<(?!\1)
$1> 1 ^(<*|>*)>$


Try it online! Link includes test cases. Explanation:

T_ADGJMPTWd


Change the first letter on each key to a digit. (This is off by 1 but that doesn't matter for an ascending/descending check. On the other hand, zeros would make my life more difficult, so I left one filler character in.)

}TL_L


Shuffle all the remaining letters up 1 and repeat until they've all been converted to digits.

(.)\1*
$1$*1<


Convert the digits into unary, but only once per run of identical digits. The unary values are seprated with a <...

(1+)<(?!\1)
$1>  ... but if the LHS turns out to be greater than the RHS, correct the < to >. 1  Delete the 1s which are no longer necessary. ^(<*|>*)>$


Check that the word is ordered. (The trailing > comes from the last digit which always compares greater than the empty space following it.)

• That's pretty cool. Thanks for the thorough explanation. – AdmBorkBork Jul 31 '17 at 20:06

# Pyth, 23 bytes

One of my first non-trivial Pyth answer! Saved 6 bytes thanks to @LeakyNun. The initial solution is below.

/{_BKmhS,9/a58Cd3.13zSK


Test Suite.

# Pyth, 29 bytes

KmhS+9]/-Cd58 3.13w|qKSKqK_SK


Test Suite.

# Explanation

/{_BKmhS,9/a58Cd3.13zSKQ - The Q means evaluated input and is implicit at the end

{                      - Deduplicate
_                     - Reverse
B                    - Bifurcate, Create a two-element list, [B, A(B)]
K                   - Variable with auto-assignement to:
m              z    - Map over the input:
hS                  - Minimum (first element of sorted list)
,                 - Create a two-element list, [A, B] with these elements:
9                  - The numeric literal 9
/                 - The integer division of:
a58Cd              - The absolute difference between 58 and ord(current_element)
3.13          - The numeric literal 3.13
SK   - K sorted
/                     Q  - Count the occurrences of the input in [K, K[::-1]]


# 05AB1E, 21 17 bytes

### Code

A•22ā₂•Sās×J‡Ô¥dË


Uses the 05AB1E encoding.

### Explanation

A                   # Push the lowercase alphabet
•22ā₂•             # Push the number 33333434
S            # Split into an array
ā           # Get the range of indices [1 .. length]
s×         # Swap and string multiply
J        # Join that array
‡       # Transliterate


This now essentially maps the following letters to the following numbers:

abcdefghijklmnopqrstuvwxyz
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
11122233344455566667778888


             Ô      # Remove consecutive duplicates
¥     # Compute the delta's of the list
d    # Check if the number is greater or equal to 0
Ë   # Check if all elements are the same


# JavaScript (ES6), 10797959288 85 bytes

Works with mixed-case strings. Returns 1 for truthy or 0 for falsey.

s=>(s=(a=[...s].map(c=>(parseInt(c,36)-3)/3.13%10|0||9))+"")==a.sort()|s==a.reverse()

• 10 bytes saved thanks to Rod.

## Try It

o.innerText=(f=
s=>(s=(a=[...s].map(c=>(parseInt(c,36)-3)/3.13%10|0||9))+"")==a.sort()|s==a.reverse()
)(i.value="Cat")
oninput=_=>o.innerText=f(i.value)
<input id=i><pre id=o>

• Math.min((parseInt(c,36)-3)/3.13|0,9) instead "2..9"[parseInt(c,36)-10] to save some bytes – Rod Jul 31 '17 at 16:26
• Thanks, @Rod; very nice. I'll have to file that away for future use. – Shaggy Jul 31 '17 at 16:28
• – user72349 Jul 31 '17 at 18:38
• Thanks, @ThePirateBay, but,sadly, that fails for input AAA. – Shaggy Aug 1 '17 at 8:05

# Gaia, 292725 17 bytes

ċ⟨):“QX’>¦Σ⁻3/⟩¦o


Try it online!

### Explanation

ċ                  Turn the input into a list of code points
⟨            ⟩¦   Map this block to each code point:
)                 Increment it
:                Copy it
“QX’            Push [81 88]
>¦          Check if the code point is greater than each of [81 88]
Σ         Sum the results
⁻        Subtract from the code point
3/      Integer divide the result by 3
o  Check if the resulting list is in sorted order (increasing or decreasing)


# 05AB1E, 13 bytes

ÇÍžq÷8
7:Ô¥dË


Whenever I see a numpad question, I have to make a pi-based answer.

Ç                    # ASCII code of each letter in the input
Í                   # add 2
žq÷                # divide by pi
8 7:            # replace 8 with 7
Ô           # remove duplicates
¥          # deltas
d         # each >= 0 ?
Ë        # are all elements equal?


# Jelly, 32 bytes

“¢Ç⁺H’D0ẋj1œṗØA⁸=€×"€9Ḋ¤Fḟ0Iḟ0ṠE


Try it online!

• RIP compression – Leaky Nun Jul 31 '17 at 19:22
• @LeakyNun The compressed number is actually 32222323 so it saves just 2 bytes. – Erik the Outgolfer Jul 31 '17 at 19:26

# C# (.NET Core), 133 bytes

using System.Linq;q=>{var u=q.Select(c=>(int)((c-58)/3.13));var z=u.Zip(u.Skip(1),(a,b)=>a-b);return!(z.Any(d=>d<0)&z.Any(d=>d>0));};


Try it online!

I feel like there's some room to save, but C# is not a concise language so maybe not. Ungolfed:

bool Ordered(string word){

IEnumerable<int> keys = word.Select(character => (int)((character - 58)/3.13));
// convert characters to keypad number

IEnumerable<int> differences = keys.Zip(keys.Skip(1), (a, b)=> a-b);
// difference between consecutive elements

return !(differences.Any(diff => diff<0) & differences.Any(diff => diff>0));
// false if both positive and negative differences exist
}


In particular I think there's a shorter way to express the final check for validity, possibly a way to inline it with the Zip. Finding a way to express the Zip without needing temporary storage for the Skip would also save something, but I doubt there's something more concise for that.

# Python 3, 143147148149 130 bytes

def g(s,f=lambda c:min(int((ord(c)-58)/3.13),9)):x=[f(a)-f(b)for a,b in zip(s,s[1:])];return any(t<0for t in x)*any(t>0for t in x)


Best I can do for now. Crude function turns the letter into the number based off of ascii code. There are definitely some improvements to be made. 0 is truthy, 1 is falsey (sorry). Saved 10 bytes thanks to Rod, another 3 thanks to Mr. Xcoder.

Try it online!

• you can use x=[f(a)-f(b)for a,b in zip(s,s[1:])] to save some bytes – Rod Jul 31 '17 at 16:33
• also, min(int((ord(c)-58)/3.13),9) is a shorter way to convert the char – Rod Jul 31 '17 at 16:34
• 140 bytes – Mr. Xcoder Jul 31 '17 at 16:38
• @Rod Thanks! Very helpful. – C McAvoy Jul 31 '17 at 16:38
• You'll need to swap your outputs around in order for this to be valid. – Shaggy Jul 31 '17 at 22:24

# Python 2, 111 103 bytes

-8 bytes thanks to @Arnold Palmer:no lower() needed

• Takes uppercase letters as input.
lambda x:sorted(f(x))in[f(x),f(x)[::-1]]
f=lambda x:['22233344455566677778889999'[ord(i)-65]for i in x]


Try it online!

• You can remove the .lower() since the input can be in whatever case you specify. – Arnold Palmer Jul 31 '17 at 19:28

# PHP 7, 98+1 95+1 84+1 bytes

a golfed port of Arnauld´s answer.

for(;$c=ord($argn[$i]);$v|=$i++?$p>$k?2:$p<$k:0,$p=$k)$k=($c-58)*.32%10?:9;echo$v<3;


accepts uppercase; empty output for falsy, 1 for truthy.

Run as pipe with -nR or try it online.

original post:

for(;$c=$argn[$i++];$p?$a[]=$p<=>$k:0,$p=$k)$k=(ord($c)-58)/3.13-($c>Y)|0;echo min($a)*max($a)>=0;


# CJam, 373130 27 bytes

q{_"SVZY"#g-i3/}%_$_W%](e=g  Try it Online Of course the ugly version ends up being shorter... q{ e# For each character in string... _"SVZY"#g e# Get index of that character in "SVZY". Signum that. (returns 1 or 0 if inside string, -1 if not.) -i3/ e# Subtract value from character (i.e 'Z' becomes 'Y', 'F' becomes 'G'). Convert to int. Integer divide by 3. (this is just how the math works out for proper mapping of characters to phone digits.) }% _$_W%]    e# Put mapped string, sorted version, and reverse sorted version in array.
(         e# Pop mapped string from array onto stack.
e=        e# Count occurences of mapped string in array.
g         e# Signum.


# C (gcc), 183 169 153 117 bytes

#define a(b)(int)fmin(*(b c)/3.2,27)
d(char*c){int r=1,p=1;for(;1<strlen(c);)r&=a()<=a(1+),p&=a()>=a(++);return r|p;}


Try it online!

Old solution:

#define a(b)(int)((int)(b*.32-17.6)*.9)
d(char*c){int l=~-strlen(c),i=0,r=1,p=1;for(;i<l;++i)r&=a(c[i])<=a(c[i+1]),p&=a(c[l-i])<=a(c[l-i-1]);return r+p;}


Saved 8 bytes thanks to ThePirateBay.

Old old solution:

d(char*c){char*a="22233344455566677778889999";int l=strlen(c)-1,i=0,r=1,p=1;for(;i<l;++i){if(a[c[i]-65]>a[c[i+1]-65])r=0;if(a[c[l-i]-65]>a[c[l-i-1]-65])p=0;}return r+p;}


Old old old solution:

d(char*c){char*a="22233344455566677778889999";int l=strlen(c);int i,r=1,p=1;for(;i<l-1;++i)if(a[c[i]-65]>a[c[i+1]-65])r=0;for(i=l-1;i>0;--i)if(a[c[i]-65]>a[c[i-1]-65])p=0;return r+p;}


# TI-Basic, 92 66 bytes

ΔList(int(seq(inString("BC DEF GHI JKL MNO PQRSTUV WXYZ",sub(Ans,I,1))/4,I,1,length(Ans
0≤min(Ansmax(Ans


Converts each character in the string to an integer from 0 to 7, and takes the difference between each consecutive element; then checks whether the minimum and maximum differences have the same sign (or either is 0).

• I think ΔList(int(4^-1seq(inString("DEF GHI JKL MNO PQRSTUV WXYZ",sub(Ans,I,1))+3,I,1,length(Ans saves one byte. – lirtosiast Aug 25 '17 at 0:17

# Zsh, 73 69 57 bytes

-12 bytes by using @anders-kaseorg's 3681/code conversion.

for c (${(s::)1})((y=3681/#c,A|=y<p,D|=p&&p<y,p=y,!D|!A))  A few things we abuse: • ((statement,statement,...)) is a sequence of arithmetic expressions which returns truthy if the last statement is non-zero. • The return value of a loop is the return value of the last statement in a loop. • Arithmetic operator precedences were pretty nice to us, as only one pair of no parentheses were used. One byte could be saved if ! bound less tightly than &. • Unset parameters expand to 0 in arithmetic expansions. • The function we use to map to the keypad number is CODE / 3.2 - 18 (with a special case for Z), but since we only need the change between codes, we don't do the linear adjustment. for c (${(s::)1})           # split into characters
(( y = #c-90 ? 0^(#c/3.2) : 27,   # this sets y to the keypad number + 18
A |= y < p,          # set the A flag if we detect it is not ascending
D |= p && p < y,     # ditto descending, don't compare on first iteration
p = y,               # save the current character
!D | !A              # return false if D and A are both set
))
`

2 bytes can be saved if the truthy/falsey values can be swapped.