# Judge a book by its cover

## Overview

The cover of a book will be provided in the following ASCII format:

______
|    |
|    |
|    |
|    |
------


The input can be in any reasonable format (eg. a list of strings, a nested list of characters, etc.)

Inside the "borders" of the cover, other printable ascii characters might appear, which contribute to the "popularity index" (the definition of which has been completely made up for this challenge).

### Popularity Index

Let the popularity index of a book be the number of unique characters that appear on the book cover (this does not include space characters or the edges of the cover). A book may have no unique characters on the cover (the cover is blank), in which case the popularity index is 0.

Examples

______
|\/ /|
|/\/ |
| /\/|
|/ /\|
------


Two unique characters (/ and \) so the popularity index is 2.

______
| / /|
|/ / |
| / /|
|/ / |
------


One unique character (/), so the popularity index is 1

______
|    |
|    |
|    |
|    |
------


No unique characters, the popularity index is 0

______
|\^$@| |/\/ | | 456| |/ /\| ------  8 unique characters, so the popularity index is 8. ______ |\^$@|
|987 |
| 456|
|/hi!|
------


14 unique characters, so the popularity index is 14.

______
|////|
|////|
|////|
|////|
------


One unique character (/), so the popularity index is 1.

### Damaged Books

The edges of the book might also be damaged:

Top         Left        Right       Bottom

_~____      ______      ______      ______
|    |      |    |      |    |      |    |
|    |      |    |      |    |      |    |
|    |  or  {    |  or  |    }  or  |    |
|    |      |    |      |    |      |    |
------      ------      ------      ---~--


The book may have several of these "scratches". The scratches at the top and bottom will always be represented by a ~, while the scratches on the left and right will always be represented by a { and } respectively. Each scratch will decrease the popularity index by 1.

As a result, it is possible for a book to have a negative popularity index.

Given an ASCII representation of a book cover in the format described above, determine the "popularity index" of the book

### Assumptions

• You can assume that the characters |, _, and - will not appear elsewhere on the book cover (only on the edges). However, the scratch characters ({, }, and ~) may appear on the book cover in which case they should be treated as any other unique character on the cover.

• All characters appearing on the cover will be printable ascii

• Note that spaces do not count as a unique character. As the examples above show, a "blank" cover is one that only contains spaces.

• The book will always be the same size (height and width are fixed). Hence, the popularity index will never exceed 16.

### Scoring

This is . Happy golfing!

More Examples

______
|{. }|
|/. /|
|/. /}
|/. /|
------


4 unique characters ({, }, . and /) and one scratch (} on the right) so the popularity index is 3.

______
{    |
| .. |
|    }
{    |
--~~--


One unique character (.) and 5 scratches so the popularity index is -4.

### Test Cases

______
|    |
|    |
|    |
|    |
------    ->    0

______
|\/ /|
|/\/ |
| /\/|
|/ /\|
------    ->    2

______
| / /|
|/ / |
| / /|
|/ / |
------    ->    1

______
|\^$@| |/\/ | | 456| |/ /\| ------ -> 8 ______ |!!!!| |+ +| | ** | |<**>| ------ -> 5 ______ |\^$@|
|987 |
| 456|
|/hi!|
------    ->    14

______
|THIS|
| is |
| a. |
|BOOK|
------    ->    11

______
|////|
|////|
|////|
|////|
------    ->    1

______
|abcd|
|efgh|
|ijkl|
|mnop|
------    ->    16

______
|{. }|
|/. /|
|/. /}
|/. /|
------    ->    3

