<?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: Sunil Kumar · GapSeat</title>
    <description>The latest articles on DEV Community by Sunil Kumar · GapSeat (@gapseat).</description>
    <link>https://dev.to/gapseat</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%2F4204173%2F21810aec-9572-4293-948b-03b790a5cea9.png</url>
      <title>DEV Community: Sunil Kumar · GapSeat</title>
      <link>https://dev.to/gapseat</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gapseat"/>
    <language>en</language>
    <item>
      <title>Finding empty berths on 'fully booked' Indian trains: reservation charts as intervals</title>
      <dc:creator>Sunil Kumar · GapSeat</dc:creator>
      <pubDate>Sat, 24 Oct 2026 07:52:47 +0000</pubDate>
      <link>https://dev.to/gapseat/finding-empty-berths-on-fully-booked-indian-trains-reservation-charts-as-intervals-1anf</link>
      <guid>https://dev.to/gapseat/finding-empty-berths-on-fully-booked-indian-trains-reservation-charts-as-intervals-1anf</guid>
      <description>&lt;p&gt;On Indian Railways, a train can show "waiting list" for days and still leave with empty berths. People cancel late, don't turn up, or book only part of the route. Once the railway prepares the &lt;strong&gt;reservation chart&lt;/strong&gt;, the berth-level picture is public, but it's spread across dozens of coaches and station pairs. Almost nobody can use it by hand.&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://gapseat.in" rel="noopener noreferrer"&gt;GapSeat&lt;/a&gt; to answer one question: &lt;em&gt;is there a berth I can use between my two stations?&lt;/em&gt; And if no single berth covers the trip, &lt;em&gt;what's the fewest number of berth changes that does?&lt;/em&gt; This post is about the model and the search behind that second answer, which I call seat hopping.&lt;/p&gt;

&lt;h2&gt;
  
  
  The data: a berth is a list of segments
&lt;/h2&gt;

&lt;p&gt;After the chart is prepared, each berth comes with segments along the train's route: booked from station A to station D, vacant from D to F, booked again from F onwards, and so on. Stations have an order along the route, so the first step is to replace station codes with their index in the schedule.&lt;/p&gt;

&lt;p&gt;Two rules matter more than anything else:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Segments are half-open intervals.&lt;/strong&gt; A booking from A to D occupies the legs A→B, B→C and C→D. It does &lt;em&gt;not&lt;/em&gt; occupy D→E. The passenger gets off at D, and the berth is free from there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Occupied always wins, and unknown is never vacant.&lt;/strong&gt; If the data says a leg is both vacant and occupied (it happens when charts overlap), treat it as occupied. If a station isn't in the schedule, ignore that segment instead of guessing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With stations indexed, a berth becomes a set of vacant &lt;em&gt;legs&lt;/em&gt; (leg &lt;code&gt;i&lt;/code&gt; is the stretch from station &lt;code&gt;i&lt;/code&gt; to station &lt;code&gt;i + 1&lt;/code&gt;). Consecutive legs merge into ranges:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;VacantSeatRange&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;vacantSeatRanges&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SeatResult&lt;/span&gt; &lt;span class="n"&gt;seat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;vacant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;{},&lt;/span&gt; &lt;span class="n"&gt;occupied&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;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;final&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;seat&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;segments&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;to&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="n"&gt;from&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;to&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="c1"&gt;// unknown never counts&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;var&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;occupied&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;occupied&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;vacant&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;              &lt;span class="c1"&gt;// half-open: [from, to)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;vacant&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;removeAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;occupied&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                               &lt;span class="c1"&gt;// occupied wins&lt;/span&gt;
  &lt;span class="c1"&gt;// ...merge sorted consecutive legs into [from, to) ranges&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Checking whether one berth covers a trip is now a range check: is there a vacant range with &lt;code&gt;from &amp;lt;= boarding&lt;/code&gt; and &lt;code&gt;to &amp;gt;= destination&lt;/code&gt;?&lt;/p&gt;

&lt;h2&gt;
  
  
  Seat hopping: a shortest path, built backwards
&lt;/h2&gt;

&lt;p&gt;When no single berth covers the trip, we want a sequence of berths: berth A from your station to Kanpur, then berth B from Kanpur to your destination. We want the fewest changes, because every change means moving your luggage at a station, sometimes into another coach.&lt;/p&gt;

&lt;p&gt;Think of each station as a node. Any vacant range that covers station &lt;code&gt;s&lt;/code&gt; gives an edge from &lt;code&gt;s&lt;/code&gt; to every later station it reaches. The problem is a shortest path, where "shortest" means fewest edges.&lt;/p&gt;

