<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Blog | Zimo Li]]></title><description><![CDATA[Blog | Zimo Li]]></description><link>https://blog.zimo.li</link><generator>RSS for Node</generator><lastBuildDate>Mon, 20 Apr 2026 12:05:31 GMT</lastBuildDate><atom:link href="https://blog.zimo.li/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[I wrote a Parquet viewer in Rust to avoid running SQL for the PM]]></title><description><![CDATA[Every data team knows the drill: a PM needs to “just take a quick look” at some Parquet data. That usually means asking an engineer to write SQL or spin up a tool to pull a few rows. It’s a small ask, but one that happens often enough to slow everyon...]]></description><link>https://blog.zimo.li/tablr</link><guid isPermaLink="true">https://blog.zimo.li/tablr</guid><category><![CDATA[Rust]]></category><category><![CDATA[Parquet]]></category><category><![CDATA[Polars]]></category><category><![CDATA[GUI]]></category><dc:creator><![CDATA[Zimo Li]]></dc:creator><pubDate>Wed, 28 May 2025 20:52:36 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1748464372547/e6ed0de0-4d7e-4156-ac14-a5a18f6a9b86.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Every data team knows the drill: a PM needs to “just take a quick look” at some Parquet data. That usually means asking an engineer to write SQL or spin up a tool to pull a few rows. It’s a small ask, but one that happens often enough to slow everyone down.</p>
<p>That’s why I built <a target="_blank" href="https://github.com/lzm0/tablr"><strong>Tablr</strong></a> — an extremely simple desktop app for browsing Parquet files quickly and without any coding.</p>
<p>It’s a native desktop app written in Rust, powered by the blazing-fast <a target="_blank" href="https://github.com/pola-rs/polars">Polars</a> library and the <a target="_blank" href="https://github.com/emilk/egui"><code>egui</code></a> UI framework, and it runs on macOS, Windows and Linux.</p>
<h3 id="heading-key-features">Key Features</h3>
<p>Here’s what you get out of the box:</p>
<ul>
<li><p>🗂️ <strong>Multi-file support</strong> – Load single files or partitioned datasets with ease.</p>
</li>
<li><p>⚡ <strong>Infinite scrolling</strong> – Efficiently scroll through massive datasets, thanks to Polars’ lazy execution.</p>
</li>
<li><p>🔍 <strong>Basic filtering and sorting</strong> – Click on a column to sort, or filter rows using simple operations like “equals” or “contains”.</p>
</li>
<li><p>💻 <strong>Native performance</strong> – Built with Rust for speed and low memory usage.</p>
</li>
<li><p>🌐 <strong>Cross-platform</strong> – Works on Linux, macOS, and Windows.</p>
</li>
</ul>
<h3 id="heading-how-to-try-it">How to Try it</h3>
<p>There aren’t prebuilt binaries yet, but if you’ve got Rust installed, it’s easy to get up and running:</p>
<pre><code class="lang-bash">git <span class="hljs-built_in">clone</span> https://github.com/lzm0/tablr
<span class="hljs-built_in">cd</span> tablr
cargo run --release
</code></pre>
<p>One of my favourite features is that you can open Parquet files straight from your terminal — no GUI navigation required. Just point it at a <code>.parquet</code> file:</p>
<pre><code class="lang-bash">tablr path/to/your_file.parquet
</code></pre>
<p>Or, if you’ve got a folder of partitioned files:</p>
<pre><code class="lang-bash">tablr path/to/your_dataset/*.parquet
</code></pre>
<h3 id="heading-get-involved">Get Involved</h3>
<p>For ideas, bug reports and pull requests, jump in here:</p>
<p>👉 <a target="_blank" href="https://github.com/lzm0/tablr">https://github.com/lzm0/tablr</a></p>
]]></content:encoded></item><item><title><![CDATA[🤯TIL for...else exists in Python]]></title><description><![CDATA[Today, I learned that you can pair an else clause with a loop statement such as for or while. It looks like this:
for i in iterator:
    # do something
else:
    # do something else

This may look quite counterintuitive for the first time, but we wil...]]></description><link>https://blog.zimo.li/til-forelse-exists-in-python</link><guid isPermaLink="true">https://blog.zimo.li/til-forelse-exists-in-python</guid><category><![CDATA[Python]]></category><dc:creator><![CDATA[Zimo Li]]></dc:creator><pubDate>Thu, 23 Feb 2023 18:17:26 GMT</pubDate><content:encoded><![CDATA[<p>Today, I learned that you can pair an <code>else</code> clause with a loop statement such as <code>for</code> or <code>while</code>. It looks like this:</p>
<pre><code class="lang-python"><span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> iterator:
    <span class="hljs-comment"># do something</span>
<span class="hljs-keyword">else</span>:
    <span class="hljs-comment"># do something else</span>
</code></pre>
<p>This may look quite counterintuitive for the first time, but we will unwrap it in a moment! ✨</p>
<p>First, let's review the basic syntax of a <code>for</code> loop in Python. A <code>for</code> loop allows you to iterate over an iterator (such as a list, tuple, or string) and execute a block of code for each value. Here's an example:</p>
<pre><code class="lang-python">fruits = [<span class="hljs-string">'apple'</span>, <span class="hljs-string">'banana'</span>, <span class="hljs-string">'cherry'</span>]
<span class="hljs-keyword">for</span> fruit <span class="hljs-keyword">in</span> fruits:
    print(fruit)
</code></pre>
<p>This code would output:</p>
<pre><code class="lang-python">apple
banana
cherry
</code></pre>
<h1 id="heading-simple-example">Simple example</h1>
<p>Now, let's add an <code>else</code> clause to this loop:</p>
<pre><code class="lang-python">fruits = [<span class="hljs-string">'apple'</span>, <span class="hljs-string">'banana'</span>, <span class="hljs-string">'cherry'</span>]
<span class="hljs-keyword">for</span> fruit <span class="hljs-keyword">in</span> fruits:
    print(fruit)
<span class="hljs-keyword">else</span>:
    print(<span class="hljs-string">"No more fruits"</span>)
</code></pre>
<p>This code would output:</p>
<pre><code class="lang-python">apple
banana
cherry
No more fruits
</code></pre>
<p>In this case, the <code>else</code> block is executed after the <code>for</code> loop has finished normally. If the loop is exited early (for example, by a <code>break</code> statement), the <code>else</code> block will not be executed.</p>
<h1 id="heading-linear-search-example">Linear search example</h1>
<p>This <code>for..else</code> can be quite helpful in some scenarios. For example, you can use it to check if a value exists in a list:</p>
<pre><code class="lang-python">values = [<span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span>, <span class="hljs-number">4</span>, <span class="hljs-number">5</span>]
<span class="hljs-keyword">for</span> value <span class="hljs-keyword">in</span> values:
    <span class="hljs-keyword">if</span> value == <span class="hljs-number">6</span>:
        print(<span class="hljs-string">"Value found!"</span>)
        <span class="hljs-keyword">break</span>
<span class="hljs-keyword">else</span>:
    print(<span class="hljs-string">"Value not found."</span>)
</code></pre>
<p>Let's take a look at another version without using <code>for...else</code>:</p>
<pre><code class="lang-python">values = [<span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span>, <span class="hljs-number">4</span>, <span class="hljs-number">5</span>]

found = <span class="hljs-literal">False</span>
<span class="hljs-keyword">for</span> value <span class="hljs-keyword">in</span> values:
    <span class="hljs-keyword">if</span> value == <span class="hljs-number">6</span>:
        found = <span class="hljs-literal">True</span>
        <span class="hljs-keyword">break</span>

<span class="hljs-keyword">if</span> found:
    print(<span class="hljs-string">"Value found!"</span>)
<span class="hljs-keyword">else</span>:
    print(<span class="hljs-string">"Value not found."</span>)
</code></pre>
<p>You can see, we have to introduce a boolean flag if we do not use the <code>else</code> clause. This makes the control flow more complex.</p>
<h1 id="heading-breaking-out-of-nested-loops">Breaking out of nested loops</h1>
<p>In Python, the <code>break</code> statement only terminates the nearest enclosing loop. In some cases, you may want to break out of a parent loop.</p>
<p>Consider this code that determines if <code>n</code> is a prime number:</p>
<pre><code class="lang-python">n = <span class="hljs-number">11</span>

<span class="hljs-keyword">if</span> n &lt;= <span class="hljs-number">1</span>:
    print(<span class="hljs-string">f"<span class="hljs-subst">{n}</span> is not a prime number!"</span>)
    exit()

<span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(<span class="hljs-number">2</span>, n):
    <span class="hljs-keyword">for</span> j <span class="hljs-keyword">in</span> range(<span class="hljs-number">2</span>, n):
        <span class="hljs-keyword">if</span> i * j == n:
            print(<span class="hljs-string">f"<span class="hljs-subst">{n}</span> is not a prime number!"</span>)
            <span class="hljs-keyword">break</span>
    <span class="hljs-keyword">else</span>:
        <span class="hljs-keyword">continue</span> <span class="hljs-comment"># only executed if the inner loop did NOT break</span>
    <span class="hljs-keyword">break</span> <span class="hljs-comment"># only executed if the inner loop DID break</span>
<span class="hljs-keyword">else</span>:
    print(<span class="hljs-string">f"<span class="hljs-subst">{n}</span> is a prime number!"</span>)
</code></pre>
<p>It works by iterating over all pairs of numbers <code>(i, j)</code> less than <code>n</code> and checking if the product of any of them is equal to <code>n</code>. In the case where we find <code>i * j</code> is equal to <code>n</code>, we can say that <code>n</code> is not a prime number and we would want to exit the outer <code>for</code> loop early.</p>
<p>If the inner loop completes successfully (i.e., without breaking), it means that no pairs of numbers were found whose product equals <code>n</code>. In this case, the <code>else</code> clause of the inner loop is executed. This <code>else</code> clause triggers the <code>continue</code> statement, which causes the outer loop to immediately proceed to the next value of <code>i</code> without executing the remaining code in the outer loop.</p>
<p>If the inner loop breaks at any point, the <code>else</code> clause of the inner loop is skipped and the outer loop would reach the <code>break</code> statement, which breaks the outer loop as well.</p>
<p><a target="_blank" href="https://peps.python.org/pep-3136/#specification">PEP 3136</a> is a Python Enhancement Proposal that proposes support for labels in <code>break</code> and <code>continue</code> statements. However, it was <a target="_blank" href="https://mail.python.org/pipermail/python-3000/2007-July/008663.html">rejected</a> due to the complexity it would add to Python.</p>
<p>The example I have shown above is one of the workarounds that produce clean code. Another solution is to put the code in a function and use <code>return</code> instead. Using a function is usually preferred because code with low cyclomatic complexity is usually more maintainable.</p>
]]></content:encoded></item><item><title><![CDATA[Why is my Rust code blocking???]]></title><description><![CDATA[Rust is a low-level programming language known for its performance and safety. We'll look at why my first attempt to use std::process resulted in blocking.
Problem
The code snippet below is an example of a Rust program that can potentially block. It ...]]></description><link>https://blog.zimo.li/why-is-my-rust-code-blocking</link><guid isPermaLink="true">https://blog.zimo.li/why-is-my-rust-code-blocking</guid><category><![CDATA[Rust]]></category><category><![CDATA[FFmpeg]]></category><dc:creator><![CDATA[Zimo Li]]></dc:creator><pubDate>Wed, 07 Dec 2022 19:56:12 GMT</pubDate><content:encoded><![CDATA[<p>Rust is a low-level programming language known for its performance and safety. We'll look at why my first attempt to use <code>std::process</code> resulted in blocking.</p>
<h3 id="heading-problem">Problem</h3>
<p>The code snippet below is an example of a Rust program that can potentially block. It uses the <code>Command</code> struct from the <code>std::process</code> module to spawn a new process that runs the <code>ffmpeg</code> command. It reads an audio file in <code>mp3</code> format from standard input (<code>stdin</code>), passes it to <code>ffmpeg</code> as input, convert the audio file to <code>ogg</code> format, and then reads the output from <code>ffmpeg</code> and writes it to standard output (<code>stdout</code>).</p>
<pre><code class="lang-rust"><span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">main</span></span>() {
    <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> cmd = Command::new(<span class="hljs-string">"ffmpeg"</span>)
        .arg(<span class="hljs-string">"-i"</span>)
        .arg(<span class="hljs-string">"-"</span>)
        .arg(<span class="hljs-string">"-c:a"</span>)
        .arg(<span class="hljs-string">"libopus"</span>)
        .arg(<span class="hljs-string">"-f"</span>)
        .arg(<span class="hljs-string">"ogg"</span>)
        .arg(<span class="hljs-string">"-"</span>)
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .spawn()
        .unwrap();

    <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> input_data = <span class="hljs-built_in">Vec</span>::new();
    stdin().read_to_end(&amp;<span class="hljs-keyword">mut</span> input_data).unwrap();
    <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> output_data = <span class="hljs-built_in">Vec</span>::new();

    <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> stdin = cmd.stdin.take().unwrap();
    <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> stdout = cmd.stdout.take().unwrap();

    stdin.write_all(&amp;input_data).unwrap();
    stdout.read_to_end(&amp;<span class="hljs-keyword">mut</span> output_data).unwrap();

    <span class="hljs-keyword">let</span> exit_status = cmd.wait().unwrap();

    <span class="hljs-keyword">if</span> !exit_status.success() {
        <span class="hljs-built_in">panic!</span>(<span class="hljs-string">"ffmpeg failed"</span>);
    }
}
</code></pre>
<p>In this scenario, the Rust program is using the <code>write_all</code> method to write data to the <code>stdin</code> pipe, which is connected to the <code>ffmpeg</code> child process. The <code>ffmpeg</code> process is writing data to its <code>stdout</code> pipe, but nobody is consuming that data.</p>
<p>When the <code>stdout</code> pipe buffer fills up, the <code>ffmpeg</code> process will block while trying to write further data. This means it will also stop reading data from <code>stdin</code>, causing the <code>stdin</code> pipe buffer to fill up. When the <code>stdin</code> pipe buffer is full, the parent Rust process will also block while trying to write further data.</p>
<h3 id="heading-solution">Solution</h3>
<p>To avoid this issue, the program can use another thread to write the data to <code>stdin</code>, allowing the <code>ffmpeg</code> process to read from <code>stdin</code> and the parent process to write to it concurrently.</p>
<pre><code class="lang-rust"><span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">main</span></span>() {
    <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> cmd = Command::new(<span class="hljs-string">"ffmpeg"</span>)
        .arg(<span class="hljs-string">"-i"</span>)
        .arg(<span class="hljs-string">"-"</span>)
        .arg(<span class="hljs-string">"-c:a"</span>)
        .arg(<span class="hljs-string">"libopus"</span>)
        .arg(<span class="hljs-string">"-f"</span>)
        .arg(<span class="hljs-string">"ogg"</span>)
        .arg(<span class="hljs-string">"-"</span>)
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .spawn()
        .unwrap();

    <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> input_data = <span class="hljs-built_in">Vec</span>::new();
    stdin().read_to_end(&amp;<span class="hljs-keyword">mut</span> input_data).unwrap();
    <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> output_data = <span class="hljs-built_in">Vec</span>::new();

    <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> stdin = cmd.stdin.take().unwrap();
    <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> stdout = cmd.stdout.take().unwrap();

    <span class="hljs-comment">// Use a thread to write to stdin</span>
    <span class="hljs-keyword">let</span> writer = thread::spawn(<span class="hljs-keyword">move</span> || {
        stdin.write_all(&amp;input_data).unwrap();
    });

    stdout.read_to_end(&amp;<span class="hljs-keyword">mut</span> output_data).unwrap();

    writer.join().unwrap();

    <span class="hljs-keyword">let</span> exit_status = cmd.wait().unwrap();

    <span class="hljs-keyword">if</span> !exit_status.success() {
        <span class="hljs-built_in">panic!</span>(<span class="hljs-string">"ffmpeg failed"</span>);
    }
}
</code></pre>
<h3 id="heading-why-am-i-doing-this">Why am I doing this?</h3>
<p>I'm working on a Telegram bot that can send voice messages.</p>
<p>Since the <a target="_blank" href="https://core.telegram.org/bots/api#sendvoice">sendVoice</a> API in Telegram only accepts .OGG file encoded with OPUS, I created this simple Rust program wrapped in an <a target="_blank" href="https://actix.rs/">Actix Web</a> server.</p>
<p>Source code: <a target="_blank" href="https://github.com/lzm0/mpeg2opus">https://github.com/lzm0/mpeg2opus</a></p>
]]></content:encoded></item><item><title><![CDATA[Introducing Cronify]]></title><description><![CDATA[Cronify is a simple and easy-to-use utility that helps you schedule your cron jobs using Natural Language Processing (NLP).
What is a cron job?
A cron job is a scheduled task that is executed at a specified time or interval. Cron is a time-based job ...]]></description><link>https://blog.zimo.li/introducing-cronify</link><guid isPermaLink="true">https://blog.zimo.li/introducing-cronify</guid><category><![CDATA[cron]]></category><category><![CDATA[nlp]]></category><category><![CDATA[Linux]]></category><category><![CDATA[Next.js]]></category><category><![CDATA[openai]]></category><dc:creator><![CDATA[Zimo Li]]></dc:creator><pubDate>Wed, 03 Aug 2022 16:21:35 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1659543092366/m1vAHbfYT.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><a target="_blank" href="https://cronify.zimo.li/">Cronify</a> is a simple and easy-to-use utility that helps you schedule your cron jobs using Natural Language Processing (NLP).</p>
<h2 id="heading-what-is-a-cron-job">What is a cron job?</h2>
<p>A cron job is a scheduled task that is executed at a specified time or interval. <a target="_blank" href="https://en.wikipedia.org/wiki/Cron">Cron</a> is a time-based job scheduler in Unix-like operating systems.</p>
<h2 id="heading-why-use-cronify">Why use Cronify?</h2>
<p>Cronify is the easiest way to schedule cron jobs. With Cronify, you don't need to worry about the correct cron schedule expression. Just tell Cronify when you want the job to run, and it will generate the correct expression for you.</p>
<h2 id="heading-how-does-it-work">How does it work?</h2>
<p>Cronify uses OpenAI's Codex language model to understand the user's input and generate the correct cron schedule expression. It is using the same model that powers <a target="_blank" href="https://github.com/features/copilot">GitHub Copilot</a>.</p>
<h2 id="heading-how-to-use-cronify">How to use Cronify?</h2>
<p>To use Cronify, you just need to specify the time or interval when you want the job to run. For example, "every day at 10am", "every 2 hours", "every Monday at 9am".</p>
<h2 id="heading-contributing">Contributing</h2>
<p>The source code of Cronify is available at https://github.com/lzm0/cronify</p>
]]></content:encoded></item><item><title><![CDATA[Create Telegram Alerts with Cloudflare Workers and ScheduledEvent]]></title><description><![CDATA[Introduction
Motivations
Creating notifications on real events in the world is a very common scenario for developers.
Telegram Bot provides a convenient API for sending messages, enabling users to program cron jobs to monitor events of their interest...]]></description><link>https://blog.zimo.li/create-telegram-alerts-with-cloudflare-workers-and-scheduledevent</link><guid isPermaLink="true">https://blog.zimo.li/create-telegram-alerts-with-cloudflare-workers-and-scheduledevent</guid><category><![CDATA[cloudflare]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[serverless]]></category><category><![CDATA[telegram]]></category><dc:creator><![CDATA[Zimo Li]]></dc:creator><pubDate>Fri, 15 Jul 2022 21:02:07 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1657557088216/LnF3MaRAt.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-introduction">Introduction</h1>
<h2 id="heading-motivations">Motivations</h2>
<p>Creating notifications on real events in the world is a very common scenario for developers.</p>
<p><a target="_blank" href="https://core.telegram.org/bots">Telegram Bot</a> provides a convenient API for sending messages, enabling users to program cron jobs to monitor events of their interest and trigger push notifications without the knowledge of iOS or Android development.</p>
<p>There are many ways to host such cron jobs:</p>
<h3 id="heading-running-on-a-raspberry-pi-or-other-forms-of-home-server">Running on a Raspberry Pi (or other forms of home server)</h3>
<p>Home internet can face some stability issues such as internet connection. As recently as last Friday, <a target="_blank" href="https://blog.cloudflare.com/cloudflares-view-of-the-rogers-communications-outage-in-canada/">Rogers experienced a Canada-wide outage affecting mobile and broadband users</a>.</p>
<p>Electricity bills, or even a friend tripping over power cables (this happened to me), can also prevent you from building a reliable service.</p>
<h3 id="heading-running-on-a-cloud-provider">Running on a cloud provider</h3>
<p>Virtual machines from cloud providers can incur a non-trivial cost. The cheapest VPS hosting services can cost from $2.50 to $5.00 monthly. The CPUs on these machines could be almost idle since most cron jobs wait for network responses, which is wasteful.</p>
<p>In addition, using a static IP address for web scraping will likely trigger a CAPTCHA.</p>
<h2 id="heading-what-makes-cloudflare-workers-special">What makes Cloudflare Workers special</h2>
<p>Cloudflare has a serverless edge computing platform called <a target="_blank" href="https://workers.cloudflare.com/">Workers</a>.</p>
<p>It chose the V8 JavaScript engine built by the Google Chrome team instead of traditional virtualization and containerization technologies like Kubernetes and Docker to power a sandboxed multi-tenant execution environment.</p>
<p>The V8 engine introduced the concept of <a target="_blank" href="https://v8.dev/docs/embed">Isolates</a> in <a target="_blank" href="https://v8.dev/blog/10-years">2011</a>. An isolate is a VM instance with its own heap. A single process can run hundreds or thousands of Isolates. This saves tremendous overhead compared to containerized applications that occupy one process each.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1657557088216/LnF3MaRAt.png" alt="image.png" /></p>
<p>As a result, Cloudflare is able to offer a free plan for Workers, where the first 100,000 requests per day are free.</p>
<p>What is even more incredible, <a target="_blank" href="https://developers.cloudflare.com/workers/platform/cron-triggers/">Cron Triggers</a>, which allows developers to run Workers on a schedule, are provided at <strong>no additional cost</strong>. This is because cron jobs are less sensitive to latency as no users are waiting for them to finish. This gives Cloudflare more flexibility on where and when to schedule the Cron Triggers.</p>
<p>Cloudflare is not the only company that utilizes V8 isolates for serverless computing. <a target="_blank" href="https://deno.com/deploy">Deno Deploy</a> and <a target="_blank" href="https://vercel.com/docs/concepts/functions/edge-functions">Vercel Edge Functions</a> offer similar services, with minor differences in their runtime APIs.</p>
<h1 id="heading-get-started">Get Started</h1>
<h2 id="heading-create-a-telegram-bot">Create a Telegram bot</h2>
<p>Creating a Telegram bot is as simple as sending <code>/newbot</code> to <a target="_blank" href="https://t.me/botfather">@BotFather</a>.</p>
<p>You will get an authentication token by doing so. It is the only information you need to talk to the Telegram Bot API.</p>
<h2 id="heading-find-out-your-telegram-user-id">Find out your Telegram user ID</h2>
<p>This blog post assumes that you only need to send notifications to yourself or a list of known users. If you want more complex interactions, check out these <a target="_blank" href="https://core.telegram.org/bots/samples">examples</a>.</p>
<p>Your bot needs your user ID to send a message to you. This information can be retrieved by sending <code>/start</code> to <a target="_blank" href="https://t.me/userinfobot">@userinfobot</a>.</p>
<h2 id="heading-create-a-cron-trigger">Create a Cron Trigger</h2>
<ol>
<li>Create a new Worker project called <code>cron</code> in the Cloudflare dashboard</li>
<li>Under the Triggers tab, configure the interval that you want your Worker to execute, or use the <a target="_blank" href="https://crontab.guru/">cron syntax</a>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1657917329180/3Z_dmQCMY.png" alt="image.png" /></li>
</ol>
<h2 id="heading-configure-credentials">Configure credentials</h2>
<p>Under Settings -&gt; Variables, save your Telegram Bot token as <code>TELEGRAM_BOT_TOKEN</code> and your user id as <code>TELEGRAM_CHAT_ID</code>.</p>
<p>Hit <code>Encrypt</code> when you enter these values, they will no longer be viewable once saved.
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1657917672155/APNfXY93R.png" alt="image.png" /></p>
<h2 id="heading-write-the-code">Write the code</h2>
<ol>
<li><p>Click <code>Quick Edit</code> to open an online IDE to edit your Worker script
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1657915082421/kUKhEShsH.png" alt="image.png" /></p>
</li>
<li><p>Create a <code>sendMessage</code> function. Your credentials can be accessed via global variables in the Worker script</p>
<pre><code class="lang-JavaScript"><span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">sendMessage</span>(<span class="hljs-params">message</span>) </span>{
    <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">'https://api.telegram.org/bot'</span> + TELEGRAM_BOT_TOKEN + <span class="hljs-string">'/sendMessage'</span>, {
        <span class="hljs-attr">method</span>: <span class="hljs-string">'POST'</span>,
        <span class="hljs-attr">headers</span>: {  <span class="hljs-string">'Content-Type'</span>: <span class="hljs-string">'application/json'</span> },
        <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify({
            <span class="hljs-attr">chat_id</span>: TELEGRAM_CHAT_ID,
            <span class="hljs-attr">text</span>: message.toString().substring(<span class="hljs-number">0</span>, <span class="hljs-number">4096</span>)
        })
    })
    <span class="hljs-keyword">return</span> response
}
</code></pre>
</li>
<li><p>Use <code>addEventListener</code> to subscribe to the <code>scheduled</code> event. It will be invoked by the Workers runtime at a schedule you configured</p>
<pre><code class="lang-JavaScript">addEventListener(<span class="hljs-string">'scheduled'</span>, <span class="hljs-function"><span class="hljs-params">event</span> =&gt;</span> {
    event.waitUntil(sendMessage(<span class="hljs-string">"Hello, World!"</span>))
})
</code></pre>
</li>
<li><p>You can trigger a scheduled event within the web editor. Your bot will send you a <code>Hello, World!</code> if everything is configured correctly
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1657918168165/YzzKXzbb2.png" alt="image.png" /></p>
</li>
</ol>
<p>Once you click <code>Save and Deploy</code>, the Worker will run on the schedule behind the scene.</p>
<p>Use the <a target="_blank" href="https://developers.cloudflare.com/workers/runtime-apis/fetch/">Fetch API</a> from the Workers runtime to monitor interesting things on the internet. You can set up a stock alert, or send daily weather every day in the morning. The possibility is unlimited.</p>
<h1 id="heading-notes">Notes</h1>
<ol>
<li><p>Cron only has a 1-minute granularity. In other words, a Worker can only be triggered every minute at most.</p>
<p>A workaround to this is to use <code>setTimeout</code> to schedule multiple events within a minute.</p>
<p>For example, if you want to schedule a Worker to send a message every 30 seconds, start the Worker by sending a message, and do <code>setTimeout(sendMessage, 30000)</code> immediately after.</p>
</li>
<li><p>If a Worker is managed with <a target="_blank" href="https://developers.cloudflare.com/workers/wrangler/get-started/">Wrangler</a>, Cron Triggers should be exclusively managed through the <a target="_blank" href="https://developers.cloudflare.com/workers/wrangler/configuration/">wrangler.toml</a> file.</p>
</li>
</ol>
]]></content:encoded></item><item><title><![CDATA[Dealing with Daylight Saving Time in Scala/Java]]></title><description><![CDATA[Handing daylight saving time programmatically can be tricky. It could create a mini Y2K problem twice a year when done incorrectly.
If you want to get the current LocalDateTime in a different time zone, the best approach is to use an Instant and then...]]></description><link>https://blog.zimo.li/dealing-with-daylight-saving-time</link><guid isPermaLink="true">https://blog.zimo.li/dealing-with-daylight-saving-time</guid><category><![CDATA[Scala]]></category><category><![CDATA[Java]]></category><category><![CDATA[time]]></category><dc:creator><![CDATA[Zimo Li]]></dc:creator><pubDate>Thu, 07 Jul 2022 17:32:50 GMT</pubDate><content:encoded><![CDATA[<p>Handing daylight saving time programmatically can be tricky. It could create a mini Y2K problem twice a year when done incorrectly.</p>
<p>If you want to get the current <a target="_blank" href="https://docs.oracle.com/javase/8/docs/api/java/time/LocalDateTime.html">LocalDateTime</a> in a different time zone, the best approach is to use an <a target="_blank" href="https://docs.oracle.com/javase/8/docs/api/java/time/Instant.html">Instant</a> and then convert it to a <a target="_blank" href="https://docs.oracle.com/javase/8/docs/api/java/time/ZonedDateTime.html">ZonedDateTime</a>.
Use <a target="_blank" href="https://docs.oracle.com/javase/8/docs/api/java/time/ZonedDateTime.html#toLocalDateTime--">ZonedDateTime.toLocalDateTime</a> to extract the <a target="_blank" href="https://docs.oracle.com/javase/8/docs/api/java/time/LocalDateTime.html">LocalDateTime</a> part of this date-time.</p>
<p>The example below shows how clocks were turned forward and backward an hour in <a target="_blank" href="https://docs.oracle.com/javase/8/docs/api/java/time/package-summary.html">java.time</a> during DST changes in 2022.</p>
<pre><code class="lang-Scala"><span class="hljs-keyword">import</span> java.time.{<span class="hljs-type">ZoneId</span>, <span class="hljs-type">ZonedDateTime</span>, <span class="hljs-type">LocalDateTime</span>, <span class="hljs-type">Instant</span>}
<span class="hljs-keyword">import</span> java.time.temporal.<span class="hljs-type">ChronoUnit</span>

<span class="hljs-keyword">val</span> halfAnHourBeforeDstStarted = <span class="hljs-type">Instant</span>.parse(<span class="hljs-string">"2022-03-13T07:30:00.00Z"</span>)
<span class="hljs-keyword">val</span> halfAnHourBeforeDstEnds = <span class="hljs-type">Instant</span>.parse(<span class="hljs-string">"2022-11-06T06:30:00.00Z"</span>)

<span class="hljs-comment">/*
Mar 13, 2022 - Daylight Saving Time Started
Sunday, March 13, 2022, 2:00:00 am clocks were turned forward 1 hour to
Sunday, March 13, 2022, 3:00:00 am local daylight time instead.
 */</span>