______
{    |
| .. |
|    }
{    |
--~~--    ->    -4

~~~~~~
{    }
{    }
{    }
{    }
~~~~~~    ->    -20

______
|~~~~|
|.   |
{....}
|.   |
------    ->    0

______
|{~~}|
|    |
|    |
|    |
------    ->    3

__~~__
|{~~}|
|    |
|    |
{    |
-----~    ->    -1

• Why am I thinking of Pawn Stars' book expert while reading this?... :p May 27, 2020 at 21:09
• The more scratches a book has, the more popular the book is. Like, I borrowed TAOCP from the library with about 56 scratches on the cover of a book.
– user92069
May 28, 2020 at 8:04
• @Arnauld Yes that's fine, since the length will always be the same May 28, 2020 at 14:25
• By the way there's a software challenge at the Internet Archive too: blog.archive.org/2019/01/05/…
– Nemo
May 28, 2020 at 19:07
• The examples in the "Damaged books" section have dashes at the top. May 29, 2020 at 14:04

# JavaScript (ES6),  75 74  73 bytes

Takes a single string of 36 characters as input, i.e. without any line feed.

s=>s.replace(o=/[^ |_-]/g,(c,n)=>t+=n*9%56%37%9&1?o[c]^(o[c]=1):-1,t=0)|t


Try it online!

### How?

We filter out spaces, pipes, underscores and hyphens:

/[^ |_-]/g


It means that we're going to match only:

• scratches on the border (-1 point)
• or non-space characters on the cover (+1 point the first time each of them appears)

Given the 0-indexed position $$\n\$$ of the character in the input string, the shortest solution I've found so far to figure out if we're located on the border or on the cover is the following convoluted modulo chain:

$$\big(((9\times n)\bmod 56)\bmod 37\big)\bmod 9$$

which gives:

$$\begin{pmatrix} 0&1&2&3&4&5\\ 6&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&33&34&35 \end{pmatrix} \rightarrow \begin{pmatrix} \color{blue}0&\color{blue}0&\color{blue}0&\color{blue}0&\color{blue}0&\color{blue}8\\ \color{blue}8&7&7&7&7&\color{blue}6\\ \color{blue}6&5&5&5&5&\color{blue}4\\ \color{blue}4&3&3&3&3&\color{blue}2\\ \color{blue}2&1&1&1&1&\color{blue}0\\ \color{blue}0&\color{blue}0&\color{blue}8&\color{blue}8&\color{blue}8&\color{blue}8 \end{pmatrix}$$

With even numbers on the border and odd numbers on the cover.

### Generalization of the formula

Given a square matrix of width $$\w>3\$$ whose cells are indexed from $$\0\$$ to $$\w^2-1\$$, the $$\n\$$-th cell is located on a border iff the result of the following expression is even:

$$\big(((p\times n)\bmod m_0)\bmod m_1\big)\bmod p$$

with:

$$p=2w-3$$ $$m_0=2\cdot (w-1)^2+w$$ $$m_1=2\cdot (w-2)^2+w-1$$

Try it online!

NB: This is an empirical result that I didn't attempt to prove.

• @FilipHaglund My initial formula was n/6%5<1|n%6%5<1 (which gives the opposite result) and I was not able to reduce it further with conventional tricks. I know from experience that such modulo chains often work well if the input domain is arranged in a non-random manner and the output domain is small. So I tried that, and it was brute-forced indeed. May 29, 2020 at 11:31
• @FilipHaglund See my post update for an a-posterioti generalization of the formula. May 29, 2020 at 12:07
• @Joe85AC I think the best we can do is (n*n%35)**.5%1. It's pretty neat and looks simpler than the current formula, but it's actually 1 byte longer. Jun 5, 2020 at 8:28
• @Joe85AC Interestingly, your formula works for 1, 2, 4, 6, 12, 18, 30, 42, 60, 72, 102, 108, 138, 150, ... Jun 6, 2020 at 9:06
• @Arnauld -- That seems to be A014574. The sequence of the average of two twin primes... Seems we're on the verge of publishing a "number theory" paper... :-) mathworld.wolfram.com/TwinPrimes.html Jun 6, 2020 at 9:23

# Python 2, 73 bytes

Input is taken as a 2D list $$\ b \$$. Output is the popularity index of $$\ b \$$.

lambda b:len({j.pop(1)for j in b[1:5]*4}-{' '})-sum(map(b.count,'{}~'))


Try it online!

The left part of the expression does two things: adds up the popularity index inside the cover, and removes its characters. The right part subtracts the damaged characters from the remaining characters.

# J, 5141 40 bytes

_1#.1#.(+./~#:33)e.&'{}~'(' '~:~.)/.&,]


Try it online!

-11 bytes thanks to Bubbler!

Both the function table trick to compress #:@68208171135 down to +./~@#:@33, and the "negative one base trick" are due to them.

## how

1. +./~@#:@33 Let's begin with this phrase, which produces:

1 1 1 1 1 1
1 0 0 0 0 1
1 0 0 0 0 1
1 0 0 0 0 1
1 0 0 0 0 1
1 1 1 1 1 1


How? 33 in binary #:@33 is 1 0 0 0 0 1. Then create an "or" function table +./ of this list with itself. It is easy to verify this will be 1 exactly on the border cells.

2. /.&,] Flatten the input and use the mask above to split the input into two groups, using J's Key /. adverb:

Apply this verb to the              Damage:
1st group, the edges:    -------->  Is each character an
|                              element of '{}~'?
___/____                           Returns a 0-1 list.
e.&'{}~'(' '~:~.)
^^^^^^^^^
|                    Reputation:
And this one to the second -->  Is space not equal
(the interior)                  to each character
of the uniq? Also
a 0-1 list.


We now have two 0-1 lists. For example:

Reputation: 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0
Damage:     1 1 0 1 1


J will extend the shorter list to the length of the longer, filling it with zeros:

0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0
1 1 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

3. 1#. Sum rowwise:

1 4

4. _1#. This uses a -1 base as a golfy way to subtract 1 from 4. By definition of base:

(1 * -1^1) + (4 * -1^0)
(1 * -1) + (4 * 1)
3

• 41 bytes. May 28, 2020 at 0:10
• Save one more byte with +./~@#:@33 -> (+./~#:33). May 28, 2020 at 7:33
• @Bubbler Impressive stuff. I learned at least 2 new tricks. The compression of the mask using +. is great, and I've never seen negative base used like that either. Hats off to you! May 28, 2020 at 22:58

# Python 2, 68 bytes

lambda s:len(set((s[8:-6]*4)[::7])-{' '})-20+sum(map(s.count,'_|-'))


Try it online!

Instead of counting defects on the borders, counts non-defects _|- throughout and subtract from 20, using the rule that only the borders can contain these characters. The (s[8:-6]*4)[::7] is a nice string-slicing way to extract the characters on the cover without borders.

• @dingledooper Nice one, you should post an answer for the outgolfing bounty.
– xnor
May 29, 2020 at 18:47

# 05AB1E, 25 22 bytes

4Fćˆøí}˜ðKÙg¯˜…~{}S¢O-