&lt;p&gt;The planner works &lt;strong&gt;backwards from the destination&lt;/strong&gt;. For each station, from the last one before the destination back to the boarding station, it keeps the best few ways to finish the trip from there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;best&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;JourneyStep&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&amp;gt;{&lt;/span&gt; &lt;span class="nl"&gt;to:&lt;/span&gt; &lt;span class="p"&gt;[[]]&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;   &lt;span class="c1"&gt;// at the destination, nothing left to do&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;var&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// edges: every station reachable from `start` on one berth&lt;/span&gt;
  &lt;span class="c1"&gt;// candidates: one step to `next`, then the best ways on from `next`&lt;/span&gt;
  &lt;span class="c1"&gt;// keep the top `limit`, ranked by number of steps&lt;/span&gt;
  &lt;span class="n"&gt;best&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rank&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;best&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few details make the results feel right to a passenger:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No branch cutoff.&lt;/strong&gt; Every station is considered as a possible change point. A pruning heuristic would be faster, but it could hide a direct berth or a one-change route, and that's the one result a user must never miss.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One representative berth per reachable station.&lt;/strong&gt; When ten berths all go from station 4 to station 9, the path only needs one of them. The results screen still offers the other berths that cover the same stretch, so the user can pick a lower berth or the same coach.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Staying on the same berth is one step.&lt;/strong&gt; If the best way on from station 5 starts on the very berth you're already on, the two steps merge. A berth that is vacant across two separate data segments shouldn't look like a change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ranking ties: travel farther first.&lt;/strong&gt; Between two paths with the same number of changes, prefer the one that keeps you on the first berth longer. People would rather change once near the end than early.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deduplicate by station pattern.&lt;/strong&gt; Two paths that change at the same stations are the same plan for the passenger, so we keep only one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bounds.&lt;/strong&gt; At most 5 paths, and at most 8 steps per path. Beyond that it isn't a plan anyone would follow.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each station looks at the vacant ranges that cover it, and each range reaches at most every later station, so the work is roughly proportional to &lt;em&gt;stations × ranges × stations&lt;/em&gt;. A long-distance train has well under a hundred halts and fewer than two thousand berths, which runs comfortably on a phone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing the edge cases
&lt;/h2&gt;

&lt;p&gt;Most of the planner's tests are named after a way it could mislead a passenger:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;adjacent legs on the same berth require no seat change&lt;/li&gt;
&lt;li&gt;occupied overlap overrides vacancy, and unknown stations can't prove vacancy&lt;/li&gt;
&lt;li&gt;it doesn't bridge a vacancy gap on the same berth&lt;/li&gt;
&lt;li&gt;it can return to a berth after using another berth through that berth's gap&lt;/li&gt;
&lt;li&gt;it considers transfer stations inside a larger vacant interval&lt;/li&gt;
&lt;li&gt;it finds the direct seat even after many shorter candidate ranges&lt;/li&gt;
&lt;li&gt;it rejects disconnected paths even when the endpoints match&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The one I trust most is a property test. It generates 100 seeded random trains and, for each, computes the true minimum number of berths with a separate, deliberately simple search over every station pair. Then it checks three things: the planner's best path uses exactly that many berths, every step of every path really is vacant on its berth, and the planner returns nothing when no path exists. Hand-picked cases catch the bugs you thought of; that test catches the ones you didn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  The app around it
&lt;/h2&gt;

&lt;p&gt;GapSeat is a Flutter app (with &lt;code&gt;flutter_bloc&lt;/code&gt; for state and &lt;code&gt;get_it&lt;/code&gt; for dependency injection) plus a static website. A few things that turned out to matter for this audience:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;An offline timetable.&lt;/strong&gt; The app downloads a compact binary timetable once and queries it in a long-lived worker isolate. Station search and train lists work on a weak connection. Chart and vacancy data always come live, because occupancy can't be cached.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hindi, Marathi and Tamil&lt;/strong&gt;, alongside English, because a lot of the demand comes from Hindi and Hinglish searches.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TalkBack support&lt;/strong&gt; across the main vacancy-search journey, so screen-reader users can search a train and read the results.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Limits, honestly
&lt;/h2&gt;

&lt;p&gt;A vacancy result is a snapshot of the published chart. Berths can be booked through current booking or allotted by the TTE after you check. GapSeat doesn't book or allot anything. You still book through IRCTC current booking or a counter, or ask the TTE with a valid ticket. And GapSeat isn't affiliated with IRCTC or Indian Railways.&lt;/p&gt;

&lt;p&gt;If you're curious, try the web checker at &lt;a href="https://gapseat.in/chart-vacancy" rel="noopener noreferrer"&gt;gapseat.in/chart-vacancy&lt;/a&gt;, or get the &lt;a href="https://play.google.com/store/apps/details?id=com.gapseat.app" rel="noopener noreferrer"&gt;Android app&lt;/a&gt;. I'd love feedback on the planner. Is there a ranking you'd pick differently?&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>dart</category>
      <category>algorithms</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