halfAnHourBeforeDstStarted.atZone(<span class="hljs-type">ZoneId</span>.of(<span class="hljs-string">"US/Central"</span>)).toLocalDateTime
<span class="hljs-comment">// = 2022-03-13T01:30</span>
halfAnHourBeforeDstStarted
  .plus(<span class="hljs-number">1</span>, <span class="hljs-type">ChronoUnit</span>.<span class="hljs-type">HOURS</span>)
  .atZone(<span class="hljs-type">ZoneId</span>.of(<span class="hljs-string">"US/Central"</span>))
  .toLocalDateTime
<span class="hljs-comment">// = 2022-03-13T03:30</span>

<span class="hljs-comment">/*
Nov 6, 2022 - Daylight Saving Time Ends
Sunday, November 6, 2022, 2:00:00 am clocks are turned backward 1 hour to
Sunday, November 6, 2022, 1:00:00 am local standard time instead.
 */</span>
halfAnHourBeforeDstEnds.atZone(<span class="hljs-type">ZoneId</span>.of(<span class="hljs-string">"US/Central"</span>)).toLocalDateTime
<span class="hljs-comment">// = 2022-11-06T01:30</span>
halfAnHourBeforeDstEnds
  .plus(<span class="hljs-number">1</span>, <span class="hljs-type">ChronoUnit</span>.<span class="hljs-type">HOURS</span>)
  .atZone(<span class="hljs-type">ZoneId</span>.of(<span class="hljs-string">"US/Central"</span>))
  .toLocalDateTime