-3 bytes by taking inspiration of @JonathanAllan's Jelly answer, so make sure to upvote him!

Input as a matrix of characters.

Original 25 bytes approach:

|¦¨ε¦¨}ðýê¦gIJ…_|-S¢O20-+


Default input as loose lines in STDIN.

Explanation:

4F            # Loop 4 times:
ć           #  Extract head; pop and push remainder-matrix and first line separately
ˆ          #  Pop and add this first line to the global array
ø         #  Zip/transpose the remaining matrix; swapping rows/columns
í        #  Reverse each line
#  (øí basically rotates the matrix once clockwise)
}˜            # After the loop: flatten the remaining matrix
ðK          # Remove all spaces
Ù         # Uniquify it
g        # And pop and push the amount of remaining unique (non-space) characters
¯˜            # Push the global array, and flatten it as well
…~{}        # Push string "~{}"
S       # Convert it to a list of characters: ["~","{","}"]
¢      # Count each in the flattened global array
O     # Sum those
-             # Subtract the two from one another
# (after which the result is output implicitly)

|             # Push all lines of inputs as a list of strings
¦¨           # Remove the first and last lines
ε  }       # Map over each line:
¦¨        #  And also remove the first and last character of each line
ðý     # Join everything together with space delimiter
ê    # Uniquify and sort the characters
¦   # Remove the first character (the space)
g  # And pop and push its length
I             # Push the input-list of lines again
J            # Join them all together
…_|-        # Push string "_|-"
S       # Convert it to a list of characters: ["_","|","-"]
¢      # Count each in the joined input
O     # Take the sum of those counts
20-  # Subtract 20 from this
+             # Add the two together
# (after which the result is output implicitly)

• This sure beats what I was thinking of. I was heading down a probably 50-100 byte answer before I saw this. Good job! May 28, 2020 at 8:04
• @Lyxal And here I actually had the feeling 1 or 2 more bytes could be saved. :) Out of curiosity, what approach did you had in mind? One with a matrix similar as the J/APL answers? May 28, 2020 at 8:13
• I was going to port my literal interpretation approach in my python answer. As I said, many more bytes than yours. This answer is most likely golfable ;P May 28, 2020 at 8:14
• Actually, this seems like a more golfed version of my said approach. Huh. Who'da thunk it? May 28, 2020 at 8:16
• @Lyxal Yeah, the count part is rather similar indeed. First part is a bit different, although that's to expected, since I doubt unique and sort the characters for example is cheap in Python. ;) May 28, 2020 at 8:20

