<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Enzo</title>
    <description>The latest articles on DEV Community by Enzo (@enzofalvo).</description>
    <link>https://dev.to/enzofalvo</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1389018%2F30ad0b7b-a746-4b31-8b25-1f75a183350d.png</url>
      <title>DEV Community: Enzo</title>
      <link>https://dev.to/enzofalvo</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/enzofalvo"/>
    <language>en</language>
    <item>
      <title>I analyzed 14,905 love letters without reading them</title>
      <dc:creator>Enzo</dc:creator>
      <pubDate>Sat, 05 Sep 2026 22:01:08 +0000</pubDate>
      <link>https://dev.to/enzofalvo/i-analyzed-14905-love-letters-without-reading-them-4760</link>
      <guid>https://dev.to/enzofalvo/i-analyzed-14905-love-letters-without-reading-them-4760</guid>
      <description>&lt;p&gt;I run &lt;a href="https://lovepaper.app" rel="noopener noreferrer"&gt;LovePaper&lt;/a&gt;, a site where you write a letter to someone and it becomes a page with a permanent link. No account, nothing to install.&lt;/p&gt;

&lt;p&gt;Last week I wanted to know what people actually write on it. I ran the numbers on September 1st, across 14,905 letters. By the time I published this the database was at 17,780, it adds around 700 a day. Everything below is from that September 1st snapshot.&lt;/p&gt;

&lt;p&gt;This post is what I found, how I measured it without opening a letter, the one time I deliberately broke that rule, and the bug that almost made me publish a wrong number.&lt;/p&gt;

&lt;h2&gt;
  
  
  The constraint came first
&lt;/h2&gt;

&lt;p&gt;A love letter is the most private thing someone will type into a stranger's website. So the rule was: &lt;strong&gt;nothing leaves the database except a count.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No text printed, logged, or written to a file. Every query returns a number, a percentage, or a frequency table. If a question can only be answered by reading, it doesn't get answered.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;withEmoji&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;emojiTotal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wordCounts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;EMOJI&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\p&lt;/span&gt;&lt;span class="sr"&gt;{Extended_Pictographic}/gu&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;EMOJI&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;withEmoji&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;emojiTotal&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;ems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;wordCounts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+/&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// `t` goes out of scope here. Only counters survive.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Boring code. That's the point.&lt;/p&gt;

&lt;h2&gt;
  
  
  What 14,905 letters look like
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The median letter is 156 words.&lt;/strong&gt; A quarter are under 70. A fifth run past 300. The longest is 689.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;51% contain at least one emoji&lt;/strong&gt;, averaging 6.4 when they do. The eight most used:&lt;/p&gt;

&lt;p&gt;❤️ · 😘 · 🤍 · 🥹 · 💗 · 😭 · 💋 · 🥺&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three of those eight are crying faces.&lt;/strong&gt; I expected hearts.&lt;/p&gt;

&lt;p&gt;Over 1,091,079 words with stopwords removed: &lt;strong&gt;"sorry" is the 25th most written word. "forever" is the 74th.&lt;/strong&gt; People apologize about three times more often than they promise anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Only 3.3% ever open the edit screen.&lt;/strong&gt; Whatever comes out the first time is what gets sent.&lt;/p&gt;

&lt;p&gt;And &lt;strong&gt;72.1% pick the dark theme.&lt;/strong&gt; I built the light one first, assuming a love letter wanted to look like paper. Wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one time I broke my own rule
&lt;/h2&gt;

&lt;p&gt;Before writing, people pick an occasion: birthday, anniversary, proposal, apology, long distance, friendship, or "just because."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;40.5% pick "just because"&lt;/strong&gt; with every real occasion sitting on the same screen. And &lt;strong&gt;apology beats marriage proposal four to one&lt;/strong&gt;, 420 against 106.&lt;/p&gt;

&lt;p&gt;That second number bothered me. 1.5% proposals felt wrong for a love letter site. Either the number was broken or the feature was.&lt;/p&gt;