<span class="hljs-comment">// = 2022-11-06T01:30</span>
</code></pre>
<p>reference: https://www.timeanddate.com/time/change/usa</p>
]]></content:encoded></item><item><title><![CDATA[Scala Reduce vs Fold]]></title><description><![CDATA[There is only a subtle difference between reduce and fold. Fold requires an initial value, whereas reduce doesn’t need one.
Reduce example:
scala> (1 to 10).reduce(_ * _)
val res0: Int = 3628800
Fold example:
scala> (1 to 10).fold(0)(_ * _)
val res0:...]]></description><link>https://blog.zimo.li/scala-reduce-vs-fold</link><guid isPermaLink="true">https://blog.zimo.li/scala-reduce-vs-fold</guid><category><![CDATA[Scala]]></category><category><![CDATA[Functional Programming]]></category><dc:creator><![CDATA[Zimo Li]]></dc:creator><pubDate>Tue, 24 May 2022 20:51:50 GMT</pubDate><content:encoded><![CDATA[<p>There is only a subtle difference between <code>reduce</code> and <code>fold</code>. Fold requires an initial value, whereas reduce doesn’t need one.</p>
<p>Reduce example:</p>
<pre><code>scala<span class="hljs-operator">&gt;</span> (<span class="hljs-number">1</span> to <span class="hljs-number">10</span>).reduce(<span class="hljs-keyword">_</span> <span class="hljs-operator">*</span> <span class="hljs-keyword">_</span>)
val res0: Int <span class="hljs-operator">=</span> <span class="hljs-number">3628800</span>
</code></pre><p>Fold example:</p>
<pre><code>scala<span class="hljs-operator">&gt;</span> (<span class="hljs-number">1</span> to <span class="hljs-number">10</span>).fold(<span class="hljs-number">0</span>)(<span class="hljs-keyword">_</span> <span class="hljs-operator">*</span> <span class="hljs-keyword">_</span>)
val res0: Int <span class="hljs-operator">=</span> <span class="hljs-number">0</span>