# Jelly, 25 bytes

Ḣ;ɼȧZṚµ4¡FQḟ⁶,®f“~{}”¤Ẉ_/


A full program accepting a single argument as a list of Python strings which prints the result.

Try it online! Or see the test-suite.

### How?

Ḣ;ɼȧZṚµ4¡FQḟ⁶,®f“~{}”¤Ẉ_/ - Main Link: list of lists of characters (the lines)
µ4¡                 - repeat this monadic chain four times:
Ḣ                         -   head (gives us the head AND modifies the chain's input)
ɼ                       -   recall from the register (initially 0) and apply & store:
;                        -     concatenate
Z                     -   transpose the chain's (now modified) input
ȧ                      -   logical AND (the concatenate result with the transpose, to get us the transpose of the chain's input)
Ṛ                    -   reverse (together with the transpose this is rotate one quarter)
F                - flatten (what we have left is the cover's inner characters)
Q               - de-duplicate
®           -   recall from the register (the cover's edge characters)
f“~{}”     -   keep only "~{}" characters
,            - pair (the two lists of characters)
Ẉ   - length of each
/ - reduce by:
_  -   subtraction
- implicit print (a list containing only one entry prints just that entry)


# Python 2, 66 bytes

lambda s:len(set((s[8:-6]*4)[::7]+' '))-21+sum(map(s.count,'_|-'))


Try it online!

A slight improvement over @xnor's answer. Instead of removing spaces from the cover, we add spaces to the cover, and then subtract 1.

# Python 3, 106102 80 bytes

lambda b:len({*(b[8:12]+b[15:19]+b[22:26]+b[29:33])})-21+sum(map(b.count,"-|_"))


Try it online!

I know there's a shorter Python answer, and I know that this isn't the most optimal method. But I had come up with a solution which I golfed, and I'm posting it for the sake of completion. -2 thanks to @Kevin and -2 thanks to OVS. A further -22 thanks to @mathjunkie.

• return len(set(m))-s-1 can be return~s+len(set(m)) for -2 May 28, 2020 at 8:16
• And another -1 by switching to Python 2 and use print instead of return. :) Try it online. May 28, 2020 at 8:19
• Back to Python 3 with {*m} instead of set(m) ;). And if you replace 20 with 19 the -1 can be removed, this doesn‘t save any bytes though.
– ovs
May 29, 2020 at 14:42
• 80 bytes, by converting to a lambda and using map instead of c=b.count: Try it online! May 29, 2020 at 22:20
• This currently fails for cases with no spaces. There are a few ways to deal with this... May 30, 2020 at 3:23

# perl -M5.010 -n -0543, 82 bytes

$i--for/\}$|^\{|~(?=[-_~]*$)/gm;s/^[-_~]+$|^.|.$//gm;$c{$_}++for/\S/g;say$i+keys%c


Try it online!

First, the penalty is calculated by counting {s which start a line, }s which end a line, and any ~ in the top and bottom lines. We then strip off the boundaries, and count the number of unique non-space characters; the penalty is added to that, to get the final score.

Reads a single cover from STDIN, writes the score to STDOUT.

# APL (Dyalog Unicode), 42 35 bytes

-6 thanks to Bubbler.

Anonymous tacit prefix function. Requires ⎕IO←0 (0-based indexing).

-⍨/(,∘.⍱⍨6⍴5↑1){≢∪⍣⍺⊢⍵~⊃⍺↓⊂'_|-'}⌸∊


Try it online!

∊ϵnlist (flatten)

(){}⌸ apply the below function to each of the two groups identified by 1s and 0s in:

5↑1 take five elements from 1; [1,0,0,0,0]

6⍴ cyclically reshape that to length six; [1,0,0,0,0,1]

∘.⍱⍨ NOR-table of that;
[[0,0,0,0,0,0],
 [0,1,1,1,1,0],
 [0,1,1,1,1,0],
 [0,1,1,1,1,0],
 [0,1,1,1,1,0],
 [0,0,0,0,0,0]]

, ravel (flatten)

The following function will be applied to each multiset (0: edge characters; 1: inner characters):

'_|-' a string; "_|-"

⊂ enclose it; ["_|-"]

⍺↓ drop 1 element if the set is that of inner characters, otherwise drop 0 elements

⊃ get the first element; the above string or " " if gone

⍵~ remove those characters from the edge/inner set

∪⍣⍺⊢ if inner, then get the unique of that, else leave as-is

≢ tally that

-⍨/ subtract the first count from the last count

• 36 bytes May 28, 2020 at 0:25

# Charcoal, 49 41 bytes

≔⪫Ｅ⁶Ｓ¶θＰθＢ⁶ ＷΦＫＡ¬№υκ⊞υ⊟ι⎚Ｉ⊖⁻Ｌυ⁻²⁰ΣＥ-_|№θι


Try it online! Link is to verbose version of code. Explanation:

≔⪫Ｅ⁶Ｓ¶θ


Input the cover.

ＰθＢ⁶


Print the cover, but immediately erase the border.

ＷΦＫＡ¬№υκ⊞υ⊟ι


Collect all the unique characters, which always includes a space, as we used that to erase the border. (I feel that I should be able to use Ｗ⁻ＫＡυ⊞υ⊟ι but Charcoal crashes when I try.)

⎚Ｉ⊖⁻Ｌυ⁻²⁰ΣＥ-_|№θι


Clear the canvas and output the popularity, adjusting for the extra space collected and the number of missing border characters in the original string.

I also tried counting the popularity by directly inspecting the string instead of printing it and deleting the border but it turns out that that costs bytes overall.

Previous 49-byte solution does not assume that edge characters only appear on the edge:

≔⪫Ｅ⁶Ｓ¶θＰθＢ⁶ ＷΦＫＡ¬№υκ⊞υ⊟ιＰθ↘ＵＯ⁴ ≔ΦＫＡ№{~}ιθ⎚Ｉ⊖⁻ＬυＬθ


Try it online! Link is to verbose version of code. Explanation:

≔⪫Ｅ⁶Ｓ¶θ


Input the cover.

ＰθＢ⁶


Print the cover, but immediately erase the border.

ＷΦＫＡ¬№υκ⊞υ⊟ι


Collect all the unique characters, which always includes a space, as we used that to erase the border. (I feel that I should be able to use Ｗ⁻ＫＡυ⊞υ⊟ι but Charcoal crashes when I try.)

Ｐθ↘ＵＯ⁴


Print the cover again, but this time keep only the border.

≔ΦＫＡ№{~}ιθ


Collect all the {, ~ and } characters.

⎚Ｉ⊖⁻ＬυＬθ


Clear the canvas and output the calculated popularity, adjusting for the extra space collected.

• @mathjunkie Sorry about that, I introduced a silly typo while golfing (ω should have been ¶).
– Neil
May 27, 2020 at 23:39

# Java (OpenJDK 8), 326320319305 bytes

int f(List<Character>[]c){int s=0,i=3;c[0].addAll(c[5]);for(;i>1;)S(50*i---55,c[0]);c[5].clear();for(;i<5;c[5].addAll(c[i++]))s+=c[i].remove(0)-c[i].remove(4);S(32,c[5]);for(;c[5].size()>0;i++)S(c[5].get(0),c[5]);return i-5-c[0].size()+s;}void S(int x,List c){c.removeAll(Collections.singleton((char)x));}


Try it online!

THANKS to Muskovets's suggestion of using List (-4), I also omitted the generic type in the second method (List instead of List< Character>)

Takes input as List< Character>[] and outputs an int.

It uses 2 methods (I'm not entirely sure how "allowed" that is)

See inside for comments and ungolfed and alternate solutions (including a single method one).

Interesting part: the ascii values for { | } are 123, 124, 125. That means that this part changes the popularity index by:

for each row: the left char minus the right char

{...} 123 - 125 = -2

{...| 123 - 124 = -1

|...} 124 - 125 = -1

|...| 124 - 124 = 0

It only works with only exactly the given possible inputs, none else (illegal inputs like }...{). Pretty lucky.

This was done with ArrayList, but possibly some other Collection with a shorter name or better methods would be better. Edit: indeed there is: Vector. updated. (Now List)

Takes input as String[]. Only one method.

# Java (OpenJDK 8), 267 261 bytes

int f(String[]c){int s=0,i=4;c[0]=c[0].replace("_","")+c[5].replace("-","");c[5]="";for(;i>0;c[5]+=c[i--].substring(1,5))s+=c[i].charAt(0)-c[i].charAt(5);for(;c[5].trim().length()>0;i++)c[5]=c[5].replace(c[5].trim().substring(0,1),"");return i-c[0].length()+s;}


Try it online!

• Uhm... List is a collection. It'd give you -4 :) May 29, 2020 at 6:16
• +1 for triple minus in F(50*i---55 May 29, 2020 at 7:06
• 301 bytes May 31, 2020 at 5:35

# Perl 5 + -p0 -MList::Util+uniq, 55 bytes

s/^.{5}|.
.|.{5}$/$\-=$&=~y!~{}!!;""/ge;$\+=uniq/\S/g}{


Try it online!

# PHP, 104 93 bytes

Original code from yesterday:

while($s<35+$c=ord($b[$t=$s++]))$p-=$t>5&$s<31?$t%6?$s%6?0&$i[$c]=1:$c:-$c:$c%5;echo$p+count($i)-$i[32];


Try it online!

Improved with a mod-expression from @Arnauld, cf. his solution.

for(;$s<36;$s++)$p-=$s*9%56%37%9&1?!$i[$b[$s]]=1:ord($b[$s])%29%8>0;echo$p+count($i)-$i[" "];


Try it online!

## Explanation

• I was using the fact that char codes of intact top and bottom edges are %5=0, and left and right edges have char codes that are in sequence.
• I've now researched the char codes of the six edge chars a little better.
Try [45,95,124,123,125,126]%18%15. There are also other options like $c%29%8==0 and so on. ## Please Help Me Get Better I appreciate any comments or suggestions. • Hi, welcome to the site! I'm not a PHP golfer myself but you can check out tips for golfing in PHP for some suggestions (there's a similar thread for basically every language). Jun 4, 2020 at 15:56 • Could you also add a link to somewhere your code could be tested online, such as: Try it online! (for that site, you'll probably need to add some code to the header and footer to demonstrate how to call your function/program) Jun 4, 2020 at 16:27 • @mathjunkie -- I added a link. Thanks for the question! Jun 4, 2020 at 17:10 ## Bash + standard tools 121 Bytes bc<<<$(sed '1d;$d;s/.//;s/.$//' $1|grep -o '[^ ]'|sort -u|wc -l)-$(sed -E '2,${$!s/(.)..../\1/}' \$1|tr -cd '[~{}]'|wc -c)


Input is a file of the book.

Try it online! (+6 bytes because input needs to be a string here)

# C (gcc), 174 bytes

//N(c)=p-=*s++!=c
p,v,i,j,k;f(char*s){char u[16]={};for(v=p=0,i=6;i--;N(95));for(j=4;j--;N(124))for(N(124),i=4;i--;)strchr(u,k=*s++-32)||(u[v++]=k);for(i=6;i--;N(45));s=p+v;}


Try it online!

Pretty standard: I count the number of unique characters in the cover (which is built up in a string), then deduct the amount of damage.

• Suggest {v=p=0};for(i=6 instead of {};for(v=p=0,i=6 May 31, 2020 at 4:20

# Erlang (escript), 184 bytes

Just a port of Lyxal's Python answer. (Where's the unique function in Erlang?)

u([H|T])->[H]++u([I||I<-T,I/=H]);u(X)->X.
f(X)->length(u(lists:flatten([lists:droplast(tl(I))||I<-lists:droplast(tl(string:split(X,"
",all)))])))-21+length([I||I<-X,([I]--"_|-")==[]]).


Try it online!

• This doesn't work when the cover has no spaces: Try it online! should be 16 but gives 15 May 31, 2020 at 14:39

# Ruby, 101 98 bytes

->s{a,*b,c=s.lines;(b.flat_map{x,*y,z,_=_1.chars;a+=x+z;y}|[' ']).size-22+(a+c).tr('{}~','').size}


### 100 byte version because TIO doesn't support ruby 2.7

->s{a,*b,c=s.lines;(b.flat_map{|i|x,*y,z,_=i.chars;a+=x+z;y}|[' ']).size-22+(a+c).tr('{}~','').size}


Try it online!