&lt;p&gt;So I opened those letters. All 109 of them. It's my database and I decided a mislabeled feature was worth the exception, but I want to be straight that it &lt;em&gt;was&lt;/em&gt; an exception, not the method.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;About eight were actually proposing marriage.&lt;/strong&gt; The rest were first confessions. People asking someone out. Several written to someone who had no idea yet.&lt;/p&gt;

&lt;p&gt;The label said "proposal." The users meant "I'm about to tell you." &lt;strong&gt;That's a product bug, and no aggregate would have shown it to me.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug that almost made me publish a wrong number
&lt;/h2&gt;

&lt;p&gt;I wanted the language breakdown. First version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;hasTagalogMarkers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tl&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;hasPortugueseMarkers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;hasSpanishMarkers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;es&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;hasEnglishMarkers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It reported &lt;strong&gt;15% Tagalog&lt;/strong&gt;. That felt high, so I looked closer.&lt;/p&gt;

&lt;p&gt;The function is order-dependent. Any English letter containing one Tagalog word, and love letters borrow words constantly, matched the first branch and never reached the others. I wasn't detecting language. I was detecting &lt;em&gt;"does this contain any Tagalog word at all."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The fix was to stop deciding on first match and start scoring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;en&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;pt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;es&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;tl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;word&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;words&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lang&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;LANGS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;MARKERS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;word&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;top&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;topScore&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ranked&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[,&lt;/span&gt; &lt;span class="nx"&gt;runnerUp&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ranked&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;topScore&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;undetermined&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;topScore&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;runnerUp&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;1.3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;undetermined&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// too close to call&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;top&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two guards matter more than the scoring. &lt;strong&gt;A minimum score&lt;/strong&gt;, so three stray words can't decide. And &lt;strong&gt;a margin over the runner-up&lt;/strong&gt;, so a near-tie returns &lt;code&gt;undetermined&lt;/code&gt; instead of a confident wrong answer.&lt;/p&gt;

&lt;p&gt;Real numbers: &lt;strong&gt;53.5% English, 9.3% Portuguese, 6.9% Tagalog, 5.6% Spanish&lt;/strong&gt;, and &lt;strong&gt;24.7% honestly undetermined&lt;/strong&gt;, which is the part I'd have buried if I were trying to look clever.&lt;/p&gt;

&lt;p&gt;The first version wasn't broken. It ran fine and returned a plausible number. That's the dangerous kind.&lt;/p&gt;

&lt;h2&gt;
  
  
  There is no love song
&lt;/h2&gt;

&lt;p&gt;You can attach a song. 664 letters have one, across &lt;strong&gt;423 distinct tracks&lt;/strong&gt;, and &lt;strong&gt;79% of those appear exactly once.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No shared soundtrack. Everyone brings their own.&lt;/p&gt;

&lt;h2&gt;
  
  
  The day I found out about by accident
&lt;/h2&gt;

&lt;p&gt;On September 1st the numbers jumped. 463 letters three days earlier, then 619, then 843, then &lt;strong&gt;1,070 in a single day&lt;/strong&gt;, the biggest day the site has ever had.&lt;/p&gt;

&lt;p&gt;I went looking for a bug. Instead I found that &lt;strong&gt;148 of those letters had the words "letter day" written inside them&lt;/strong&gt;, in English and in Bengali, people wishing each other a happy one.&lt;/p&gt;

&lt;p&gt;September 1st is World Letter Writing Day. I had never heard of it. My users had.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;p&gt;Aggregate-only is more work than dumping rows into a notebook, and it rules out whole questions. I only found the mislabeled feature by breaking it on purpose, once, with a reason I could defend out loud.&lt;/p&gt;

&lt;p&gt;I still think it's the right default. The dataset is only interesting because people trusted it with something real.&lt;/p&gt;

&lt;p&gt;If you want a number I didn't cover, ask in the comments and I'll run it.&lt;/p&gt;

&lt;p&gt;Always tell people you love them!! 💌&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>datascience</category>
      <category>javascript</category>
      <category>privacy</category>
    </item>
  </channel>
</rss>
