| Bytes | Lang | Time | Link |
|---|---|---|---|
| 062 | JavaScript V8 | 250731T104426Z | user3048 |
| 048 | ARM64 machine code | 250714T021445Z | Nate Eld |
| nan | Turing Machine Code | 250714T222737Z | ErikDaPa |
| 073 | Swift 6 | 250710T180457Z | macOSist |
| 032 | Japt | 250609T163353Z | Shaggy |
| nan | CP1610 machine code | 250617T091851Z | Arnauld |
| 033 | Opcode x64 | 250617T102350Z | l4m2 |
| 072 | Python 3 | 250611T103332Z | user3048 |
| 063 | C gcc | 250611T083747Z | user3048 |
| 646 | VBA | 250610T182429Z | mc-gwidd |
| 058 | GNU sed e | 250610T162142Z | Toby Spe |
| nan | Commodore 64 Assembler | 250608T195454Z | Jani Joe |
| 023 | 05AB1E | 250610T082203Z | Kevin Cr |
| 104 | C64 Basic | 250609T155843Z | OSI8 |
| 107 | Google Sheets | 250608T123334Z | doubleun |
| 052 | Ruby pl | 250608T011347Z | Value In |
| 061 | Perl 5 F | 250608T211150Z | Xcali |
| 032 | Vyxal 3 | 250608T200159Z | pacman25 |
| 067 | R | 250608T071632Z | pajonk |
| 093 | APL+WIN | 250608T150501Z | Graham |
| 024 | Jelly | 250608T173039Z | Jonathan |
| 047 | Retina 0.8.2 | 250607T234926Z | Neil |
| 062 | JavaScript Node.js | 250608T010034Z | l4m2 |
| 029 | Charcoal | 250608T065330Z | Neil |
| 071 | JavaScript ES6 | 250607T235700Z | Arnauld |
| 163 | TinyAPL | 250607T235629Z | RubenVer |
| 158 | Google Sheets / Excel | 250607T233623Z | z.. |
JavaScript (V8), 62 bytes
JavaScript anonymous function that performs the specified operation using string substitution and regular expressions. Should be engine-agnostic. Strings of appropriate length are identified by the regular expression based on the first matched character and subsequently replaced by the first character of the matched string. Minor golfing trick in the expression by adding a superfluous O to the last class so that NOVEMBER can be matched with the corresponding sub-expression.
s=>s.replace(/([BDHIORT]|[QSVY].|N?[CFJOUW]..)?..../g,t=>t[0])
ARM64 machine code, 48 bytes
Takes pointer to null-terminated input in x0, pointer to output buffer (which must be large enough) in x1, following standard AAPCS calling conventions for
void nato(const char *in, char *out);.
Little-endian hex words:
58000143
38404402
38001422
d37ff844
9ac42065
8b45f800
7101385f
9a801400
35ffff22
d65f03c0
267b2000
074c5c0d
Assembly:
nato:
ldr x3, table
again:
ldrb w2, [x0], #4 // skip 4 bytes automatically
strb w2, [x1], #1
lsl x4, x2, #1 // 2 bits per table entry
lslv x5, x3, x4
add x0, x0, x5, lsr #62
cmp w2, #'N'
cinc x0, x0, eq
cbnz w2, again
ret
table:
.xword 0x074c5c0d267b2000
Approach is similar to x86 solution by l4m2 (but I thought of it mostly independently). We use a 2-bit lookup table in a register to find the length of the mnemonic minus 4. There is special code to handle N.
Comments:
The post-increment load lets us skip the first 4 bytes for free.
lslvautomatically masks the shift count to 6 bits, so we can simply double the input byte and use it directly as a shift count.We left-shift the desired bits to the top bits (
lslv x5, x3, x4) so that the subsequentaddcan use a free right-shift to get it back to the low bits. If we had initially right-shifted the desired bits to the bottom bits, we'd need an extra instruction to mask off the others.I couldn't think of a more clever way to handle
N, so the table has its length as 7, and we spend two instructions (8 bytes) to conditionally increment the pointer if the byte wasN.I only intended to handle upper-case input, but I realized that it automatically handles lower-case as well, since it only looks at the low 5 bits of each byte.
Turing Machine Code, 34 / 121 rules
0 A * r 3
0 B * r 4
0 C * r 6
0 D * r 4
0 E * r 3
0 F * r 6
0 G * r 3
0 H * r 4
0 I * r 4
0 J * r 6
0 K * r 3
0 L * r 3
0 M * r 3
0 N * r 7
0 O * r 4
0 P * r 3
0 Q * r 5
0 R * r 4
0 S * r 5
0 T * r 4
0 U * r 6
0 V * r 5
0 W * r 6
0 X * r 3
0 Y * r 5
0 Z * r 3
0 _ * * halt
7 * _ r 6
6 * _ r 5
5 * _ r 4
4 * _ r 3
3 * _ r 2
2 * _ r 1
1 * _ r 0
Note: This program replaces unnecessary letters with spaces instead of removing them. Therefore, you will see spaces in between letters.
Second longer program:
0 A * r 3 ; # of letters to delete ahead ↓
0 B * r 4
0 C * r 6
0 D * r 4
0 E * r 3
0 F * r 6
0 G * r 3
0 H * r 4
0 I * r 4
0 J * r 6
0 K * r 3
0 L * r 3
0 M * r 3
0 N * r 7
0 O * r 4
0 P * r 3
0 Q * r 5
0 R * r 4
0 S * r 5
0 T * r 4
0 U * r 6
0 V * r 5
0 W * r 6
0 X * r 3
0 Y * r 5
0 Z * r 3
0 _ * l RW ; now to the deleting spaces part
7 * x r 6 ; deleting letters
6 * x r 5
5 * x r 4
4 * x r 3
3 * x r 2
2 * x r 1
1 * x r 0
RW x _ l RW ; delete trailing spaces
RW * * r ML ; when reached letter, shift to the right
ML * * l ML
ML x 0 l MV ; until reaching first x from the right, mark a 0
MV x x l MV ; now search for another letter ↓
MV A x r A
MV B x r B
MV C x r C
MV D x r D
MV E x r E
MV F x r F
MV G x r G
MV H x r H
MV I x r I
MV J x r J
MV K x r K
MV L x r L
MV M x r M
MV N x r N
MV O x r O
MV P x r P
MV Q x r Q
MV R x r R
MV S x r S
MV T x r T
MV U x r U
MV V x r V
MV W x r W
MV X x r X
MV Y x r Y
MV Z x r Z
MV _ _ r CL
A x x r A ; replacing 0 with that letter ↓
A 0 A r ML
B x x r B
B 0 B r ML
C x x r C
C 0 C r ML
D x x r D
D 0 D r ML
E x x r E
E 0 E r ML
F x x r F
F 0 F r ML
G x x r G
G 0 G r ML
H x x r H
H 0 H r ML
I x x r I
I 0 I r ML
J x x r J
J 0 J r ML
K x x r K
K 0 K r ML
L x x r L
L 0 L r ML
M x x r M
M 0 M r ML
N x x r N
N 0 N r ML
O x x r O
O 0 O r ML
P x x r P
P 0 P r ML
Q x x r Q
Q 0 Q r ML
R x x r R
R 0 R r ML
S x x r S
S 0 S r ML
T x x r T
T 0 T r ML
U x x r U
U 0 U r ML
V x x r V
V 0 V r ML
W x x r W
W 0 W r ML
X x x r X
X 0 X r ML
Y x x r Y
Y 0 Y r ML
Z x x r Z
Z 0 Z r ML
CL x _ r CL ; now just clear unnecessary prefix
CL 0 _ r CL
CL * * * halt
Note: This one removes the extra spaces instead, but however it is longer. Probably need some optimizations.
Desmos, 210 bytes
Input is a list of codepoints.
L=I-64 => convert into idx in the alphabet
S=[4,5,7,5,4,7,4,5,5,7,4,4,4,8,5,4,6,5,6,5,7,6,7,4,6,4] => length of the phonetic spelling of each letter
s=S[L][x]
G(L,x)=[G(L,x-1)-1,s][2-sign(G(L,x-1)-1)] => create decreasing idx from each head letter
G(L,1)=s
H(L,x)=\left\{x=1,G(L,x-1)=1,0\right\} => mark idx of head letters
A=[L[x]H(L,x)forx=[1...L.count]]
f(I)=A[A>0]+64 => keep head letters and convert back to letters (as codepoints)
Swift 6, 73 bytes
{($0+"").replacing(/(?=(.))([BDHIORT]|[QSVY].|N?[CFJOUW]..)?..../){$0.1}}
A direct port of the Retina answer.
Japt, 32 bytes
Adaptation of Arnauld's solution.
©Î+ßUs`35c‹9Ÿt …nwbd¦`¸mn33 ¬gUc
©Î+ßUs`...`¸mn33 ¬gUc :Implicit input of string U
© :Logical AND with
Î : First character of U
+ : Append
ß : Recursive call with argument
Us : Slice U from index
`...` : Compressed string "35cim9rat atnwbdli"
¸ : Split on spaces
m : Map
n33 : Convert from base 33
¬ : Join
g : Get character at 0-based index
Uc : Codepoint of first character of U
CP-1610 machine code, 34 DECLEs1=42.5 bytes
1. CP-1610 instructions are encoded with 10-bit values (0x000 to 0x3FF), known as DECLEs. Although the Intellivision is also able to work on 16-bit data, programs were really stored in 10-bit ROM back then.
A routine that takes the address of a NUL-terminated string in R3 and writes another NUL-terminated string at the address in R4.
Source code
ROMW 10 ; use 10-bit ROM
ORG $4800 ; map our program at $4800
PRINT EQU $1867 ; PRINT routine from the EXEC
BT EQU $200 ; BACKTAB address
OUT EQU $300 ; output address
;; --------------------------------------------------------- ;;
;; main code ;;
;; --------------------------------------------------------- ;;
4800 001 SDBD ; set up an interrupt service routine
4801 2B8 068 048 MVII #isr, R0 ; to do some minimal STIC initialization
4804 240 100 MVO R0, $100
4806 040 SWAP R0
4807 240 101 MVO R0, $101
4809 002 EIS ; enable interrupts
480A 001 SDBD ; test #1
480B 2BB 02F 048 MVII #test1, R3
480E 2BC 300 MVII #OUT, R4
4810 004 148 076 CALL nato
4813 2B9 300 MVII #OUT, R1 ; print the result
4815 2BB 007 MVII #7, R3
4817 2BC 200 MVII #BT, R4
4819 004 118 067 CALL PRINT
481C 001 SDBD ; test #2
481D 2BB 046 048 MVII #test2, R3
4820 2BC 300 MVII #OUT, R4
4822 004 148 076 CALL nato
4825 2B9 300 MVII #OUT, R1 ; print the result
4827 2BB 007 MVII #7, R3
4829 2BC 214 MVII #BT+20, R4
482B 004 118 067 CALL PRINT
482E 017 DECR R7 ; spin forever
482F 048 04F ... test1 STRING "HOTELECHOLIMALIMAOSCAR", 0
4846 051 055 ... test2 STRING "QUEBECUNIFORMECHOBRAVOECHOCHARLIE", 0
;; --------------------------------------------------------- ;;
;; ISR ;;
;; --------------------------------------------------------- ;;
isr PROC
4868 240 020 MVO R0, $0020 ; enable display
486A 1C0 CLRR R0
486B 240 030 MVO R0, $0030 ; no horizontal delay
486D 240 031 MVO R0, $0031 ; no vertical delay
486F 240 032 MVO R0, $0032 ; no border extension
4871 240 028 MVO R0, $0028 ; black background
4873 240 02C MVO R0, $002C ; black border
4875 0AF JR R5 ; return from ISR
ENDP
;; --------------------------------------------------------- ;;
;; our routine ($4876 to $4897 -> 34 DECLEs) ;;
;; --------------------------------------------------------- ;;
nato PROC
4876 299 @loop MVI@ R3, R1 ; R1 = input character
4877 261 MVO@ R1, R4 ; write it at R4
4878 089 TSTR R1 ; stop if it's zero
4879 204 00F BEQ @done
487B 001 SDBD ; subtract 65 and add twice the address
487C 2F9 0D5 090 ADDI #@len*2-65, R1 ; of the @len table (carry is cleared)
487F 071 RRC R1 ; floor-divide by 2 by rotating
4880 289 MVI@ R1, R1 ; read the table
4881 201 002 BC @adv ; was the original value even? ...
4883 065 SLR R1, 2 ; ... yes: right-shift by one nibble
4884 065 SLR R1, 2
4885 3B9 00F @adv ANDI #$F, R1 ; keep the least significant nibble
4887 0CB ADDR R1, R3 ; skip that many characters
4888 220 013 B @loop ; process the next character
488A 0AF @done JR R5 ; return
488B 045 075 ... @len DECLE $45, $75, $47, $45 ; A-H
488F 057 044 ... DECLE $57, $44, $48, $54 ; I-P
4893 065 065 ... DECLE $65, $65, $76, $74 ; Q-X
4897 064 DECLE $64 ; Y-Z
ENDP
Output
Opcode (x64, Linux), 33 bytes
00000000: adad aa91 00c9 7409 48b8 d031 3570 98ed ......t.H..15p..
00000010: 08c3 48d3 e83c 8774 e783 e003 4801 c6eb ..H..<.t....H...
00000020: e0 .
fx: lodsd
f: lodsd
stosb
xchg ecx, eax
add cl, cl
jz .r-1 ; ret
mov rax, 0xc308ed98703531d0
.r: shr rax, cl
cmp al, 0x87 ; It only appears at N
jz fx
and eax, 3
add rsi, rax
jmp f
Support both upper and lower case. Require few bytes after src readable.
Python 3, 72 bytes
Function f accepts an upper-case string and returns a lower-case string. This solution uses a slightly different method of compression from other solutions.
The word sizes are packed into an integer that is treated as a bit vector of 25×3 bits. This bit vector is then encoded in hexadecimal as the magic number 0x32430590834CA281800, which corresponds to the octal string 3110301310203231212014000. Note reverse order, which saves one byte in decoding despite the hexadecimal literal being two chars longer than otherwise (0x2108A2D308164304B, 17 digits, for the curious).
The correct length is then extracted from the string by shifting the appropriate amount (3×ordinal) and extracting the three least-significant bits using the modulo operator.
f=lambda s:s and s[0]+f(s[4+(0x32430590834CA281800>>ord(s[0])%25*3)%8:])
C (gcc), 67 63 bytes
Similar to many other approaches here, including Arnauld's. Input as a upper-case, widechar string with NUL terminator, output via stdout.
Length of the phonetic representation is encoded as a string and one character is extracted from the string based on the first character of input. Following a suggestion from @emanresu, the characters were changed from ASCII-encoding to raw bytes with values in the 4-8 range. The extracted character is then used to shift the string pointer to the right. The function then recurses, passing the pointer to the next invokation.
Golfy tricks include using && short circuiting for branching and recursion, rather than for. The return value of putchar() is used inside the array subscript (returns printed char on success). Obviously, widechar string used to shave a single byte off total solution.
f(int*s){*s&&f(s+"\4\4\4\8\5\4\6\5\6\5\7\6\7\4\6\4\5\7\5\4\7\4\5\5\7"[putchar(*s)%25]);}
For purposes of clarity, escape characters are used in the code snippet to represent the raw bytes, which would otherwise be non-graphical. Raw characters are used in the TIO source code.
VBA, 646 bytes
Public Function REVERSENATO(itext As String)
Dim clen As Long
Dim nato() As String
Dim ctext As String
Dim newstring As String
ReDim nato(26, 1)
For c = 1 To 26
nato(c, 0) = Sheets("Sheet1").Range("F" & c)
nato(c, 1) = Sheets("Sheet1").Range("G" & c)
Next c
ctext = itext
clen = Len(ctext)
Do Until clen = 0
For test = 1 To 26
If InStr(1, ctext, nato(test, 1)) = 1 Then
ctext = Right(ctext, Len(ctext) - Len(nato(test, 1)))
clen = Len(ctext)
newstring = newstring & nato(test, 0)
Exit For
End If
Next test
Loop
REVERSENATO = newstring
End Function
Not a good or small solution but I did it so that's something.
GNU sed -e, 58 bytes
s/(NOVE|[CFJUW]..|[QSVY].|[BDHIORT]|)..../\l&/g
s/[A-Z]//g
This produces lower-case output, which seems acceptable in such a decoder.
How it works: simple regex determines number of letters; \l downcases the first letter of each proword, then we remove all remaining capital letters.
Commodore 64 Assembler, 48 47 Bytes (6502/KickAssembler)
Explanation
- Usage: Write the input string starting from the top of the screen, add a whitespace after the string, go to an empty line, and run the program with
SYS 4096. Program will read first letters of the words until it reaches the whitespace. - In short: The program reads the first letter on the screen, prints it out, and uses the letter's screencode as an index to a lookup table containing word lengths, i.e. how many chars to skip, i.e. where in the string the next word begins.
- The code itself is only 21 bytes, whereas the word length lookup table hogs the remaining 26 bytes.
- Max length of the input string is 256 chars, due to using the 6502 CPU's 8-bit index register to keep track of which part of the string we're processing. According to the author, this can be considered a system limitation and is thus fine.
Code uses very few special sizecoding tricks to reduce its size, it is pretty vanilla 6502 and quite compact by nature. Maybe only three things worth mentioning:
- Using an unintended opcode (that is, an opcode that isn't in the list of official opcodes of 6502 as provided by the manufacturer, but still works)
LAX(Load to A and X) which loads the char's screencode to bothAandXregisters in one go. This negates the need for separateLDA(LoaD to Accumulator) andTAX(transfer A to X) commands, thus saving a byte. - UPDATE: Instead of Absolute,Y addressing to point to the top of the screen, the aforementioned
LAXuses Indirect Indexed addressing. In other words,lax $0400,ybecomeslax ($0e),y, as memory addresses $0e-$0f happen to hold address $0400 afterSYS. This saves another byte. - Using the screencode of the first letter of the word as an index to the word lengths lookup table. Pretty common practice on old, slow systems, but maybe still worth mentioning as someone new to assembly may find this fascinating. It sure fascinated me when I learned about such usage for the first time. 😁 Screencodes for A-Z are $01-$1B, and table index is zero based, thus if we want the length for e.g. letter 'A', we just look up into table location table-1+'A'.
Code
* = $1000 // Run the program with SYS4096
ldy #0 // Reset Y, which will be used as string index.
next: lax ($0e),y // Load char from string, indexed by Y, into A and X.
cmp #$20 // Is it a whitespace char?
beq done // If yes, whole string is processed and we're done.
adc #$40 // Convert screen code $01-$1b to PETSCII code $41-$5b...
jsr $ffd2 // ...and call ROM routine to print the char on screen.
tya // Copy string index from Y to A...
adc length-1,x // ...and add length of the current word to it, using the
// charcode as the index to a lookup table with lengths.
tay // Copy updated string index back from A to Y.
bcc next // If we've not gone over 256 chars, continue.
done: rts // We're done - return to system.
length: .byte 4,5,7,5,4,7,4,5,5,7,4,4,4,8,5,4,6,5,6,5,7,6,7,4,6,4
05AB1E, 23 bytes
Δć©?•2úèæ…½D₅’wη•A®kè.$
I/O in lowercase.
Try it online or verify all test cases.
Explanation:
Δ # Loop until the result no longer changes,
# using the (implicit) input-string in the first iteration:
ć # Extract the head of the current string
© # Store this in variable `®` (without popping)
? # Pop and output this letter
•2úèæ…½D₅’wη• # Push compressed integer 3464363446333743545465635
A # Push the lowercase alphabet
® # Push letter `®`
k # Get the index of this letter in the alphabet
è # Use that to index into the compressed integer
# (index 25 for 'z' will wrap around to the leading 3)
.$ # Remove that many leading characters from the remainder-string
See this 05AB1E tip of mine (section How to compress large integers?) to understand why •2úèæ…½D₅’wη• is 3464363446333743545465635.
C64 Basic, 104 bytes
The phonetic spelling string begins in the top left corner of the screen.
Google Sheets, 107 bytes
=let(f,lambda(f,s,if(s="",,left(s)&f(f,mid(s,mid(5686585668555&9657676878575,code(s)-64,1),9^9)))),f(f,A1))
Put the string in cell A1 and the formula in C1. Gets first letters by looking up the offset recursively.

Ungolfed:
=let(
f, lambda(f, s,
if(len(s),
left(s) &
f(f, mid(s,
mid(
5686585668555 & 9657676878575,
code(s) - 64,
1
),
9^9
)),
)
),
f(f, A1)
)
The concatenation operator & gets a string, and takes one byte less than "quotes".
Ruby -pl, 54 52 bytes
Port of l4m2's Node.js port of Neil's Retina solution. -2 bytes from that answer optimizing the regex.
gsub(/([BDHIORT]|N?[CFJOUW]..|[QSVY].)?..../){$&[0]}
Ruby -nl, 80 bytes
My original solution; I just think the way I determine how many letters to skip is neat so it's here for posterity.
i=0;($><<c=$_[i];i+=4+%w"AEGKLMPXZ BDHIORT QSVY CFJUW N".index{_1[c]})while$_[i]
Perl 5 -F, 61 bytes
say@F[0,map$p+=$F[$p]=~y/A-Z/45754745574448546565767474/r,@F]
I could have ported @Neil's solution and gotten this to 51 bytes, but where's the fun in that?
Vyxal 3, 32 bytes
øA‹"-MK,x¬<8≠☷Ṫ“f$iλhϢᑂ$f]lᐐḧσ↻i
i couldnt figure out cyclical indexing in vyxal so its just a scan fixpoint jank
this might be better in older versions with builtins for fixpoints and whatnot
R, 67 bytes
\(s)gsub("(?=(.))([BDHIORT]|N?[CFJOUW]..|[QSVY].)?....","\\1",s,,T)
Port of l4m2's Node.js port of Neil's Retina solution.
APL+WIN, 93 bytes
Prompts for character string.
c←⎕
a←⎕av[65+⍳26]
n←⍎¨'45754745574448546565767464'
r←⍬
:while 0<⍴c
r←r,↑c
c←n[↑a⍳c]↓c
:end
r
Jelly, 24 bytes
OḢị“CṁƝ.ḍ⁹8ɲṠȯ:’D¤⁸ṫµƬZḢ
A monadic Link that accepts a list of characters and yields a list of characters.
How?
OḢị“...’D¤⁸ṫµƬZḢ - Link: list of characters, S
µƬ - collect up, starting with S, while distinct, applying:
O - ordinals of {Current}
Ḣ - head -> I = ordinal of first character
¤ - nilad followed by links as a nilad:
“...’ - 65767687857556865856685559
D - to decimal
ị - {I} 1-index, cyclically into {[6,5,7,6,...]}
⁸ṫ - tail of {Current} from {that} 1-index
Z - transpose
Ḣ - head
Other, similar but more complicated 24's:
ḢȮOị“¥QḌEfṠƇ¤’b5¤+4⁸ṫßḷ¡ (full program)
ḢȮOị“¥QḌEfṠƇ¤’b5¤+4⁸ṫµ¹¿ (full program)
OḢị“¥QḌEfṠƇ¤’b5¤+5⁸ṫµƬZḢ (monadic Link)
Retina 0.8.2, 92 80 49 47 bytes
(?=(.))([BDHIORT]|[QSVY].|N?[CFJOUW]..)?....
$1
Try it online! Link includes test cases. Explanation: Inspired by @Ausername's lookbehind approach. Saved 2 bytes thanks to @l4m2.
(?=(.))([BDHIORT]|[QSVY].|N?[CFJOUW]..)?....
$1
Look ahead to capture and keep the first letter of the word, then skip ahead depending on the first letter(s); BDHIORT words are five letters long, QSVY words are six letters long, CFJUW words are seven letters long, N(O) words are eight letters long, and the rest are four letters long.
Previous 80-byte no-lookahead solution:
(.)(U.B?I?E?T?|RAV?|(OX|OV.|H.)...|O.E|IC?T?E?R?.|[ENS]I?F?..|[CLO].|AN?K?.).
$1
Try it online! Link includes test cases. Explanation:
(.)(...|...).
$1
Keep the first letter of each word, where the alternatives match as follows:
U.B?I?E?T?.-QUEBEC,JULIETT,ZULURAV?.-BRAVO,XRAY(OX|OV.|H.)....-FOXTROT,NOVEMBER,CHARLIE,WHISKEYO.E.-ROMEO,HOTELIC?T?E?R?..-VICTOR,SIERRA,MIKE,KILO,LIMA[ENS]I?F?...-DELTA,INDIA,UNIFORM,OSCAR[CLO]..-ECHO,ALFA,GOLFAN?K?..-YANKEE,TANGO,PAPA
JavaScript (Node.js), 64 62 bytes
s=>s.replace(/([BDHIORT]|N?[CFJOUW]..|[QSVY].)?..../g,x=>x[0])
Based on Neil's solution but js needn't capture the first char in regex
-2 bytes changing regex, which should feed back Neil
Charcoal, 29 bytes
FS¿ω≦⊖ω«ι≔I§”)⧴&Xφ⟧x\`t⊙;g”℅ιω
Try it online! Link is to verbose version of code. Explanation: Port of @Arnauld's JavaScript answer.
FS
Loop over the letters of the input string.
¿ω≦⊖ω
If we're skipping letters then decrement the number left to skip, ...
«ι
otherwise output the current letter, and...
≔I§”...”℅ιω
... use a cyclically indexed compressed lookup table to see how many letters need to be skipped.
JavaScript (ES6), 71 bytes
f=s=>s&&s[0]+f(s.slice("4448546565767464575474557"[s.charCodeAt()%25]))
(or 69 bytes in Node.js)
TinyAPL, 163 bytes
ugh.
⦅⊣⌿⍨1⌽·∨ᑒ/⊣⍷ᐵ⍨'0'∘≠⥼⊆⊢⦆∘"LFA0RAVO0HARLIE0ELTA0CHO0OXTROT0OLF0OTEL0NDIA0ULIETT0ILO0IMA0IKE0OVEMBER0SCAR0APA0UEBEC0OMEO0IERRA0ANGO0NIFORM0ICTOR0HISKEY0RAY0ANKEE0ULU"
⦅⊣⌿⍨1⌽·∨ᑒ/⊣⍷ᐵ⍨'0'∘≠⥼⊆⊢⦆∘...
'0'∘≠⥼⊆⊢ # Split big string by '0'
⊣⍷ᐵ⍨ # Find each in the argument, returning a boolean list where each beginning of a substring is true
∨ᑒ/ # Or-reduce the masks
1⌽ # Rotate the mask by 1 (since the first character is not present in the substrings)
⊣⌿⍨ # Filter the argument by that mask
💎
Created with the help of Luminespire.
Google Sheets / Excel, 158 bytes
Expects input in A1
=REGEXREPLACE(A1,"LFA|RAVO|HARLIE|ELTA|CHO|OXTROT|OLF|OTEL|NDIA|ULIETT|ILO|IMA|IKE|OVEMBER|SCAR|APA|UEBEC|OMEO|IERRA|ANGO|NIFORM|ICTOR|HISKEY|RAY|ANKEE|ULU",)