scala<span class="hljs-operator">&gt;</span> (<span class="hljs-number">1</span> to <span class="hljs-number">10</span>).fold(<span class="hljs-number">1</span>)(<span class="hljs-keyword">_</span> <span class="hljs-operator">*</span> <span class="hljs-keyword">_</span>)
val res1: Int <span class="hljs-operator">=</span> <span class="hljs-number">3628800</span>
</code></pre><p>This is because <code>reduce</code> requires the operator to be both <strong>commutative</strong> and <strong>associative</strong>, whereas <code>fold</code> requires the operator to be <strong>associative</strong> only.</p>
<p>A reminder of some basic arithmetics concepts:</p>
<p>Commutative property:
\[
a + b = b + c
\]</p>
<p>Associative property:
\[
(a + b) + c= a + (b + c)
\]</p>
]]></content:encoded></item><item><title><![CDATA[Emoji, according to Microsoft, is English]]></title><description><![CDATA[See microsoft/vscode-github-triage-actions
const nonenglishChunk = translationChunk.replace(usKeyboardChars, '').replace(emojiChars, '');]]></description><link>https://blog.zimo.li/emoji-according-to-microsoft-is-english</link><guid isPermaLink="true">https://blog.zimo.li/emoji-according-to-microsoft-is-english</guid><category><![CDATA[Visual Studio Code]]></category><category><![CDATA[Microsoft]]></category><category><![CDATA[emoji]]></category><category><![CDATA[GitHub]]></category><dc:creator><![CDATA[Zimo Li]]></dc:creator><pubDate>Thu, 21 Apr 2022 16:31:19 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1650558758613/qOmzbmjnc.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>See <a target="_blank" href="https://github.com/microsoft/vscode-github-triage-actions/blob/d96ee889c7794c69adb96b6cd8ebffbcab74b556/english-please/EnglishPlease.js#L20">microsoft/vscode-github-triage-actions</a></p>
<pre><code class="lang-JavaScript"><span class="hljs-keyword">const</span> nonenglishChunk = translationChunk.replace(usKeyboardChars, <span class="hljs-string">''</span>).replace(emojiChars, <span class="hljs-string">''</span>);
</code></pre>
]]></content:encoded></item></channel></rss>