<?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" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ Nibesh Khadka - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ Nibesh Khadka - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Sun, 24 May 2026 16:29:55 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/author/kdk/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Configure Google Workspace Addon For Tier 2 CASA Security Assessment – Step by Step Guide ]]>
                </title>
                <description>
                    <![CDATA[ As part of the Google CASA process, developers can run static analysis on their application’s source code using an inline integration with OpenText’s Fortify Source Code Analyzer (SCA) via the CASA portal. Naturally, I had to prepare my source code a... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/tier-casa-security-assessment/</link>
                <guid isPermaLink="false">66ba5b0dcccc49d721b6ea3a</guid>
                
                    <category>
                        <![CDATA[ Google ]]>
                    </category>
                
                    <category>
                        <![CDATA[ google cloud ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Nibesh Khadka ]]>
                </dc:creator>
                <pubDate>Fri, 23 Feb 2024 16:43:39 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2024/02/Addon-Assesment-Poster--4.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>As part of the <a target="_blank" href="https://appdefensealliance.dev/casa">Google CASA process</a>, developers can run static analysis on their application’s source code using an inline integration with OpenText’s Fortify Source Code Analyzer (SCA) via the CASA portal.</p>
<p>Naturally, I had to prepare my source code as per instruction. In this article, I will share how I packaged and submitted my Add-on's source code in Ubuntu OS. </p>
<p>But before that, let's talk a little about Tier 2 CASA assessment.</p>
<h2 id="heading-what-is-tier-2-casa-security-assessment">What is Tier 2 CASA Security Assessment?</h2>
<p>The <a target="_blank" href="https://appdefensealliance.dev/casa">Tier 2 CASA</a> (Cloud Application Security Assessment) is a self-service security assessment process for applicants seeking access to Google Workspace data or to comply with specific Google Workspace policies. </p>
<p>It allows developers to scan their applications and submit the results for verification without an external assessor accessing the code or infrastructure.</p>
<h3 id="heading-importance-tier-2-casa-security-assessment">Importance Tier 2 CASA security assessment</h3>
<p>Tier 2 CASA is important for several reasons:</p>
<ul>
<li><strong>Security Assurance:</strong> It provides independent verification of your application's security posture, reducing the risk of data breaches and protecting user privacy.</li>
<li><strong>Compliance:</strong> It helps meet security requirements for accessing Google Workspace data or adhering to Google policies, like the Workspace Marketplace Terms of Service.</li>
<li><strong>Efficiency:</strong> It's a faster and more cost-effective alternative to Tier 1 assessments, which involves external assessors directly examining your application.</li>
<li><strong>Trust</strong>: If your addon is published without verification it'll display an "unverified" message to the clients while installing the addon, which creates distrust and can lead to the installation process of your addon to be abandoned.</li>
</ul>
<p>In the context of my Google Workspace Addon <a target="_blank" href="https://appdefensealliance.dev/casa">Scan Me</a>, the use of <a target="_blank" href="https://developers.google.com/apps-script/add-ons/concepts/editor-scopes#restricted_scopes">restrictive</a> OAuth scope  <a target="_blank" href="https://developers.google.com/identity/protocols/oauth2/scopes#drive"><code>auth/drive</code></a> of Google Drive API likely triggered the need for a Tier 2 assessment. This scope grants your addon access to see, edit, create, and delete all of your Google Drive files, which falls under Google's security and privacy requirements.</p>
<h3 id="heading-additional-resources">Additional Resources</h3>
<ul>
<li><strong><a target="_blank" href="https://appdefensealliance.dev/casa/tier-2/getting-started">CASA Tier 2 Overview</a></strong></li>
<li><strong><a target="_blank" href="https://tacsecurity.com/google-casa-cloud-application-security-assessment/">CASA Documentation</a></strong></li>
<li><strong><a target="_blank" href="https://workspace.google.com/terms/marketplace/tos/">Google Workspace Marketplace Terms of Service</a></strong></li>
</ul>
<p><strong>Disclaimer</strong>: While I'll explain the Tier 2 CASA process<a target="_blank" href="https://workspace.google.com/marketplace/app/scan_me/613697866593">,</a> it's crucial to consult the official documentation and Google's security guidelines for specific requirements and guidance.</p>
<p>The assessment certification is free, by the way. To prepare your addon for the CASA assessment process follow the following steps.</p>
<h2 id="heading-step-1-sign-up-for-the-new-assessment-procedure"><strong>Step 1</strong> – <strong>Sign up for the new Assessment Procedure</strong></h2>
<p>If you're using restrictive scopes, you'll receive an email from Google's Verification team at some point requesting to verify the scopes after you've submitted your add-on for verification. </p>
<p>This email is the notification document. So, you need to download this email as a PDF, which must be submitted in the application form later on.</p>
<p>In that email, you'll find the following instructions for Tier 2 evaluation. You'll find a link to <a target="_blank" href="https://rc.products.pwc.com/login/casa/register">register</a> or <a target="_blank" href="https://rc.products.pwc.com/login/casa/">log-in</a> to the CASA portal. Click the link and register to the site. Then click on <strong>Start New Assessment&gt; Create New Assessment.</strong></p>
<p>Fill in the information asked carefully. Upload the previously downloaded email where you're asked for a Tier 2 notification pdf.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1708599483954/704e9bf1-ac25-414d-b3f7-dcda721a82fd.png?auto=compress,format&amp;format=webp&amp;auto=compress,format&amp;format=webp" alt="Image" width="1748" height="1240" loading="lazy">
<em>Starting New CASA Assesment of the Addon</em></p>
<p><strong>Note</strong>: For Google Workspace Addon, the type of application is <strong>Local App</strong>.</p>
<p><strong>Caution</strong>: As shown in the image above, even though "<strong>Project ID</strong>" is asked in the input field, they are asking for the <strong>Project Number</strong> included in the email, not the <strong>Project ID</strong> of your Google Cloud Console project.</p>
<p>After you carefully fill in the details and submit the form, you'll arrive at a new screen – <strong>Application Screening</strong> – where there are two things that you should download:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1708599799176/52631f9a-8719-472e-997e-2169d1063127.png?auto=compress,format&amp;format=webp&amp;auto=compress,format&amp;format=webp" alt="Image" width="915" height="163" loading="lazy">
<em>Download Scan Cenral Package and Setup Insruction</em></p>
<ol>
<li>Fortify Scan Central Package.</li>
<li>Instruction on compressing your application's source code for initial assessment.</li>
</ol>
<h2 id="heading-step-2-download-and-setup-java-jdk"><strong>Step 2</strong> – <strong>Download and Setup Java JDK</strong></h2>
<p>To use the Scan Central package as mentioned in the instructions, a minimum of JDK 11 is required. </p>
<p>For setting up the path for the Java environment in Linux, I followed <a target="_blank" href="https://stackoverflow.com/a/73414921/6163929">this</a> instruction on StackOverflow.</p>
<h2 id="heading-step-3-setup-path-for-scan-central"><strong>Step 3</strong> – <strong>Setup Path for Scan Central</strong></h2>
<p>Now let's add the path to the Scan Central in our system.</p>
<p>In your CLI, open <code>.bashrc</code> file with <code>sudo nano ~/.bashrc</code>. Append the following path at the end of the file:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># SCAN Central </span>
<span class="hljs-comment"># Path looks like following</span>
<span class="hljs-comment">#/home/&lt;username&gt;/Fortify_ScanCentral_Client_22.2.1_x64/bin</span>

 <span class="hljs-built_in">export</span> PATH=<span class="hljs-variable">$PATH</span>:&lt;Path To bin folder <span class="hljs-keyword">in</span> Scan Central&gt;
</code></pre>
<p>Save (CTRL+S) and exit (CTRL + X) the file.</p>
<p>Open <code>.profile</code> with <code>sudo nano ~/.profile</code> and add the same path as above. You can check the version of Scan Central in your CLI with the command <code>scancentral -version</code>, to make sure the setup was successful.</p>
<h2 id="heading-step-4-packaging-source-code-for-assessment"><strong>Step 4</strong> – <strong>Packaging Source Code for</strong> A<strong>ssessment</strong></h2>
<p>To package the source code for your Google Workspace Addon, go to the root directory of your project. If you're following the instruction manual, go to the section for JavaScript code packaging.</p>
<p>In the root directory run any of the following commands:</p>
<pre><code class="lang-bash"><span class="hljs-comment">#cmd 1 </span>
scancentral package -bt none -o myPackage.zip
<span class="hljs-comment"># or cmd 2</span>
scancentral package -bt none --scan-node-modules -o myPackage.zip
</code></pre>
<p><strong>Note</strong>: The command <code>scancentral.bat</code> is for Windows users.</p>
<p>As mentioned in the instruction, command 2 increases the size of the package and is not necessary for Node.js or Angular. I created Workspace Addon so I don't have node-modules in my source code.</p>
<p>After that, you'll see a compressed package named <strong>myPackage</strong> in the directory where you ran the packaging operation.</p>
<h2 id="heading-step-5-initiate-the-scan-process"><strong>Step 5</strong> – <strong>Initiate the Scan Process</strong></h2>
<p>After packaging, go back to the CASA portal and click on your assessment ID link in the list, and open up the <strong>Application Screening</strong> window. Here:</p>
<ol>
<li>Click the <strong>Begin Scan Process</strong> button.</li>
<li>Upload the package you just compressed.</li>
<li>Click the <strong>Upload File &amp; Initiate Scan</strong> button.</li>
</ol>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/02/casa-form-filling--2.png" alt="Image" width="600" height="400" loading="lazy">
<em>Upload Source Code To Fortify Scan</em></p>
<p>This will initiate auto scanning of your application which is the beginning of assessment for your Addon.</p>
<p><strong>Reminder</strong>: As I've personally experienced, if your source code uses the <code>Math.random()</code> method, then the auto-scanner will not pass your code.</p>
<p>If you pass this phase, the manual verification process will begin where you'll have to fill in forms for the survey. Go to this <a target="_blank" href="https://lookerstudio.google.com/u/0/reporting/757d8fab-9682-4b74-9acc-58efb5e3081c/page/p_ana6axxq4c?s=tug3GYx0bmg">link</a> for the questions that'll be asked in the CASA survey. Here, choose the <strong>Local App</strong> option for App Type for a Google Workspace Add-on. I want to remind you that they will change based on the answer provided.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Alright, I'm hoping this blog helped you reduce the time and confusion that I had to encounter when I was trying to assess my <a target="_blank" href="https://workspace.google.com/marketplace/app/scan_me/613697866593">addon</a>. And please don't give up midway during the evaluation otherwise your months of hard work will be in vain.</p>
<p>My addon <a target="_blank" href="https://workspace.google.com/marketplace/app/scan_me/613697866593">Scan Me</a>, scans the Google Drive and prepares an audit report in a spreadsheet file of your choosing in your Google Drive. It makes it extremely easy for you to analyze your Google Drive from one place, and it also offers a free quota. If you're looking for a similar addon I hope you'll try this addon. </p>
<p>This is Nibesh khadka, have a good day.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Create a Search Form in Google Sheets – Google Apps Script Tutorial ]]>
                </title>
                <description>
                    <![CDATA[ Have you ever struggled with navigating a massive spreadsheet? You can do away with the endless scrolling and unlock the power of targeted data retrieval with a custom search form using Google Apps Script.  In this hands-on guide, we'll craft a searc... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/create-search-form-in-google-sheets/</link>
                <guid isPermaLink="false">66ba5b06bab56b9458240037</guid>
                
                    <category>
                        <![CDATA[ google apps script ]]>
                    </category>
                
                    <category>
                        <![CDATA[ google sheets ]]>
                    </category>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Nibesh Khadka ]]>
                </dc:creator>
                <pubDate>Fri, 09 Feb 2024 15:39:47 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2024/02/Search-Form-Thumbnail-3.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Have you ever struggled with navigating a massive spreadsheet? You can do away with the endless scrolling and unlock the power of targeted data retrieval with a custom search form using Google Apps Script. </p>
<p>In this hands-on guide, we'll craft a search tool that seamlessly integrates with your spreadsheet, allowing you to:</p>
<ul>
<li><strong>Search across multiple tabs:</strong> Effortlessly query data from different sections of your spreadsheet with dedicated input fields.</li>
<li><strong>Master AND &amp; OR searches:</strong> Find exactly what you need with both <strong>AND</strong> and <strong>OR</strong> search functionalities, ensuring precise or flexible matching based on your requirements.</li>
<li><strong>Boost your productivity:</strong> Save valuable time by eliminating manual searches and filtering through only the relevant data points.</li>
</ul>
<p>Ready to transform your spreadsheet into a dynamic search hub? Follow along as we explore the world of Google Apps Script and empower you to become a spreadsheet search master!</p>
<p>You can find all the code and related assets in <a target="_blank" href="https://github.com/nibukdk/Search-Form-In-Google-Sheets">this GitHub repo</a>.</p>
<div class="embed-wrapper">
        <iframe width="560" height="315" src="https://www.youtube.com/embed/FkYW2oEbEQk" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>
<p></p><h2 id="toc">Table Of Contents</h2><p></p>
<ul>
  <li>
    <a href="#understanding-the-spreadsheet-structure">Understanding The Spreadsheet Structure</a>
  </li>
  <li>
    <a href="#how-to-build-the-search-form-dynamic-dropdowns-and-logic">How To Build The Search Form – Dynamic Dropdowns And Logic</a>
  </li>
  <li><a href="#and-vs-or-search">AND Vs OR Search</a></li>
  <li>
    <a href="#how-to-the-create-search-function-with-google-apps-script">How To The Create Search Function With Google Apps Script</a>
  </li>
  <li>
    <a href="#how-to-create-the-andsearch-function">How To Create The andsearch Function</a>
  </li>
  <li>
    <a href="#how-to-match-all-criteria-with-and-search">How To Match All Criteria with And Search</a>
  </li>
  <li>
    <a href="#bringing-search-results-to-life">Bringing Search Results To Life</a>
  </li>
  <li>
    <a href="#how-to-deduplicate-data-to-ensure-accuracy">How To Deduplicate Data To Ensure Accuracy</a>
  </li>
  <li>
    <a href="#putting-it-all-together-displaying-your-search-results">Putting It All Together – Displaying Your Search Results</a>
  </li>
  <li>
    <a href="#how-to-use-or-search-to-find-data-that-matches-any-term">How To Use Or Search To Find Data that Matches Any Term</a>
  </li>
  <li>
    <a href="#keeping-users-informed-toast-messages-for-seamless-search">Keeping Users Informed – Toast Messages for Seamless Search</a>
  </li>
  <li>
    <a href="#putting-it-all-together-testing-your-search-form">Putting It All Together – Testing Your Search Form</a>
  </li>
  <li>
    <a href="#congratulations-you-ve-built-a-powerful-search-engine-in-google-sheets-">Congratulations! You've Built a Powerful Search Engine in Google Sheets!
    </a>
  </li>
  <li>
    <a href="#exploring-customization-options">Exploring Customization Options</a>
  </li>
</ul>


<h2 id="heading-understanding-the-spreadsheet-structure">Understanding the Spreadsheet Structure</h2>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/02/sheet_tab.png" alt="Image" width="600" height="400" loading="lazy">
<em>Structure Of The Spreadsheet</em></p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/02/TABS.png" alt="Image" width="600" height="400" loading="lazy">
<em>Different Tabs In Spreadsheets</em></p>
<p>As portrayed in the image above, there are five tabs in the spreadsheet. The data is divided into three tabs by year: 2021, 2022 &amp; 2023.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/02/header_row.png" alt="Image" width="600" height="400" loading="lazy">
<em>Columns in 2021, 2022 &amp; 2023 in Spreadsheet</em></p>
<p>All the columns are the same in these three tabs.</p>
<p>Let's look at the structure of your dedicated search tab. It's divided into two key sections:</p>
<ol>
<li><strong>Search Form (Rows 1-7):</strong> This is where you interact with your data. Each input field corresponds to a specific column in your other spreadsheet tabs, allowing you to tailor your search queries. Think of them as filters, helping you hone in on the information you need.</li>
<li><strong>Search Results (Rows 8+):</strong> This is where you'll find the data you sought. Each result includes the relevant information you specified in your search, along with an additional column named "Sprd Name - Row Index." This acts as a convenient map, pinpointing the exact spreadsheet tab and row where each result originates from. No more hunting through endless rows – you'll be laser-focused on the data you need.</li>
</ol>
<p>By understanding this organized layout, you can navigate your search experience efficiently and retrieve the information you require swiftly.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/02/search_form_2.png" alt="Image" width="600" height="400" loading="lazy">
<em>Search Form</em></p>
<p>### </p>
<h3 id="heading-how-to-build-the-search-form-dynamic-dropdowns-and-logic">How to Build the Search Form – Dynamic Dropdowns and Logic</h3>
<p>The search form has three input fields: Client, Quantity, and Description. Each utilizes a dropdown menu automatically populated with unique values from the Config spreadsheet tab. But how does this magic happen?</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1706803411374/7a8a5da4-b71a-4e35-b8d2-750cb611f23b.png" alt="Image" width="285" height="514" loading="lazy">
<em>Data Validation Of Dropdown Inputs</em></p>
<p>Here's what happens behind the scenes: </p>
<ol>
<li><strong>Data Source:</strong> Values for the dropdowns are meticulously collected from three separate tabs: 2021, 2022, and 2023.</li>
<li><strong>Combining Forces:</strong> A clever formula merges these values into a single, consolidated list.</li>
<li><strong>Splitting it Up:</strong> This combined list is then transformed into an array, allowing individual values to be accessed.</li>
<li><strong>Rearranging the Data:</strong> Transposition magic turns the row of values into a column, making them easier to work with.</li>
<li><strong>Duplicate Removal:</strong> The <code>UNIQUE</code> function eliminates any repeated values, ensuring a concise and organized list.</li>
<li><strong>Sorting it Out:</strong> Finally, the remaining values are sorted alphabetically for your browsing convenience.</li>
</ol>
<p>Here's the formula used: <code>SORT(UNIQUE(TRANSPOSE(split(TEXTJOIN(",",TRUE,'2021'!A2:A1001)&amp;","&amp;TEXTJOIN(",",TRUE,'2022'!A2:A1001)&amp;","&amp;TEXTJOIN(",",TRUE,'2023'!A2:A1001),","))))</code></p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/02/config_tab.png" alt="Image" width="600" height="400" loading="lazy">
<em>Config Sheets</em></p>
<h3 id="heading-and-vs-or-search">AND vs OR Search</h3>
<p>A dedicated checkbox (located in G4:G5) serves as the control center for your search logic. When checked, it activates the <strong>AND</strong> search, requiring all specified criteria to be present in the results. </p>
<p>Leaving it unchecked switches to the <strong>OR</strong> search, providing more flexible results as long as any criterion matches.</p>
<p>Remember, the downloadable spreadsheet retains all the pre-configured formulas and data validation rules, making setup a breeze. We'll dive into crafting the magical search function in the next step!</p>
<h2 id="heading-how-to-the-create-search-function-with-google-apps-script">How to the Create Search Function With Google Apps Script</h2>
<p>Open script editor from <strong>Extensions&gt;Apps Script</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1706803889387/4802b8d2-965f-4c14-8daa-efd0512a1c06.png" alt="Image" width="668" height="482" loading="lazy">
<em>Open Apps Script From Sheets</em></p>
<p>For this project, you'll create two files <strong>search.gs</strong> and <strong>utils.gs</strong> in the editor.</p>
<p>Inside the <strong>search.gs</strong> file, let's first fetch our spreadsheet and input terms.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">var</span> ss = SpreadsheetApp.getActiveSpreadsheet();
<span class="hljs-keyword">var</span> searchSheet = ss.getSheetByName(<span class="hljs-string">"search"</span>);
<span class="hljs-keyword">var</span> _2023Sheet = ss.getSheetByName(<span class="hljs-string">"2023"</span>);
<span class="hljs-keyword">var</span> _2022Sheet = ss.getSheetByName(<span class="hljs-string">"2022"</span>);
<span class="hljs-keyword">var</span> _2021Sheet = ss.getSheetByName(<span class="hljs-string">"2021"</span>);
<span class="hljs-comment">// ranges for name, description and quantity columns for each tab</span>
<span class="hljs-keyword">var</span> nameRangeNotation = <span class="hljs-string">'A2:A'</span>
<span class="hljs-keyword">var</span> descriptionRangeNotation = <span class="hljs-string">'F2:F'</span>
<span class="hljs-keyword">var</span> quantityRangeNotation = <span class="hljs-string">'E2:E'</span>
<span class="hljs-comment">// value for input boxes</span>
<span class="hljs-keyword">var</span> clientName = searchSheet.getRange(<span class="hljs-string">'B2:C2'</span>).getValue();
<span class="hljs-keyword">var</span> quantity = searchSheet.getRange(<span class="hljs-string">'E2'</span>).getValue();
<span class="hljs-keyword">var</span> description = searchSheet.getRange(<span class="hljs-string">'G2:H2'</span>).getValue();
<span class="hljs-keyword">var</span> hasIncludeAllSelected = searchSheet.getRange(<span class="hljs-string">'G4:G5'</span>).getValue();
</code></pre>
<p>Now below this code, we'll create the function <code>search</code>, which will orchestrate everything from the top.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">/**
 * The main function assigned to search button in the spreadsheet. It orchestrates search opearaion.
 */</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">search</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">try</span> {

    <span class="hljs-keyword">let</span> status;

    <span class="hljs-keyword">if</span> (hasIncludeAllSelected) {
      <span class="hljs-comment">//perform AND search</span>
      <span class="hljs-keyword">const</span> newData = andSearch(clientName, description, quantity);

    }
    <span class="hljs-keyword">else</span> {
         }



  } <span class="hljs-keyword">catch</span> (e) {
    <span class="hljs-built_in">console</span>.log(e)

  }
}
</code></pre>
<p>In this project, we'll build our functions one step at a time. Let's start by determining the search type based on the checkbox in G4:G5.</p>
<p>If the box is checked, we'll activate the <strong>AND</strong> search functionality. This means all specified criteria in the input fields must be present in the results. To handle this, we'll call a dedicated function named <code>andSearch()</code>. </p>
<p>We'll create this function next, directly below the existing <code>search</code> function.</p>
<p>This approach ensures our script adapts to the user's chosen search type, providing accurate and relevant results based on their needs.</p>
<h3 id="heading-how-to-create-the-andsearch-function">How to Create the <code>andSearch()</code> Function</h3>
<pre><code class="lang-javascript">
<span class="hljs-comment">/**
 * Performs "AND" search for the given keywords in their respective columns Last Name, Descroption and Quantity for 
 * tabs 2021, 2022, 2023. Returns new nested arrays for search results to be filled in search spreadsheet.
 * <span class="hljs-doctag">@param <span class="hljs-type">{String}</span> </span>name 
 * <span class="hljs-doctag">@param <span class="hljs-type">{String}</span> </span>description 
 * <span class="hljs-doctag">@param <span class="hljs-type">{String}</span> </span>quantity 
 * <span class="hljs-doctag">@returns <span class="hljs-type">{Array&lt;Array&lt;String&gt;&gt;?}</span> </span>- [[],[],[]]
 */</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">andSearch</span>(<span class="hljs-params">name = null, description = null, quantity = null</span>) </span>{

  <span class="hljs-comment">// get matching index for each sheet.</span>
  <span class="hljs-keyword">const</span> _2021SheetNameSearchIndexes = name === <span class="hljs-string">""</span> ? [] : searchSheetByColumn(_2021Sheet, nameRangeNotation, name);
  <span class="hljs-keyword">const</span> _2021SheetQuantitySearchIndexes = quantity === <span class="hljs-string">""</span> ? [] : searchSheetByColumn(_2021Sheet, quantityRangeNotation, quantity);
  <span class="hljs-keyword">const</span> _2021SheetDescriptionSearchIndexes = description === <span class="hljs-string">""</span> ? [] : searchSheetByColumn(_2021Sheet, descriptionRangeNotation, description);


  <span class="hljs-keyword">const</span> _2022SheetNameSearchIndexes = name === <span class="hljs-string">""</span> ? [] : searchSheetByColumn(_2022Sheet, nameRangeNotation, name);
  <span class="hljs-keyword">const</span> _2022SheetQuantitySearchIndexes = quantity === <span class="hljs-string">""</span> ? [] : searchSheetByColumn(_2022Sheet, quantityRangeNotation, quantity);
  <span class="hljs-keyword">const</span> _2022SheetDescriptionSearchIndexes = description === <span class="hljs-string">""</span> ? [] : searchSheetByColumn(_2022Sheet, descriptionRangeNotation, description);

  <span class="hljs-keyword">const</span> _2023SheetNameSearchIndexes = name === <span class="hljs-string">""</span> ? [] : searchSheetByColumn(_2023Sheet, nameRangeNotation, name);
  <span class="hljs-keyword">const</span> _2023SheetQuantitySearchIndexes = quantity === <span class="hljs-string">""</span> ? [] : searchSheetByColumn(_2023Sheet, quantityRangeNotation, quantity);
  <span class="hljs-keyword">const</span> _2023SheetDescriptionSearchIndexes = description === <span class="hljs-string">""</span> ? [] : searchSheetByColumn(_2023Sheet, descriptionRangeNotation, description);


 <span class="hljs-comment">//.... continue</span>

}
</code></pre>
<p>This function takes three parameters, each corresponding to a user-defined search term: name, description and quantity.</p>
<p>If any of these search terms are blank, we simply assign an empty array as the result. This efficiently handles scenarios where users leave certain fields unfilled.</p>
<p>The core logic relies on the <code>searchSheetByColumn</code> function. Think of it as a data detective that checks specific columns within spreadsheet tabs. It takes three crucial arguments:</p>
<ul>
<li><strong>sheet</strong>: The specific tab to search within (for example: "2021").</li>
<li><strong>rangeNotation</strong>: The column range to target (for example: "A2:A").</li>
<li><strong>searchVal</strong>: The value to match within the chosen column (for example: "Khadka").</li>
</ul>
<p>Using this information, <code>searchSheetByColumn</code> scans the designated column and returns an array containing the row indexes where the <code>searchVal</code> is found in that sheet.</p>
<h4 id="heading-searching-input-value-in-a-column">Searching Input Value In a Column</h4>
<p>Let's create the function <code>searchSheetByColumn</code> in the <strong>utils.gs</strong> file now.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">/**
 * Searches the given keyword in the given column inside the given spreadsheet tab.
 * It returns all the matching indexes of data. Indexes are index from array not row.
 * <span class="hljs-doctag">@param <span class="hljs-type">{Spreadsheet}</span> <span class="hljs-variable">sheet</span></span> - sheet to search from
 * <span class="hljs-doctag">@param <span class="hljs-type">{String}</span> <span class="hljs-variable">rangeNotation</span></span> - range of the column in the given spreadsheet
 * <span class="hljs-doctag">@param <span class="hljs-type">{String}</span> <span class="hljs-variable">searchVal</span></span> - keyword to search
 * <span class="hljs-doctag">@returns <span class="hljs-type">{Array&lt;number&gt;}</span> </span>- [1,23,12,45,12] 
 */</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">searchSheetByColumn</span>(<span class="hljs-params">sheet, rangeNotation, searchVal</span>) </span>{
  <span class="hljs-keyword">const</span> data = sheet.getRange(rangeNotation).getValues().flat().filter(<span class="hljs-built_in">String</span>); <span class="hljs-comment">// get data</span>
  <span class="hljs-keyword">if</span> (data.length &lt; <span class="hljs-number">1</span>) <span class="hljs-keyword">return</span> [];
  <span class="hljs-comment">// filter only matching rows indexes</span>
  <span class="hljs-comment">// got from https://stackoverflow.com/a/58980987/6163929</span>
  <span class="hljs-keyword">const</span> allIndexes = data.map(<span class="hljs-function">(<span class="hljs-params">val, index</span>) =&gt;</span> ({ val, index }))
    .filter(<span class="hljs-function">(<span class="hljs-params">{ val, index }</span>) =&gt;</span> rangeNotation === quantityRangeNotation ? <span class="hljs-built_in">Number</span>(val) === <span class="hljs-built_in">Number</span>(searchVal) : val.toLowerCase().includes(searchVal.toLowerCase())
    )
    .map(<span class="hljs-function">(<span class="hljs-params">{ val, index }</span>) =&gt;</span>
      index + <span class="hljs-number">1</span>
    ) <span class="hljs-comment">// +1 because we extract data from second row in notation later on have to match with whole data array</span>
  <span class="hljs-keyword">return</span> allIndexes;
}
</code></pre>
<p>The code above does the following:</p>
<ul>
<li>Retrieves data from the specified range and sheet using <code>sheet.getRange(rangeNotation).getValues().flat()</code>.</li>
<li>Removes empty values by filtering with <code>filter(String)</code>.</li>
<li>Iterates through data and indexes and applies <code>map</code> to create an array of objects with both values and their corresponding indexes.</li>
<li>Converts both search term and data values to numbers using <code>Number()</code>.</li>
<li>Filters for exact matches using <code>rangeNotation === quantityRangeNotation ? Number(val) === Number(searchVal)</code></li>
<li>Converts both search term and data values to lowercase.</li>
<li>Filters for matches using <code>val.toLowerCase().includes(searchVal.toLowerCase())</code></li>
<li>Extracts matching indexes using <code>map(({ val, index }) =&gt; index + 1)</code>.</li>
<li>Adds 1 to correct for starting extraction from the second row.</li>
</ul>
<h3 id="heading-how-to-match-all-criteria-with-and-search">How to Match All Criteria with AND Search</h3>
<p> Add the following piece of code in <code>andSearch</code> function.</p>
<pre><code class="lang-javascript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">andSearch</span>(<span class="hljs-params">name = null, description = null, quantity = null</span>) </span>{

<span class="hljs-comment">// ..... continuing on from previous codes</span>
  <span class="hljs-comment">// matching indexes of rows in AND search</span>
  <span class="hljs-keyword">const</span> _2021SheetMatchingRowsIndexes = filterRowsIndexesWithAllSearchTerms(_2021SheetNameSearchIndexes, _2021SheetQuantitySearchIndexes, _2021SheetDescriptionSearchIndexes);
  <span class="hljs-keyword">const</span> _2022SheetMatchingRowsIndexes = filterRowsIndexesWithAllSearchTerms(_2022SheetNameSearchIndexes, _2022SheetQuantitySearchIndexes, _2022SheetDescriptionSearchIndexes);
  <span class="hljs-keyword">const</span> _2023SheetMatchingRowsIndexes = filterRowsIndexesWithAllSearchTerms(_2023SheetNameSearchIndexes, _2023SheetQuantitySearchIndexes, _2023SheetDescriptionSearchIndexes);
<span class="hljs-comment">//..........</span>
}
</code></pre>
<p>Remember the <strong>AND</strong> search? Its goal is to unearth data points that tick every box you've specified. To achieve this, we need to filter only rows that contain all your search terms – name, quantity, and description – across all three spreadsheets.</p>
<p>Enter the <code>filterRowsIndexesWithAllSearchTerms</code> function, to be created in the <strong>utils.gs</strong> file. This handy tool combs through each row and ensures it matches every criterion you've laid out. So, how does it work its magic? We'll explore the code next!</p>
<pre><code class="lang-javascript"><span class="hljs-comment">/**
 * Function filters only rows that consist all three keywords provided by user input
 * <span class="hljs-doctag">@param <span class="hljs-type">{Array&lt;String&gt;}</span> </span>arr1 
 * <span class="hljs-doctag">@param <span class="hljs-type">{Array&lt;String&gt;}</span> </span>arr2 
 * <span class="hljs-doctag">@param <span class="hljs-type">{Array&lt;String&gt;}</span> </span>arr3 
 * <span class="hljs-doctag">@returns <span class="hljs-type">{Array&lt;String&gt;?}</span> </span>
 */</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">filterRowsIndexesWithAllSearchTerms</span>(<span class="hljs-params">arr1, arr2, arr3</span>) </span>{
  <span class="hljs-comment">// create a nested array</span>
  <span class="hljs-keyword">const</span> arr = [arr1.length &gt; <span class="hljs-number">0</span> ? [...arr1] : <span class="hljs-string">""</span>, arr2.length &gt; <span class="hljs-number">0</span> ? [...arr2] : <span class="hljs-string">""</span>, arr3.length &gt; <span class="hljs-number">0</span> ? [...arr3] : <span class="hljs-string">""</span>].filter(<span class="hljs-built_in">String</span>);

  <span class="hljs-comment">// return empty if the master arrays length is lesser than number of search terms</span>
  <span class="hljs-keyword">if</span> (arr.length &lt; <span class="hljs-number">1</span> || arr.length &lt; numberOfInputFieldEntered) <span class="hljs-keyword">return</span> [];

  <span class="hljs-keyword">const</span> matchingIndexes = [];

  <span class="hljs-keyword">if</span> (arr.length === <span class="hljs-number">3</span>) {

    arr[<span class="hljs-number">0</span>].forEach(<span class="hljs-function">(<span class="hljs-params">val</span>) =&gt;</span> {
      <span class="hljs-keyword">if</span> (arr[<span class="hljs-number">1</span>].includes(val) &amp;&amp; arr[<span class="hljs-number">2</span>].includes(val)) {
        matchingIndexes.push(val)
      }

    });

  }
  <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (arr.length === <span class="hljs-number">2</span>) {
    arr[<span class="hljs-number">0</span>].forEach(<span class="hljs-function">(<span class="hljs-params">val</span>) =&gt;</span> {
      <span class="hljs-keyword">if</span> (arr[<span class="hljs-number">1</span>].includes(val)) {
        matchingIndexes.push(val)
      }

    });


  }
  <span class="hljs-keyword">else</span> {

    matchingIndexes.push(arr[<span class="hljs-number">0</span>]) <span class="hljs-comment">//just push the array thats not empty</span>
  }
  <span class="hljs-keyword">return</span> matchingIndexes.flat();

}
</code></pre>
<p>Here's what the code does:</p>
<p>The function takes three arrays as input, each representing matching indexes from one spreadsheet based on your search terms. However, we understand users might not fill in every search field.</p>
<p>To handle this, the function first creates a "master array" containing only non-empty arrays from the three inputs. Think of it as filtering out any blank search results. <code>const arr = [arr1.length &gt; 0 ? [...arr1] : "", arr2.length &gt; 0 ? [...arr2] : "", arr3.length &gt; 0 ? [...arr3] : ""].filter(String);</code></p>
<p>If the master array ends up being empty, it means no rows matched any of your search terms – the function simply returns an empty array, indicating no results found.</p>
<p>Similarly, if the master array has fewer elements than the total search terms you entered, it signifies an incomplete <strong>AND</strong> search. In this case, the function returns an empty array, letting you know that no results match all criteria. <code>arr.length &lt; numberOfInputFieldEntered</code></p>
<p>But when all three arrays have matches, the function begins its work, it iterates through the first array, meticulously checking if each index value exists in both the second and third arrays. If it does, that index is considered a match and added to a separate "matchingIndexes" array. This guarantees that only rows containing all your search terms are included: <code>arr[0].forEach((val) =&gt; { if (arr[1].includes(val) &amp;&amp; arr[2].includes(val)) { matchingIndexes.push(val)}</code></p>
<p>If only two arrays have matches, the function performs a simpler check, verifying if each value in the first array exists in the second. Any match is added to "matchingIndexes." <code>arr[0].forEach((val) =&gt; if (arr[1].includes(val)) { matchingIndexes.push(val)}</code>.</p>
<p>Else if only one array is present, the function simply uses that array directly.</p>
<p>In summary, the function ensures that only rows containing all your chosen search terms survive – a powerful tool for precise data retrieval!</p>
<p>Next, in your <strong>search.gs</strong> file right after you declared the variable <code>hasIncludeAllSelected</code> for the checkbox, create input value counter.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">var</span> numberOfInputFieldEntered = [clientName, description, quantity].filter(<span class="hljs-built_in">String</span>).length;
</code></pre>
<p>With this, we now have indexes for the rows from the <strong>AND</strong> search. Now, continue with your <code>andSearch</code> function and get data from those indexes.</p>
<h3 id="heading-bringing-search-results-to-life">Bringing Search Results to Life</h3>
<pre><code class="lang-javascript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">andSearch</span>(<span class="hljs-params">name = null, description = null, quantity = null</span>) </span>{
<span class="hljs-comment">//.... contnung from above</span>
  <span class="hljs-comment">// get data from row indexes</span>
  <span class="hljs-keyword">const</span> _2021SheetMatchingRows = fetchDataByRowIndexes(_2021Sheet, _2021SheetMatchingRowsIndexes)
  <span class="hljs-keyword">const</span> _2022SheetMatchingRows = fetchDataByRowIndexes(_2022Sheet, _2022SheetMatchingRowsIndexes)
  <span class="hljs-keyword">const</span> _2023SheetMatchingRows = fetchDataByRowIndexes(_2023Sheet, _2023SheetMatchingRowsIndexes)
}
</code></pre>
<p>Now that we have the matching row indexes, it's time to retrieve the actual data. Enter the <code>fetchDataByRowIndexes</code> function, residing in the <strong>utils</strong> file. This handy tool serves as your data retriever, fetching information based on the provided indexes.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">/**
 * Funciton extracts the rows of provided indexes+1, from the given spreadsheet tab.
 * <span class="hljs-doctag">@param <span class="hljs-type">{Spreadsheet}</span> <span class="hljs-variable">sheet</span></span> - sheet to search from
 * <span class="hljs-doctag">@param <span class="hljs-type">{Array&lt;number&gt;}</span> <span class="hljs-variable">indexes</span></span> - indexes of row to extract values.
 * <span class="hljs-doctag">@returns <span class="hljs-type">{Array&lt;Array&lt;Srting&gt;&gt;}</span> </span>- Arrays of nested rows in the indexes from the given sheet.
 */</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">fetchDataByRowIndexes</span>(<span class="hljs-params">sheet = _2021Sheet, indexes = []</span>) </span>{
  <span class="hljs-comment">// console.log("Inside fetchDataByRowIndexes() provided indexes are:" + indexes)</span>

  <span class="hljs-keyword">if</span> (indexes.length &lt; <span class="hljs-number">1</span>) <span class="hljs-keyword">return</span> [];

  <span class="hljs-keyword">const</span> data = sheet.getDataRange().getValues();
  <span class="hljs-keyword">const</span> newData = [];

  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> i = <span class="hljs-number">0</span>; i &lt; indexes.length; i++) {
    newData.push([...data[indexes[i]], <span class="hljs-string">`<span class="hljs-subst">${sheet.getName()}</span> - <span class="hljs-subst">${indexes[i] + <span class="hljs-number">1</span>}</span>`</span>])
  }
  <span class="hljs-comment">// console.log("Inside fetchDataByRowIndexes() data from procvided indexes:" + newData)</span>
  <span class="hljs-keyword">return</span> newData;
}
</code></pre>
<p>The retrieved data isn't just dumped onto the search sheet – it gets a special touch. The function adds an extra value for the column named <code>Sprd Name - Row Indexes</code> with <code>${sheet.getName()} - ${indexes[i] + 1}</code> . </p>
<p>This column acts like a roadmap, displaying both the originating spreadsheet name and the corresponding row index for each result. So, at a glance, you know exactly where each piece of data came from.</p>
<p>Remember, this additional information is added as the last column in the search sheet. With this valuable context, search results become even more informative and easier to navigate.</p>
<h3 id="heading-how-to-deduplicate-data-to-ensure-accuracy">How to Deduplicate Data to Ensure Accuracy</h3>
<p>The next step is to ensure that our search results are free of duplicates, no matter from the sheet they originated. After all, who wants to see the same item twice? So, append this code in the <code>andSearch</code> function:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">//.. continue inside andSearch Function</span>
 <span class="hljs-comment">// filter duplicate rows</span>
  <span class="hljs-keyword">const</span> _2021SheetMatchingUniqueRows = filterDuplicateRows(_2021SheetMatchingRows);
  <span class="hljs-keyword">const</span> _2022SheetMatchingUniqueRows = filterDuplicateRows(_2022SheetMatchingRows);
  <span class="hljs-keyword">const</span> _2023SheetMatchingUniqueRows = filterDuplicateRows(_2023SheetMatchingRows);
</code></pre>
<p>To create this function let's jump back to the file <strong>utils.gs</strong> again.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">/**
 * Takes Duplicate data that might have resulted from different individual column searches and only returns unique rows 
 * in each column from the serach results.
 * <span class="hljs-doctag">@param <span class="hljs-type">{Array&lt;String&gt;}</span> </span>arr 
 * <span class="hljs-doctag">@returns <span class="hljs-type">{Array&lt;String&gt;}</span></span>- [[],[]]
 */</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">filterDuplicateRows</span>(<span class="hljs-params">arr</span>) </span>{
  <span class="hljs-keyword">if</span> (arr.length &lt; <span class="hljs-number">1</span>) <span class="hljs-keyword">return</span> [];
  <span class="hljs-keyword">const</span> delimiter = <span class="hljs-string">"*---*--*"</span>;
  <span class="hljs-comment">// console.log("Inside filterDuplicateRows() arr to check:" + arr)</span>

  <span class="hljs-keyword">const</span> strArr = arr.map(<span class="hljs-function"><span class="hljs-params">row</span> =&gt;</span> row.join(delimiter)).flat();
  <span class="hljs-comment">// console.log("Inside filterDuplicateRows() strArr:" + strArr)</span>

  <span class="hljs-keyword">const</span> uniqueArrays = [...new <span class="hljs-built_in">Set</span>(strArr)].map(<span class="hljs-function"><span class="hljs-params">str</span> =&gt;</span> str.split(delimiter))
  <span class="hljs-comment">// console.log("Inside filterDuplicateRows() uniqueArrays:" + uniqueArrays)</span>

  <span class="hljs-keyword">return</span> uniqueArrays;

}
</code></pre>
<p>Here's what we did:</p>
<ul>
<li><strong>Creating a Unique Fingerprint:</strong> We began by crafting a special "delimiter," a combination of characters highly unlikely to appear within your actual data. Think of it as a unique tag for each row.<code>const delimiter = "*---*--*";</code></li>
<li><strong>Joining Forces:</strong> Next, we mapped through each row, joining its elements with this delimiter to create a single string. This allows us to compare strings for uniqueness instead of individual data points.<code>const strArr = arr.map(row =&gt; row.join(delimiter)).flat();</code></li>
<li><strong>Duplicate Detective:</strong> We leveraged the power of JavaScript's <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set">Set</a> object, renowned for its ability to hold only unique values. By converting our string array into a Set, we effectively remove any identical entries: <code>[...new Set(strArr)]</code></li>
<li><strong>Back to Our Roots:</strong> Finally, we converted the unique strings back into an array of arrays, splitting them using the same delimiter we used earlier. This gives us a clean, deduplicated set of results. <code>map(str =&gt; str.split(delimiter))</code></li>
</ul>
<p><strong>Note:</strong> This uniqueness is based on the combined "<code>Sprd Name - Row Indexes</code>" value, ensuring true uniqueness across spreadsheets. Without this, duplicates can exist naturally.</p>
<p>With this final step, we've achieved accurate and streamlined search results, ready to be combined and presented from the <code>andSearch</code> function.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// inside andSearch function append at last</span>

  <span class="hljs-keyword">const</span> andSearchResult = [..._2023SheetMatchingUniqueRows, ..._2022SheetMatchingUniqueRows, ..._2021SheetMatchingUniqueRows]

  <span class="hljs-keyword">if</span> (andSearchResult.length &lt; <span class="hljs-number">0</span>) <span class="hljs-keyword">return</span>;
  <span class="hljs-keyword">return</span> andSearchResult;
}
</code></pre>
<p>Find the completed <code>andSearch</code> function below.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">/**
 * Performs "AND" search for the given keywords in their respective columns Last Name, Descroption and Quantity for 
 * tabs 2021, 2022, 2023. Returns new nested arrays for search results to be filled in search spreadsheet.
 * <span class="hljs-doctag">@param <span class="hljs-type">{String}</span> </span>name 
 * <span class="hljs-doctag">@param <span class="hljs-type">{String}</span> </span>description 
 * <span class="hljs-doctag">@param <span class="hljs-type">{String}</span> </span>quantity 
 * <span class="hljs-doctag">@returns <span class="hljs-type">{Array&lt;Array&lt;String&gt;&gt;?}</span> </span>- [[],[],[]]
 */</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">andSearch</span>(<span class="hljs-params">name = null, description = null, quantity = null</span>) </span>{

  <span class="hljs-comment">// get matching index for each sheet.</span>
  <span class="hljs-keyword">const</span> _2021SheetNameSearchIndexes = name === <span class="hljs-string">""</span> ? [] : searchSheetByColumn(_2021Sheet, nameRangeNotation, name);
  <span class="hljs-keyword">const</span> _2021SheetQuantitySearchIndexes = quantity === <span class="hljs-string">""</span> ? [] : searchSheetByColumn(_2021Sheet, quantityRangeNotation, quantity);
  <span class="hljs-keyword">const</span> _2021SheetDescriptionSearchIndexes = description === <span class="hljs-string">""</span> ? [] : searchSheetByColumn(_2021Sheet, descriptionRangeNotation, description);


  <span class="hljs-keyword">const</span> _2022SheetNameSearchIndexes = name === <span class="hljs-string">""</span> ? [] : searchSheetByColumn(_2022Sheet, nameRangeNotation, name);
  <span class="hljs-keyword">const</span> _2022SheetQuantitySearchIndexes = quantity === <span class="hljs-string">""</span> ? [] : searchSheetByColumn(_2022Sheet, quantityRangeNotation, quantity);
  <span class="hljs-keyword">const</span> _2022SheetDescriptionSearchIndexes = description === <span class="hljs-string">""</span> ? [] : searchSheetByColumn(_2022Sheet, descriptionRangeNotation, description);

  <span class="hljs-keyword">const</span> _2023SheetNameSearchIndexes = name === <span class="hljs-string">""</span> ? [] : searchSheetByColumn(_2023Sheet, nameRangeNotation, name);
  <span class="hljs-keyword">const</span> _2023SheetQuantitySearchIndexes = quantity === <span class="hljs-string">""</span> ? [] : searchSheetByColumn(_2023Sheet, quantityRangeNotation, quantity);
  <span class="hljs-keyword">const</span> _2023SheetDescriptionSearchIndexes = description === <span class="hljs-string">""</span> ? [] : searchSheetByColumn(_2023Sheet, descriptionRangeNotation, description);


  <span class="hljs-comment">// matching indexes of rows in AND search</span>
  <span class="hljs-keyword">const</span> _2021SheetMatchingRowsIndexes = filterRowsIndexesWithAllSearchTerms(_2021SheetNameSearchIndexes, _2021SheetQuantitySearchIndexes, _2021SheetDescriptionSearchIndexes);
  <span class="hljs-keyword">const</span> _2022SheetMatchingRowsIndexes = filterRowsIndexesWithAllSearchTerms(_2022SheetNameSearchIndexes, _2022SheetQuantitySearchIndexes, _2022SheetDescriptionSearchIndexes);
  <span class="hljs-keyword">const</span> _2023SheetMatchingRowsIndexes = filterRowsIndexesWithAllSearchTerms(_2023SheetNameSearchIndexes, _2023SheetQuantitySearchIndexes, _2023SheetDescriptionSearchIndexes);

  <span class="hljs-comment">// get data from row indexes</span>
  <span class="hljs-keyword">const</span> _2021SheetMatchingRows = fetchDataByRowIndexes(_2021Sheet, _2021SheetMatchingRowsIndexes)
  <span class="hljs-keyword">const</span> _2022SheetMatchingRows = fetchDataByRowIndexes(_2022Sheet, _2022SheetMatchingRowsIndexes)
  <span class="hljs-keyword">const</span> _2023SheetMatchingRows = fetchDataByRowIndexes(_2023Sheet, _2023SheetMatchingRowsIndexes)

  <span class="hljs-comment">// filter duplicate rows</span>
  <span class="hljs-keyword">const</span> _2021SheetMatchingUniqueRows = filterDuplicateRows(_2021SheetMatchingRows);
  <span class="hljs-keyword">const</span> _2022SheetMatchingUniqueRows = filterDuplicateRows(_2022SheetMatchingRows);
  <span class="hljs-keyword">const</span> _2023SheetMatchingUniqueRows = filterDuplicateRows(_2023SheetMatchingRows);


  <span class="hljs-keyword">const</span> andSearchResult = [..._2023SheetMatchingUniqueRows, ..._2022SheetMatchingUniqueRows, ..._2021SheetMatchingUniqueRows]

  <span class="hljs-keyword">if</span> (andSearchResult.length &lt; <span class="hljs-number">0</span>) <span class="hljs-keyword">return</span>;

  <span class="hljs-keyword">return</span> andSearchResult;

}
</code></pre>
<h3 id="heading-putting-it-all-together-displaying-your-search-results">Putting It All Together – Displaying Your Search Results</h3>
<p>Now that we can retrieve search results based on your "AND" criteria, it's time to integrate them into your <code>search</code> function. </p>
<p>We'll continue from where we left off. In the <code>if</code> block, add the following code.</p>
<pre><code class="lang-javascript"> <span class="hljs-keyword">if</span> (hasIncludeAllSelected) {
      <span class="hljs-comment">//perform AND search</span>
      <span class="hljs-keyword">const</span> newData = andSearch(clientName, description, quantity);
      <span class="hljs-comment">// ..........................</span>
      <span class="hljs-comment">// new peice of code </span>
       status = fillSearchWithResults(searchSheet.getDataRange().getValues(), newData)
       <span class="hljs-comment">// ................................................</span>
}
</code></pre>
<p>Let's create a new function, <code>fillSearchWithResults</code>, residing in the <strong>utils.gs</strong> file:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">/**
 * To Fill search sheet with values
 * <span class="hljs-doctag">@param <span class="hljs-type">{Array&lt;Array&lt;Srting&gt;&gt;}</span>  <span class="hljs-variable">oldData</span></span> - previous search results data
 * <span class="hljs-doctag">@param <span class="hljs-type">{Array&lt;Array&lt;Srting&gt;&gt;}</span>  <span class="hljs-variable">newData</span></span> - new search result to fill
 */</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">fillSearchWithResults</span>(<span class="hljs-params">oldData, newData</span>) </span>{
  <span class="hljs-comment">// console.log("Inside fillSearchWithResults() old data:", oldData.length);</span>
  <span class="hljs-keyword">if</span> (oldData.length &gt;= <span class="hljs-number">8</span>) {
    searchSheet.getRange(<span class="hljs-number">8</span>, <span class="hljs-number">1</span>, oldData.length - <span class="hljs-number">7</span>, <span class="hljs-number">9</span>).clear(); <span class="hljs-comment">// clear until last filled data</span>
  }
  SpreadsheetApp.flush();
  Utilities.sleep(<span class="hljs-number">1000</span>);
  <span class="hljs-comment">// console.log("Inside fillSearchWithResults() new Data:", newData);</span>
  <span class="hljs-keyword">if</span> (newData.length &lt; <span class="hljs-number">1</span>) <span class="hljs-keyword">return</span> <span class="hljs-number">400</span>;
  searchSheet.getRange(<span class="hljs-number">8</span>, <span class="hljs-number">1</span>, newData.length, <span class="hljs-number">9</span>).setValues(newData);
  <span class="hljs-keyword">return</span> <span class="hljs-number">200</span>;
}
</code></pre>
<p>The function takes two key inputs:</p>
<ul>
<li><strong>Current Search Sheet Data:</strong> This represents the existing information displayed in your search sheet.</li>
<li><strong>New Search Results:</strong> This is the fresh data retrieved using the previously explained functions.</li>
</ul>
<p>Here's what happens step-by-step:</p>
<ol>
<li><strong>Clearing the Decks:</strong> If a previous search result exists (starting from row 8), the function clears it out to make space for the new findings.  <code>if (oldData.length &gt;= 8) { searchSheet.getRange(8, 1, oldData.length - 7, 9).clear(); }</code></li>
<li><strong>Empty Results? No Problem:</strong> If the newly retrieved search results are empty, the function returns a special code: 400. This code, which we'll use later, indicates to the user that no matching data was found. <code>if (newData.length &lt; 1) return 400</code></li>
<li><strong>Data Display Time!:</strong> If there are indeed results, the function saves them into the search sheet, starting from row 8. Additionally, it returns a different code: 200. This code signifies a successful operation, and we'll use it to show success messages to the user.</li>
</ol>
<p>With this final piece in place, your "AND" searches will effortlessly bring relevant data to your fingertips, presented neatly in your search sheet. </p>
<h3 id="heading-how-to-use-or-search-to-find-data-that-matches-any-term">How to Use OR Search to Find Data that Matches Any Term</h3>
<p>Our journey continues! After setting up the "AND" search, we can now conquer the "OR" search, allowing you to find data containing any of your specified terms.</p>
<p>In the <code>search</code> function's <code>else</code> block, we have the <code>orSearch</code> function. Its purpose is to sift through your data and identify rows containing at least one of your search terms. </p>
<p>Think of it as casting a wider net, capturing matches that meet any of your criteria.</p>
<pre><code class="lang-javascript">  <span class="hljs-keyword">else</span> {
      <span class="hljs-comment">//perform OR serach</span>
      <span class="hljs-keyword">let</span> newData = orSearch(clientName, description, quantity);

      status = fillSearchWithResults(searchSheet.getDataRange().getValues(), newData)
}
</code></pre>
<p>Create the function <code>orSearch</code> function below <code>andSearch</code> in the search file.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">/**
 * Performs "OR" search for the given keywords in their respective columns Last Name, Descroption and Quantity for 
 * tabs 2021, 2022, 2023. Returns new nested arrays for search results to be filled in search spreadsheet.
 * <span class="hljs-doctag">@param <span class="hljs-type">{String}</span> </span>name 
 * <span class="hljs-doctag">@param <span class="hljs-type">{String}</span> </span>description 
 * <span class="hljs-doctag">@param <span class="hljs-type">{String}</span> </span>quantity 
 * <span class="hljs-doctag">@returns <span class="hljs-type">{Array&lt;Array&lt;String&gt;&gt;?}</span> </span>- [[],[],[]]
 */</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">orSearch</span>(<span class="hljs-params">name = null, description = null, quantity = null</span>) </span>{
  <span class="hljs-comment">// get matching index for each sheet.</span>
  <span class="hljs-keyword">const</span> _2021SheetNameSearchIndexes = name === <span class="hljs-string">""</span> ? [] : searchSheetByColumn(_2021Sheet, nameRangeNotation, name);
  <span class="hljs-keyword">const</span> _2021SheetQuantitySearchIndexes = quantity === <span class="hljs-string">""</span> ? [] : searchSheetByColumn(_2021Sheet, quantityRangeNotation, quantity);
  <span class="hljs-keyword">const</span> _2021SheetDescriptionSearchIndexes = description === <span class="hljs-string">""</span> ? [] : searchSheetByColumn(_2021Sheet, descriptionRangeNotation, description);

  <span class="hljs-keyword">const</span> _2022SheetNameSearchIndexes = name === <span class="hljs-string">""</span> ? [] : searchSheetByColumn(_2022Sheet, nameRangeNotation, name);
  <span class="hljs-keyword">const</span> _2022SheetQuantitySearchIndexes = quantity === <span class="hljs-string">""</span> ? [] : searchSheetByColumn(_2022Sheet, quantityRangeNotation, quantity);
  <span class="hljs-keyword">const</span> _2022SheetDescriptionSearchIndexes = description === <span class="hljs-string">""</span> ? [] : searchSheetByColumn(_2022Sheet, descriptionRangeNotation, description);

  <span class="hljs-keyword">const</span> _2023SheetNameSearchIndexes = name === <span class="hljs-string">""</span> ? [] : searchSheetByColumn(_2023Sheet, nameRangeNotation, name);
  <span class="hljs-keyword">const</span> _2023SheetQuantitySearchIndexes = quantity === <span class="hljs-string">""</span> ? [] : searchSheetByColumn(_2023Sheet, quantityRangeNotation, quantity);
  <span class="hljs-keyword">const</span> _2023SheetDescriptionSearchIndexes = description === <span class="hljs-string">""</span> ? [] : searchSheetByColumn(_2023Sheet, descriptionRangeNotation, description);

  <span class="hljs-comment">// get values from those indexes</span>
  <span class="hljs-keyword">const</span> _2021SheetNameSearch = fetchDataByRowIndexes(_2021Sheet, _2021SheetNameSearchIndexes);
  <span class="hljs-keyword">const</span> _2021SheetQuantitySearch = fetchDataByRowIndexes(_2021Sheet, _2021SheetQuantitySearchIndexes);
  <span class="hljs-keyword">const</span> _2021SheetDescriptionSearch = fetchDataByRowIndexes(_2021Sheet, _2021SheetDescriptionSearchIndexes);

  <span class="hljs-keyword">const</span> _2022SheetNameSearch = fetchDataByRowIndexes(_2022Sheet, _2022SheetNameSearchIndexes);
  <span class="hljs-keyword">const</span> _2022SheetQuantitySearch = fetchDataByRowIndexes(_2022Sheet, _2022SheetQuantitySearchIndexes);
  <span class="hljs-keyword">const</span> _2022SheetDescriptionSearch = fetchDataByRowIndexes(_2022Sheet, _2022SheetDescriptionSearchIndexes);

  <span class="hljs-keyword">const</span> _2023SheetNameSearch = fetchDataByRowIndexes(_2023Sheet, _2023SheetNameSearchIndexes);
  <span class="hljs-keyword">const</span> _2023SheetQuantitySearch = fetchDataByRowIndexes(_2023Sheet, _2023SheetQuantitySearchIndexes);
  <span class="hljs-keyword">const</span> _2023SheetDescriptionSearch = fetchDataByRowIndexes(_2023Sheet, _2023SheetDescriptionSearchIndexes);



  <span class="hljs-comment">// filter duplicate rows</span>
  <span class="hljs-keyword">const</span> _2021SheetMatchingUniqueRows = filterDuplicateRows([..._2021SheetNameSearch, ..._2021SheetQuantitySearch, ..._2021SheetDescriptionSearch]);
  <span class="hljs-keyword">const</span> _2022SheetMatchingUniqueRows = filterDuplicateRows([..._2022SheetNameSearch, ..._2022SheetQuantitySearch, ..._2022SheetDescriptionSearch]);
  <span class="hljs-keyword">const</span> _2023SheetMatchingUniqueRows = filterDuplicateRows([..._2023SheetNameSearch, ..._2023SheetQuantitySearch, ..._2023SheetDescriptionSearch]);

  <span class="hljs-keyword">const</span> orSearchResult = [..._2021SheetMatchingUniqueRows, ..._2022SheetMatchingUniqueRows, ..._2023SheetMatchingUniqueRows]

  <span class="hljs-keyword">if</span> (orSearchResult.length &lt; <span class="hljs-number">0</span>) <span class="hljs-keyword">return</span>;

  <span class="hljs-keyword">return</span> orSearchResult;

}
</code></pre>
<p>Now, don't be surprised if some things look familiar! The overall structure of the <code>orSearch</code> function resembles its "AND" counterpart. However, a key difference sets them apart:</p>
<p>Since an "OR" search requires just one matching term, we can get rid of the <code>filterRowsIndexesWithAllSearchTerms</code> function. Recall that function ensured all terms were present, which isn't the case here.</p>
<p>In essence, the <code>orSearch</code> function works by iterating through each search term and its corresponding matching indexes. For each term, it retrieves the data from the spreadsheet using the familiar <code>fetchDataByRowIndexes</code> function. </p>
<p>Finally, it merges the retrieved data for all terms, creating a unified set of results, even if they come from different spreadsheets.</p>
<p>With this powerful tool in your arsenal, you can discover data points that might not have surfaced with an "AND" search, expanding your search capabilities and enriching your data exploration experience.</p>
<h3 id="heading-keeping-users-informed-toast-messages-for-seamless-search">Keeping Users Informed – Toast Messages for Seamless Search</h3>
<p>Now that our search functions are complete, let's add a crucial element: user feedback! After all, keeping users informed throughout the search process leads to a smoother experience.</p>
<p>To avoid confusion, replace the search function with this one:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">/**
 * The main function assigned to search button in the spreadsheet. It orchestrates search opearaion.
 */</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">search</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">try</span> {
    SpreadsheetApp.getActiveSpreadsheet().toast(<span class="hljs-string">"Searching Through Your Database..."</span>, <span class="hljs-string">'Searching'</span>);

    <span class="hljs-keyword">let</span> status;

    <span class="hljs-keyword">if</span> (hasIncludeAllSelected) {
      <span class="hljs-comment">//perform AND search</span>
      <span class="hljs-keyword">const</span> newData = andSearch(clientName, description, quantity);



      status = fillSearchWithResults(searchSheet.getDataRange().getValues(), newData)
      <span class="hljs-comment">// console.log(status);</span>
      <span class="hljs-keyword">if</span> (status === <span class="hljs-number">400</span>) { <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(SEARCH_STATUS.SEARCH_FAILURE); }
    }
    <span class="hljs-keyword">else</span> {
      <span class="hljs-comment">//perform OR serach</span>
      <span class="hljs-keyword">let</span> newData = orSearch(clientName, description, quantity);

      status = fillSearchWithResults(searchSheet.getDataRange().getValues(), newData)
      <span class="hljs-comment">// console.log(status);</span>

      <span class="hljs-keyword">if</span> (status === <span class="hljs-number">400</span>) { <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(SEARCH_STATUS.SEARCH_FAILURE); }
    }

    <span class="hljs-keyword">if</span> (status === <span class="hljs-number">200</span>) {
      SpreadsheetApp.getActiveSpreadsheet().toast(SEARCH_STATUS.SEARCH_SUCCESFULL, <span class="hljs-string">'Success'</span>);
    }

  } <span class="hljs-keyword">catch</span> (e) {
    <span class="hljs-comment">// console.log(e)</span>
    <span class="hljs-keyword">if</span> (e.Error === SEARCH_STATUS.SEARCH_FAILURE) {
      SpreadsheetApp.getActiveSpreadsheet().toast(SEARCH_STATUS.SEARCH_FAILURE, <span class="hljs-string">'Not Found!'</span>);

    } <span class="hljs-keyword">else</span> {
      SpreadsheetApp.getActiveSpreadsheet().toast(e, <span class="hljs-string">'Error!'</span>);

    }

  }
}
</code></pre>
<p>We'll leverage the <a target="_blank" href="https://developers.google.com/apps-script/reference/spreadsheet/spreadsheet#toastmsg,-title">toast</a> method provided by SpreadsheetApp to display brief, non-intrusive messages directly within the spreadsheet interface. Here's what we'll achieve:</p>
<p><strong>Search Initiated:</strong> As soon as the user clicks the search button, a toast message appears: "Searching Through Your Database..." This lets them know the search is underway, preventing confusion or unnecessary waiting. <code>SpreadsheetApp.getActiveSpreadsheet().toast("Searching Through Your Database...", 'Searching');</code></p>
<p><strong>Success Stories:</strong> If the search returns any result (indicated by a status code of 200), a positive toast message pops up: "Search Was Successful!" This confirms the operation's completion and reassures the user that relevant data was found.  <code>if (status === 200) { SpreadsheetApp.getActiveSpreadsheet().toast(SEARCH_STATUS.SEARCH_SUCCESFULL, 'Success'); }</code></p>
<p><strong>Empty Findings:</strong> While not technically an error, an empty search result (status code of 400) triggers a slightly different message: "No items found with the given criteria." This informs the user about the outcome without causing alarm. <code>if (status === 400) { throw new Error(SEARCH_STATUS.SEARCH_FAILURE); }</code></p>
<p>Here's what happens behind the scenes:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> SEARCH_STATUS = {
  <span class="hljs-attr">SEARCH_SUCCESFULL</span>: <span class="hljs-string">"Search Was Successfull!"</span>,
  <span class="hljs-attr">SEARCH_FAILURE</span>: <span class="hljs-string">"No items found with the given criteria."</span>,
}
</code></pre>
<ul>
<li>An "enum" called <code>SEARCH_STATUS</code> in the <strong>utils.gs</strong> file stores these message strings for easy access and maintenance.</li>
<li>A "catch block" handles any unexpected errors, ensuring the user receives appropriate feedback even in unusual situations.</li>
</ul>
<p>With these toast messages in place, your search functionality becomes more user-friendly and transparent. Remember, clear communication leads to happy user experience!</p>
<h3 id="heading-putting-it-all-together-testing-your-search-form">Putting It All Together – Testing Your Search Form</h3>
<p>Now that you've built your powerful search functions, it's time to see them in action! Follow these steps to test your search form directly within your spreadsheet:</p>
<ol>
<li><strong>Save Your Scripts:</strong> Make sure you've saved all your code files (<strong>utils.gs</strong> and <strong>search.gs</strong>) before proceeding.</li>
<li><strong>Assign the Search Function:</strong> Right-click on the Search button in your form and select "Assign script." In the popup window, type the name of the <code>search</code> function and click "OK." This links the button to your code.</li>
<li><strong>Ready, Set, Search:</strong> In your spreadsheet, experiment with different search combinations. Try entering terms in various combinations to see how the AND and OR searches yield different results.</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1707047218515/d6e37765-ad85-4b08-ac8f-b62f80148a1e.png" alt="Image" width="1748" height="1240" loading="lazy">
<em>Assign a function to button in Google Sheets</em></p>
<h3 id="heading-congratulations-youve-built-a-powerful-search-engine-in-google-sheets">Congratulations! You've Built a Powerful Search Engine in Google Sheets!</h3>
<p>You've successfully accomplished an impressive feat: crafting a customized search engine within your Google Sheets. Let's recap your achievements:</p>
<ul>
<li><strong>Seamless Search Form:</strong> You created a user-friendly search form directly in your spreadsheet, simplifying data exploration.</li>
<li><strong>Scriptable Power:</strong> You harnessed the power of Apps Script to develop functions that handle various search operations behind the scenes.</li>
<li><strong>AND &amp; OR Mastery:</strong> You implemented both AND and OR search functionalities, giving users flexibility in finding relevant data.</li>
<li><strong>Precise Matching:</strong> You designed a function that selects rows containing all specified search terms, ensuring accurate results.</li>
<li><strong>Duplicate Removal:</strong> You implemented a mechanism to eliminate duplicate entries, keeping your search results clean and concise.</li>
<li><strong>Informative Feedback:</strong> You integrated user-friendly toast messages to notify users about search progress and outcomes.</li>
</ul>
<h3 id="heading-exploring-customization-options">Exploring Customization Options</h3>
<p>You've built a fantastic search engine, but remember, the journey doesn't end here! With a bit of tweaking, you can adapt this tool to perfectly suit your specific needs and workflows. Here are some exciting possibilities to consider:</p>
<p><strong>Diversifying Your Data:</strong> Break free from the confines of a single spreadsheet! Explore integrating with diverse data sources like inventory management systems, tax databases, or even restaurant reviews. With some adjustments to your code, you can unlock a wealth of information across different platforms.</p>
<p><strong>Dynamic Search Inputs:</strong> Need more flexibility in your search criteria? Consider adding or removing input fields based on your evolving needs. This allows for more tailored searches and streamlines your data exploration process.</p>
<p><strong>Detailed Search Logs:</strong> Keep track of your search history! Implement a log box to automatically record your latest search terms and the number of results found. This can be invaluable for revisiting past searches and analyzing trends.</p>
<p><strong>Visual Appeal Matters:</strong> Enhance the user experience by giving your search form a visual makeover. Play with colors, fonts, and layout to create a more engaging and intuitive interface.</p>
<p><strong>Speed Optimizations:</strong> Every second counts! Explore ways to optimize your search functions for faster response times. This might involve code refinement, data indexing, or leveraging caching strategies.</p>
<p><strong>Taming Large Datasets:</strong> Working with massive databases? Don't worry, you've got options! Implement logic to overcome the 6-minute runtime limit of Google Apps Script functions. </p>
<p>By exploring these avenues, you can transform your basic search function into a powerful and personalized data exploration tool. Remember, the possibilities are endless!</p>
<p>PS: How much more productive (or procrastinating) will you be with this new ability?</p>
<p>I am Nibesh Khadka. Share this blog &amp; like the video if helpful! Find more of my contents at <a target="_blank" href="https://medium.com/script-portal">Script-Portal</a> (Medium) &amp; on my YouTube channel: <a target="_blank" href="https://youtube.com/@codingWithNibesh">CodingWithNibesh</a>.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Remove Empty Rows and Columns in Google Sheets ]]>
                </title>
                <description>
                    <![CDATA[ In this tutorial, you will learn how to remove empty rows and columns from Google Sheets using Google Apps Script. A while ago I wrote an article on how to remove empty rows and columns from Google Sheets.  I recently revisited that article and I now... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-remove-empty-rows-and-columns-in-google-sheets/</link>
                <guid isPermaLink="false">66ba5b0acccc49d721b6ea38</guid>
                
                    <category>
                        <![CDATA[ automation ]]>
                    </category>
                
                    <category>
                        <![CDATA[ google apps script ]]>
                    </category>
                
                    <category>
                        <![CDATA[ google sheets ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Nibesh Khadka ]]>
                </dc:creator>
                <pubDate>Thu, 07 Sep 2023 07:10:42 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2023/08/delete-empty-rows-and-columns-in-google-sheets.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>In this tutorial, you will learn how to remove empty rows and columns from Google Sheets using Google Apps Script.</p>
<p>A while ago I wrote an article on <a target="_blank" href="https://kcl.hashnode.dev/how-to-delete-empty-rows-and-columns-in-google-sheets">how to remove empty rows and columns from Google Sheets</a>. </p>
<p>I recently revisited that article and I now present you this revised version. </p>
<p>I also have a video version of this topic which you can check out below:</p>
<div class="embed-wrapper">
        <iframe width="560" height="315" src="https://www.youtube.com/embed/Eiqa5ST9DYM" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>
<h2 id="heading-what-we-will-cover">What We Will Cover</h2>
<p>You'll create two functions: <code>deleteExternalEmptyRowsNColumns()</code> and <code>deleteInternalEmptyRowsNColumns()</code>. </p>
<p>The first function will delete empty rows and columns from the range that are outside of the range returned by the method <a target="_blank" href="https://developers.google.com/apps-script/reference/spreadsheet/sheet#getdatarange"><code>getDataRange()</code></a>.</p>
<p>The second function will delete the rows and columns that are empty which are inside of the range returned by <code>getDataRange()</code>. </p>
<p>We will also create a menu so that we can execute these functions from the spreadsheet itself.</p>
<h2 id="heading-how-to-prep-the-sheet">How to Prep the Sheet</h2>
<p>My spreadsheet currently looks like the image below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1693110060718/c5e5f9e6-2ddf-4ee1-a08c-3a0a8ba4de87.png" alt="Sheets Sample" width="2367" height="1795" loading="lazy">
<em>Spreadsheet With Lots Of Empty Rows and Columns</em></p>
<p>It has a couple of columns and rows with data, with lots of blank rows and columns. </p>
<p>Let's make the spreadsheets look more presentable like the following image:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1693110269477/fd55cbfd-88f8-47b2-b0d1-69d1ced421b0.png" alt="Image" width="1920" height="1080" loading="lazy">
<em>Final Clean Spreadsheet Version</em></p>
<h2 id="heading-how-to-open-apps-script-project">How to Open Apps Script Project</h2>
<p>Next, let's open our Apps Script project from the Extensions tab in the spreadsheet:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/08/open_apps_script-2.png" alt="Image" width="600" height="400" loading="lazy">
<em>Open Apps Script Project From Spreadsheet's Tab</em></p>
<h2 id="heading-how-to-create-a-function-to-delete-empty-rows-and-columns-outside-of-datarange">How to Create a Function to Delete Empty Rows and Columns Outside of DataRange</h2>
<p>We'll create a function named <code>deleteExternalEmptyRowsNColumns()</code>. </p>
<p>This function will be responsible for deleting any empty rows and columns that are outside of the range of <code>getDataRange()</code>:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">/**
 * Delete the empty rows and columns outside of the DataRange()
 */</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">deleteExternalEmptyRowsNColumns</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-comment">// get sheets and data</span>
  <span class="hljs-keyword">const</span> ss = SpreadsheetApp.getActiveSheet();
  <span class="hljs-keyword">const</span> data = ss.getDataRange().getValues();

  <span class="hljs-comment">//console.log(data);</span>

  <span class="hljs-comment">// determine last row and column</span>
  <span class="hljs-keyword">const</span> lastRow = data.length;
  <span class="hljs-keyword">const</span> lastCol = data[<span class="hljs-number">0</span>].length;

  <span class="hljs-comment">// get maximum rows and columns sss</span>
  <span class="hljs-keyword">const</span> maxRows = ss.getMaxRows();
  <span class="hljs-keyword">const</span> maxCols = ss.getMaxColumns();

  <span class="hljs-comment">// only remove rows and columns if there are empty rows or columns beyond last row and columns</span>
  <span class="hljs-keyword">if</span> (maxRows &gt; lastRow) {
    ss.deleteRows(lastRow + <span class="hljs-number">1</span>, maxRows - lastRow);
  }
  <span class="hljs-keyword">if</span> (maxCols &gt; lastCol) {
    ss.deleteColumns(lastCol + <span class="hljs-number">1</span>, maxCols - lastCol);
  }

}
</code></pre>
<p>We're using maximum rows and maximum columns because these values will return the last row and the last column of the spreadsheet regardless of the content.</p>
<p> This means they also include empty rows and empty columns beyond the data range. </p>
<p>Next, we only remove the columns and rows if they are outside of the range.</p>
<p>This means that if the maximum row is greater than the last row, then we will remove the rows. The same goes for the columns.</p>
<p>We're using the <a target="_blank" href="https://developers.google.com/apps-script/reference/spreadsheet/sheet#deleterowsrowposition,-howmany"><code>deleteRows()</code></a> method to remove those rows, which takes two parameters.</p>
<p>The first one is the index of the row from where the rows should be deleted, <code>lastRow + 1</code> in our case.</p>
<p> The second parameter is the number of rows we should remove, which is <code>maxRows - lastRow</code> in our case. </p>
<p>For columns, we'll use the <a target="_blank" href="https://developers.google.com/apps-script/reference/spreadsheet/sheet#deletecolumnscolumnposition,-howmany"><code>deleteColumns()</code></a> method. The way this method works is the same as the way <code>deleteRows()</code> works but on columns.</p>
<p>If you run this function your spreadsheet will look similar to the following image: </p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1693112111932/eb0387b4-9b5a-49b3-9e9a-6b572d75be1e.png" alt="Image" width="1920" height="1080" loading="lazy">
<em>Spreadsheet with Out Of Bounds Columns and Rows Removed</em></p>
<p>You'll see the columns and rows that were outside of the range returned by <code>getDataRange()</code> have now been removed by the function.</p>
<h2 id="heading-how-to-create-a-function-to-delete-empty-rows-and-columns-inside-of-datarange">How to Create a Function to Delete Empty Rows and Columns Inside of DataRange</h2>
<p>Now we will create another function: <code>deleteInternalEmptyRowsNColumns()</code>.</p>
<p>This will be responsible for removing empty rows and columns which are included in the <code>getDataRange(</code>) method, with this function below:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">/**
 * Deletes the empty rows and columns inside of DataRange()
 */</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">deleteInternalEmptyRowsNColumns</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-comment">// get sheets and data</span>
  <span class="hljs-keyword">const</span> ss = SpreadsheetApp.getActiveSheet();
  <span class="hljs-keyword">const</span> data = ss.getDataRange().getValues();

  <span class="hljs-keyword">const</span> lastRow = data.length;
  <span class="hljs-keyword">const</span> lastCol = data[<span class="hljs-number">0</span>].length;

  <span class="hljs-comment">// lets check if there're any empty columns during the beginning which is included in data</span>
  <span class="hljs-keyword">const</span> emptyColumnIndexes = [];
  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> i = <span class="hljs-number">1</span>; i &lt;= lastCol; i++) {
    <span class="hljs-keyword">if</span> (ss.getRange(<span class="hljs-number">1</span>, i, lastRow, <span class="hljs-number">1</span>).getValues().flat().join(<span class="hljs-string">""</span>) === <span class="hljs-string">""</span>) {
      <span class="hljs-comment">// subtract length before pushes value with less than 1 of original index</span>
      <span class="hljs-comment">// because later on when we delete colums one by one the indexes </span>
      <span class="hljs-comment">//will be out of bounds/wrong due to sprd being updated to new indexes</span>
      emptyColumnIndexes.push(i - emptyColumnIndexes.length);

    }

  }

  <span class="hljs-comment">// lets delete these columns</span>
  <span class="hljs-keyword">if</span> (emptyColumnIndexes.length &gt; <span class="hljs-number">0</span>) {
    <span class="hljs-comment">// delete column</span>
    emptyColumnIndexes.forEach(<span class="hljs-function"><span class="hljs-params">ind</span> =&gt;</span> ss.deleteColumn(ind));

  }

  <span class="hljs-comment">//***************Remove Internal empty rows */</span>
  <span class="hljs-comment">// convert nested arrays to string and remove empty strings with filter</span>
  <span class="hljs-keyword">const</span> newData = ss.getDataRange().getValues().filter(<span class="hljs-function">(<span class="hljs-params">arr</span>) =&gt;</span> arr.join(<span class="hljs-string">""</span>) !== <span class="hljs-string">""</span>)

  <span class="hljs-keyword">const</span> newLastRow = newData.length;
  <span class="hljs-keyword">const</span> newLastCol = newData[<span class="hljs-number">0</span>].length;

  <span class="hljs-comment">// clear previous values</span>
  ss.clearContents();

  <span class="hljs-comment">// set new values</span>
  ss.getRange(<span class="hljs-number">1</span>, <span class="hljs-number">1</span>, newLastRow, newLastCol).setValues(newData);

<span class="hljs-comment">// now delete empty rows and columns </span>
 deleteExternalEmptyRowsNColumns();
}
</code></pre>
<p>Let's explain what the function does in the following sections.</p>
<h3 id="heading-how-to-remove-empty-columns">How to Remove Empty Columns</h3>
<p>First, we'll work on empty columns. After that, we'll remove the empty rows.</p>
<pre><code class="lang-javascript"> <span class="hljs-keyword">const</span> emptyColumnIndexes = [];
  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> i = <span class="hljs-number">1</span>; i &lt;= lastCol; i++) {
    <span class="hljs-keyword">if</span> (ss.getRange(<span class="hljs-number">1</span>, i, lastRow, <span class="hljs-number">1</span>).getValues().flat().join(<span class="hljs-string">""</span>) === <span class="hljs-string">""</span>) {
      <span class="hljs-comment">// subtract length before pushes value with less than 1 of original index</span>
      <span class="hljs-comment">// because later on when we delete colums one by one, the indexes </span>
      <span class="hljs-comment">//will be out of bounds/wrong due to sprd being updated to new indexes</span>
      emptyColumnIndexes.push(i - emptyColumnIndexes.length);

    }

  }

  <span class="hljs-comment">// lets delete these columns</span>
  <span class="hljs-keyword">if</span> (emptyColumnIndexes.length &gt; <span class="hljs-number">0</span>) {
    <span class="hljs-comment">// delete column</span>
    emptyColumnIndexes.forEach(<span class="hljs-function"><span class="hljs-params">ind</span> =&gt;</span> ss.deleteColumn(ind));

  }
</code></pre>
<p>Let's create an array named <code>emptyCoiumnIndexes</code>. It'll hold all the indexes of the columns that are empty. </p>
<p>To check if the columns are empty or not, we will loop through each column with a <code>for</code> loop starting from the first column.</p>
<p>Next, we will fetch the values of a column. In every loop, it will return a nested array and we will flatten the array.</p>
<p>After that, we will join the array with an empty string (""). </p>
<p>If the joined string is actually empty we know that this is an empty column so we will push that index to the empty column indexes array with the following code: <code>ss.getRange(1, i, lastRow, 1).getValues().flat().join("") === ""</code> .</p>
<p>But before pushing the index, we'll subtract the length of the <code>emptyColumnIndexes</code> array from the index itself each time. </p>
<p>That's because later on, when we delete this column, we have to delete each column one by one. </p>
<p>While doing so, we will find out that if we delete the first column, the structure of the spreadsheet changes, and the columns that come after the deleted column will have their index changed.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1693111825663/d8e82618-0404-4b89-89be-f22c33ed2d23.png" alt="Image" width="1920" height="1080" loading="lazy">
<em>Spreadsheet with Inbounds Empty Columns and Rows</em></p>
<p>For instance, from the preceding image, after we delete column "A" the index of column "F" will get changed to "E".</p>
<p>After this, if the <code>emptyColumnIndexes</code> is not empty then we will go through each value using <a target="_blank" href="https://www.w3schools.com/jsref/jsref_foreach.asp">forEach()</a>.</p>
<p> Then we will delete the column with <a target="_blank" href="https://developers.google.com/apps-script/reference/spreadsheet/sheet#deletecolumncolumnposition">deleteColumn()</a> method. </p>
<p>Now, run this function and you'll see a similar result as the following image, where all the empty columns have been removed:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1693112373678/58a09e91-924f-4cef-a5ec-e437fcf1a597.png" alt="Image" width="1201" height="1551" loading="lazy">
<em>Spreadsheet with Inbounds Empty Columns Removed</em></p>
<h3 id="heading-how-to-remove-empty-rows">How to Remove Empty Rows</h3>
<p>Now we will work on removing empty rows from our spreadsheet. </p>
<p>To do so, we will filter all the non-empty rows using the same process we used earlier by joining them with empty string.</p>
<p> If they are not just an empty quote (""), we'll return them as the array items in the <code>newData</code> with <code>const newData = ss.getDataRange().getValues().filter((arr) =&gt; arr.join("") !== "")</code>.</p>
<p>Now, we'll save those values to our spreadsheet after clearing previous contents.</p>
<p>But if you run this function right now, you'll find out that this alone will not remove empty columns but accumulate rows into one place like the image below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1693114104374/502bba63-dd4d-4d67-9635-0f2b2fc75358.png" alt="Image" width="1920" height="1080" loading="lazy">
<em>Spreadsheet with Outbound Empty Rows</em></p>
<p>This is not what we want.</p>
<p>So, to remove those extra rows, we will just call the function <code>deleteExternalEmptyRowsNColumns()</code> , we created earlier because now these extra spaces are outside of the range of <code>getDataRange()</code>. </p>
<p>Let's run the function again, and now we're be able to accomplish what we initially wanted:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1693114340988/7f0bf2d5-8f24-4e83-9389-ad6e833c2c4b.png" alt="Image" width="1920" height="1080" loading="lazy">
<em>Clean Spreadsheet</em></p>
<h2 id="heading-how-to-create-a-custom-menu-for-the-spreadsheet">How to Create a Custom Menu for the Spreadsheet</h2>
<p>Finally, we will create a menu so that we can run these functions from the spreadsheet itself. </p>
<p>For this create a new script file in your project named menu:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">/**
 * Menu creates menu UI in spreadsheet.
 */</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createCustomMenu</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">let</span> menu = SpreadsheetApp.getUi().createMenu(<span class="hljs-string">"Delete Empty Rows N Columns"</span>);

  menu.addItem(<span class="hljs-string">"Delete External Empty Rows and Columns"</span>, <span class="hljs-string">"deleteExternalEmptyRowsNColumns"</span>);
  menu.addItem(<span class="hljs-string">"Delete Internal Empty Rows"</span>, <span class="hljs-string">"deleteInternalEmptyRowsNColumns"</span>);
  menu.addToUi();
}

<span class="hljs-comment">/**
 * OnOpen trigger that creates menu
 * <span class="hljs-doctag">@param <span class="hljs-type">{Dictionary}</span> <span class="hljs-variable">e</span></span>
 */</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">onOpen</span>(<span class="hljs-params"></span>) </span>{
  createCustomMenu();
}
</code></pre>
<p>After saving the script, we will go to the spreadsheet and reload it. </p>
<p>After a moment, you'll be able to see a menu in the spreadsheet tab like the image below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1693114629659/3fc2f151-12c9-47d7-abc8-549b2937d9e7.png" alt="Image" width="713" height="275" loading="lazy">
<em>Spreadsheet Menu</em></p>
<p>Congrats! </p>
<p>Now you just need to copy and paste the script from the tutorial or <a target="_blank" href="https://github.com/nibukdk/detete_empty_rows_n_columns_in_spreadsheet">this</a> GitHub repo and you'll be able to clean your sheets instantly.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this tutorial, we created two functions: <code>deleteExternalEmptyRowsNColumns()</code> and <code>deleteInternalEmptyRowsNColumns()</code>. </p>
<p>We cleared empty rows and columns that were out of bounds and later deleted empty rows and columns that were in bounds of data. </p>
<p>Next, we created a menu that provides easy access to even non-coders to execute the functions mentioned above from the spreadsheet's tab.</p>
<p>Now, all that is left is for you to share this article. If you're also watching the video version I hope you'll subscribe to my <a target="_blank" href="https://youtube.com/@codingWithNibesh">channel</a> as well. </p>
<p>I am Nibesh Khadka, a freelancer specializing in automating Google products with Apps Script. Contact me if you need my services at me@nibeshkhadka.com.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Integrate ChatGPT with Google Sheets Using Google Apps Script ]]>
                </title>
                <description>
                    <![CDATA[ Welcome to this tutorial on how to integrate ChatGPT with Google Spreadsheets using the GPT API and Google Apps Script. We will create two custom formulas, GPT_SUMMARY and GPT_SIMPLIFY. You can use GPT_SUMMARY to summarize a large passage or text int... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/create-chat-gpt-formulas-in-google-sheets/</link>
                <guid isPermaLink="false">66ba5b03158e6c6a8cb8c7d0</guid>
                
                    <category>
                        <![CDATA[ chatgpt ]]>
                    </category>
                
                    <category>
                        <![CDATA[ google apps script ]]>
                    </category>
                
                    <category>
                        <![CDATA[ google sheets ]]>
                    </category>
                
                    <category>
                        <![CDATA[ spreadsheets ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Nibesh Khadka ]]>
                </dc:creator>
                <pubDate>Thu, 20 Jul 2023 16:06:35 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2023/07/GPT-IN-SHeets.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Welcome to this tutorial on how to integrate ChatGPT with Google Spreadsheets using the GPT API and Google Apps Script.</p>
<p>We will create two custom formulas, GPT_SUMMARY and GPT_SIMPLIFY. You can use GPT_SUMMARY to summarize a large passage or text into a few bullet points for easy reading. And you can use GPT_SIMPLIFY to simplify English into easy-to-read English.</p>
<p>We will also create menus with access to functions that perform the same tasks as formulas. We will then discuss the pros and cons of using formulas versus menus. </p>
<p>By the end of this tutorial, you will understand how to use ChatGPT in Google Sheets with Google Apps Script. You will also be able to modify the formulas and menus to meet your own needs, such as creating CVs, social media posts, or cover letters.</p>
<p>You can find the source code for this project in <a target="_blank" href="https://github.com/nibukdk/GPT_Google_Sheets_Integration">this</a> GitHub repo.</p>
<p>If you want to follow along with a video version of this article, here you go:</p>
<div class="embed-wrapper">
        <iframe width="560" height="315" src="https://www.youtube.com/embed/DlcJv97TZhE" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>
<h3 id="heading-pre-requisites">Pre-requisites</h3>
<p>This tutorial is not meant for beginners in Apps Script or JavaScript. I will not be explaining every method or classes used in the code. This is also not a tutorial on how to use and optimize ChatGPT – instead we'll focus on how to integrate GPT in Google Sheets.</p>
<h4 id="heading-who-is-this-tutorial-for">Who is this tutorial for?</h4>
<p>This tutorial is for intermediate to advanced users who have a basic understanding of Apps Script and JavaScript. If you are new to either of these, I recommend that you start with a beginner tutorial before attempting this one.</p>
<h2 id="heading-step-1-get-the-chatgpt-api-key">Step 1 – Get the ChatGPT API Key</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1689645559540/943d9a3e-d326-4cd9-ab45-0866898110d2.png" alt="Get ChatGpt API Keys" width="1747" height="992" loading="lazy">
<em>Get ChatGpt API Key</em></p>
<p>First, if you don't already have an account with OpenAI, you'll need to <a target="_blank" href="https://auth0.openai.com/u/signup/identifier?state=hKFo2SBWU2Y5U0ZjYXlDWG5LU0xhdmxhd1pCVW1wQ2ppUUp3eKFur3VuaXZlcnNhbC1sb2dpbqN0aWTZIERpalA1aER5X3hGdEl0TzlRdnlud3FJQ2NlcDduNm4zo2NpZNkgRFJpdnNubTJNdTQyVDNLT3BxZHR3QjNOWXZpSFl6d0Q">create</a> one. Once you have an account, you can create a new API key by going to the API keys section under the User tab. </p>
<p>Click the Create new secret key button and copy the key after it has been created. <em>You will not be able to see this API key again,</em> so be sure to copy it somewhere safe.</p>
<h2 id="heading-step-2-fetch-the-data-from-the-chatgpt-api-with-apps-script">Step 2 – Fetch the Data From the ChatGpt API with Apps Script</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1689558938459/8586ac7a-9b41-41ef-9dcd-c4297436912d.png" alt="Image" width="2227" height="1027" loading="lazy">
<em>Spreadsheet Sample</em></p>
<p>I have named my spreadsheet GPT_Integration with three columns: Passage, Simplified Passage, and Summarized Text.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1689559154810/de6ba3ed-a5af-4a23-ab45-e64bd39a48e6.png" alt="How to Open Apps Script Code Editor From Spreadsheet" width="668" height="482" loading="lazy"></p>
<p>Let's open the app script for this spreadsheet, rename it to GPT_integration, and also rename the existing file to utils.gs. We'll create a function called <code>fetchData</code> here.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> CHAT_GPT_API_KEY = <span class="hljs-string">"paste your API key here"</span>;
<span class="hljs-keyword">const</span> BASE_URL = <span class="hljs-string">"https://api.openai.com/v1/chat/completions"</span>;


<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">fetchData</span>(<span class="hljs-params">systemContent, userContent</span>) </span>{
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> headers = {
      <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span>,
      <span class="hljs-string">"Authorization"</span>: <span class="hljs-string">`Bearer <span class="hljs-subst">${CHAT_GPT_API_KEY}</span>`</span>
    };

    <span class="hljs-keyword">const</span> options = {
      headers,
      <span class="hljs-attr">method</span>: <span class="hljs-string">"GET"</span>,
      <span class="hljs-attr">muteHttpExceptions</span>: <span class="hljs-literal">true</span>,
      <span class="hljs-attr">payload</span>: <span class="hljs-built_in">JSON</span>.stringify({
        <span class="hljs-string">"model"</span>: <span class="hljs-string">"gpt-3.5-turbo"</span>,
        <span class="hljs-string">"messages"</span>: [{
          <span class="hljs-string">"role"</span>: <span class="hljs-string">"system"</span>,
          <span class="hljs-string">"content"</span>: systemContent,
        },
        {
          <span class="hljs-string">"role"</span>: <span class="hljs-string">"user"</span>,
          <span class="hljs-string">"content"</span>: userContent
        },
        ],
        <span class="hljs-string">"temperature"</span>: <span class="hljs-number">0.7</span>
      })
    };

    <span class="hljs-keyword">const</span> response = <span class="hljs-built_in">JSON</span>.parse(UrlFetchApp.fetch(BASE_URL, options));
    <span class="hljs-comment">//console.log(response);</span>
    <span class="hljs-comment">//console.log(response.choices[0].message.content)</span>
    <span class="hljs-keyword">return</span> response.choices[<span class="hljs-number">0</span>].message.content;
  } <span class="hljs-keyword">catch</span> (e) {
    <span class="hljs-built_in">console</span>.log(e)
    SpreadsheetApp.getActiveSpreadsheet().toast(<span class="hljs-string">"Some Error Occured Please check your formula or try again later."</span>);
    <span class="hljs-keyword">return</span> <span class="hljs-string">"Some Error Occured Please check your formula or try again later."</span>;
  }
}
</code></pre>
<p>Here are some key points to notice in the code above:</p>
<ol>
<li>Paste the API key that you created earlier inside the quotes.</li>
<li>We will be using the Chat Completions API. You can find more details about it <a target="_blank" href="https://platform.openai.com/docs/api-reference/chat/create">here</a>.</li>
<li>ChatGPT models have different roles, such as system, user, and assistant.</li>
<li>The systemContent parameter is where you provide roleplay for the GPT system. For example, you could say "You're an expert algebra teacher" or "You're an expert CV writer".</li>
<li>The userContent parameter is where you provide tasks to perform for the model. In our case, we will provide long passages from the spreadsheet to summarize and simplify.</li>
<li>We will be using the <a target="_blank" href="https://platform.openai.com/docs/models/gpt-3-5">GPT 3.5 turbo model</a>.</li>
<li>We are muting HTTPExceptions so that we can use our own error message in the catch block.</li>
<li>The error string is useful when we face errors such as <a target="_blank" href="https://platform.openai.com/docs/guides/rate-limits/what-are-the-rate-limits-for-our-api">Rate Limit Exceed</a>.</li>
</ol>
<p>We're returning the content from the response object of GPT that'll be later on handled by our formulas.</p>
<p>The response object from ChatGPT has following structure:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"id"</span>: <span class="hljs-string">"chatcmpl-123"</span>,
  <span class="hljs-attr">"object"</span>: <span class="hljs-string">"chat.completion"</span>,
  <span class="hljs-attr">"created"</span>: <span class="hljs-number">1677652288</span>,
  <span class="hljs-attr">"choices"</span>: [{
    <span class="hljs-attr">"index"</span>: <span class="hljs-number">0</span>,
    <span class="hljs-attr">"message"</span>: {
      <span class="hljs-attr">"role"</span>: <span class="hljs-string">"assistant"</span>,
      <span class="hljs-attr">"content"</span>: <span class="hljs-string">"\n\nHello there, how may I assist you today?"</span>,
    },
    <span class="hljs-attr">"finish_reason"</span>: <span class="hljs-string">"stop"</span>
  }],
  <span class="hljs-attr">"usage"</span>: {
    <span class="hljs-attr">"prompt_tokens"</span>: <span class="hljs-number">9</span>,
    <span class="hljs-attr">"completion_tokens"</span>: <span class="hljs-number">12</span>,
    <span class="hljs-attr">"total_tokens"</span>: <span class="hljs-number">21</span>
  }
}
</code></pre>
<p>Read more on how to use URLFetchApp from <a target="_blank" href="https://developers.google.com/apps-script/reference/url-fetch/url-fetch-app">here</a>.</p>
<h2 id="heading-step-3-integrate-chatgpt-as-a-sheets-formula">Step 3 – Integrate ChatGpt as a Sheets Formula</h2>
<h3 id="heading-gpt-simplify-formula">GPT SIMPLIFY Formula</h3>
<p>Again, for the custom formula, we'll create a new file named formula and then we will create a function named GPT_SIMPLIFY.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">/**
 * Simplifies the given paragraph in layman's term.
 * <span class="hljs-doctag">@param <span class="hljs-type">{String}</span> </span>input The value to simplify.
 * <span class="hljs-doctag">@return </span>Simplified Text.
 * <span class="hljs-doctag">@customfunction</span>
 */</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">GPT_SIMPLIFY</span>(<span class="hljs-params">input</span>) </span>{
  <span class="hljs-built_in">console</span>.log(input)
  <span class="hljs-keyword">const</span> systemContent = <span class="hljs-string">"Simplify the given text in layman's term. Remember reader is not an expert in english."</span>;
  <span class="hljs-keyword">return</span> <span class="hljs-built_in">Array</span>.isArray(input) ?
    input.flat().map(<span class="hljs-function"><span class="hljs-params">text</span> =&gt;</span> fetchData(systemContent, text)) :
    fetchData(systemContent, input);

}
</code></pre>
<ol>
<li>The <code>GPT_SIMPLIFY</code> formula simplifies whatever text is provided as input. The input to this function is data that is coming from the spreadsheet. When you select a range, a cell, or multiple cells, the data in the range will be automatically provided by the spreadsheet to this formula.</li>
<li>The <code>systemContent</code> is defined to be passed as the first parameter to the <code>fetchData(systemContent,userContent)</code> function. </li>
<li>We are checking if the input is an Array because the data passed to this function can either be a nested array or just a string if we select multiple cells or single cell, respectively, in the spreadsheet.</li>
</ol>
<p>You can read more on custom functions on this <a target="_blank" href="https://developers.google.com/apps-script/guides/sheets/functions">page</a>.</p>
<p>Now go ahead and apply this formula in your spreadsheet. I copied some text from a book I'm reading in the first column and applied the formula in the second column named "Simplify Passage", like this <code>=GPT_SIMPLIFY(A2)</code> for the second cell.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/07/gpt_simplify_formula.png" alt="Image" width="600" height="400" loading="lazy">
_Appilcation of GPT<em>SIMPLIFY Formula</em></p>
<p>Note: Make sure to refresh the spreadsheet before you apply the formula to sync with the latest changes in the script.</p>
<h3 id="heading-gpt-summarize">GPT SUMMARIZE</h3>
<p>To summarize the formula we'll just copy the simplify function and some other things, as you can see in the code below.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">/**
 * Summarzies the given paragraph. It provides from 3-5 bullet points
 *
 * <span class="hljs-doctag">@param <span class="hljs-type">{String}</span> </span>input The value to summarize.
 * <span class="hljs-doctag">@return </span>summarize Text.
 * <span class="hljs-doctag">@customfunction</span>
 */</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">GPT_SUMMARY</span>(<span class="hljs-params">input</span>) </span>{
  <span class="hljs-built_in">console</span>.log(input)
  <span class="hljs-keyword">const</span> systemContent = <span class="hljs-string">"Summarize the given text. Provide atleast 3 and atmost 5 bullet points."</span>;
  <span class="hljs-keyword">return</span> <span class="hljs-built_in">Array</span>.isArray(input) ?
    input.flat().map(<span class="hljs-function"><span class="hljs-params">text</span> =&gt;</span> fetchData(systemContent, text)) :
    fetchData(systemContent, input);

}
</code></pre>
<p>The main thing to notice here is the different system content.</p>
<p>Note: Since this is not a tutorial on how to use ChatGpt optimally, I provided instructions as the system content instead of role-play, and then just provided data in the user content. You can improvise this by providing roles in system content, and tasks as well as data as two different user roles in our <code>FetchData()</code> function.</p>
<h3 id="heading-gpt-rate-limit-error">GPT Rate Limit Error</h3>
<p>For free users, the rate limit to use the API is <strong>3/minute</strong>. As such, when you apply these formulas in more than three cells you'll encounter the error. Luckily the execution won't stop because we're returning an error string from fetch data which will be saved into those cells.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1689574131023/70e326ac-33bd-4d75-96db-208fc27b5859.png" alt="GPT Rate Limit Error" width="2178" height="321" loading="lazy">
<em>API rate limit error</em></p>
<h3 id="heading-auto-refresh-and-error">Auto Refresh and Error</h3>
<p>Moreover, the auto-refresh feature of the formula can force the re-application of the formula on cells that already have satisfying values whenever source cells are updated, in our case cells in column "A".</p>
<p>When we add a rate limit on top of auto-refresh it can cause a conundrum. You can technically make changes in custom functions to accommodate such circumstances but, I like to keep formulas light and efficient. So, I recommend we instead create custom menus and apply these functions manually.</p>
<h2 id="heading-step-4-integrate-gpt-chat-api-in-spreadsheet-menu-functions">Step 4 – Integrate GPT Chat API in Spreadsheet Menu Functions</h2>
<h3 id="heading-gpt-simplify-menu">GPT Simplify Menu</h3>
<p>First, let's create another file named <code>menu</code>. Then we'll create the <code>gptSimplifyMenu</code> function which will be an alternative to the GPT_SIMPLIFY formula.</p>
<pre><code class="lang-javascript">
<span class="hljs-comment">/**
 * Simplifies the given paragraph in layman's term.
 * <span class="hljs-doctag">@customfunction</span>
 */</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">gptSimplifyMenu</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">try</span> {
    <span class="hljs-comment">// get sheets and data</span>
    <span class="hljs-keyword">const</span> ss = SpreadsheetApp.getActiveSheet();
    <span class="hljs-keyword">const</span> data = ss.getDataRange().getValues();
    <span class="hljs-keyword">const</span> lastRow = data.length;
    <span class="hljs-keyword">const</span> lastCol = data[<span class="hljs-number">0</span>].length;

    <span class="hljs-comment">// define gpt's role play</span>
    <span class="hljs-keyword">const</span> systemContent = <span class="hljs-string">"Simplify the given text in layman's term. Remember reader is not an expert in english."</span>;


    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> i = <span class="hljs-number">1</span>; i &lt; data.length; i++) {
      <span class="hljs-comment">// only simplify if not already simplified or error occured previously</span>
      <span class="hljs-keyword">if</span> (data[i][<span class="hljs-number">1</span>] === <span class="hljs-string">""</span> || data[i][<span class="hljs-number">1</span>] === <span class="hljs-string">"Some Error Occured Please check your formula or try again later."</span>) {
        data[i][<span class="hljs-number">1</span>] = fetchData(systemContent, data[i][<span class="hljs-number">0</span>]);
        <span class="hljs-built_in">console</span>.log(data[i][<span class="hljs-number">1</span>]);

      }
    }

    ss.getRange(<span class="hljs-number">1</span>, <span class="hljs-number">1</span>, lastRow, lastCol).setValues(data);
  } <span class="hljs-keyword">catch</span> (e) {
    <span class="hljs-built_in">console</span>.log(e)
    SpreadsheetApp.getActiveSpreadsheet().toast(<span class="hljs-string">"Some Error Occured Please check your formula or try again later."</span>);

  }
}
</code></pre>
<p>Key points that are different to understand in this code are:</p>
<ol>
<li>We're hardcoding the data sources, as such data[i][1], which refers to the second column (that is "Simplified Passage") as shown in the spreadsheet image above. This means that if you're using some other columns to save data from ChatGPT to, then you'll have to make changes according to it.</li>
<li>We only fetch data when the target cell is empty or contains an error message. This helps to avoid unnecessary API calls.</li>
</ol>
<h3 id="heading-add-a-custom-function-as-a-spreadsheet-menu">Add a Custom Function as a Spreadsheet Menu</h3>
<p>The function is ready to be tested, but it still will not appear in the spreadsheet. To do so, we'll need to provide the following instructions.</p>
<pre><code class="lang-javascript">
<span class="hljs-comment">/**
 * Menu creates menu UI in spreadsheet.
 */</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createCustomMenu</span>(<span class="hljs-params"></span>) </span>{
   <span class="hljs-comment">// define menu ui </span>
  <span class="hljs-keyword">let</span> menu = SpreadsheetApp.getUi().createMenu(<span class="hljs-string">"GPT Functions"</span>);
   <span class="hljs-comment">// add function to the menu</span>
   menu.addItem(<span class="hljs-string">"GPT SIMPLIFY"</span>, <span class="hljs-string">"gptSimplifyMenu"</span>);
   <span class="hljs-comment">// add menu to the spreadsheet ui</span>
  menu.addToUi();
}

<span class="hljs-comment">/**
 * OnOpen trigger that creates menu
 * <span class="hljs-doctag">@param <span class="hljs-type">{Dictionary}</span> <span class="hljs-variable">e</span></span>
 */</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">onOpen</span>(<span class="hljs-params">e</span>) </span>{
  createCustomMenu();
}
</code></pre>
<p>In <code>createCustomMenu()</code>:</p>
<ol>
<li>We define menu with <a target="_blank" href="https://developers.google.com/apps-script/reference/base/ui#createmenucaption"><code>SpreadsheetApp.getUi().createMenu("GPT Functions")</code></a> as GPT Functions the title appearing in the spreadsheet tab.</li>
<li>We add a function to the menu with the <code>menu.addItem("GPT SIMPLIFY", "gptSimplifyMenu")</code>, where the first parameter is the title for display and the second is the function to call when pressed.</li>
<li>Add the menu to the UI with <code>menu.addToUi()</code>.</li>
</ol>
<p>The <a target="_blank" href="https://developers.google.com/apps-script/guides/triggers#onopene">onOpen</a> trigger runs automatically whenever the document the script is attached to reloads and as such will add a menu to the spreadsheet as shown in the image below.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1689573996102/660acf86-567e-4261-96db-2a8ed1c2182c.png" alt="GPT Function Menu" width="1009" height="236" loading="lazy"></p>
<p>Go ahead and try the formula – it'll only be applied if the cell is either empty or prefilled with an error message.</p>
<h3 id="heading-gpt-summarize-menu">GPT Summarize Menu</h3>
<p>We'll make some minor changes after copying the simplify function as shown below:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">/**
 * Summarzies the given paragraph. It provides from 3-5 bullet points
 * <span class="hljs-doctag">@customfunction</span>
 */</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">gptSummaryMenu</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">try</span> {
    <span class="hljs-comment">// get sheets and data</span>
    <span class="hljs-keyword">const</span> ss = SpreadsheetApp.getActiveSheet();
    <span class="hljs-keyword">const</span> data = ss.getDataRange().getValues();
    <span class="hljs-keyword">const</span> lastRow = data.length;
    <span class="hljs-keyword">const</span> lastCol = data[<span class="hljs-number">0</span>].length;

    <span class="hljs-comment">// define gpt's role play</span>
    <span class="hljs-keyword">const</span> systemContent = <span class="hljs-string">"Summarize the given text. Provide atleast 3 and atmost 5 bullet points."</span>;


    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> i = <span class="hljs-number">0</span>; i &lt; data.length; i++) {
      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Inside gptSummaryMenu() for loop`</span>)

      <span class="hljs-keyword">if</span> (i == <span class="hljs-number">0</span>) <span class="hljs-keyword">continue</span>;
      <span class="hljs-comment">// only summarize if not already summarized or error occured previously</span>
      <span class="hljs-keyword">if</span> (data[i][<span class="hljs-number">2</span>] === <span class="hljs-string">""</span> || data[i][<span class="hljs-number">2</span>] === <span class="hljs-string">"Some Error Occured Please check your formula or try again later."</span>) {
        data[i][<span class="hljs-number">2</span>] = fetchData(systemContent, data[i][<span class="hljs-number">0</span>]);
        <span class="hljs-built_in">console</span>.log(data[i][<span class="hljs-number">2</span>]);
      }
    }

    ss.getRange(<span class="hljs-number">1</span>, <span class="hljs-number">1</span>, lastRow, lastCol).setValues(data);
  } <span class="hljs-keyword">catch</span> (e) {
    <span class="hljs-built_in">console</span>.log(e)
    SpreadsheetApp.getActiveSpreadsheet().toast(<span class="hljs-string">"Some Error Occured Please check your formula or try again later."</span>);
  }
}
</code></pre>
<ol>
<li>The system role has been changed to address summary instruction.</li>
<li>The target column to save data is now the third column.</li>
<li>The doc string has been adjusted as well.</li>
</ol>
<p>As for adding this function to the menu I'll leave it to you.</p>
<h2 id="heading-tips-to-modify-the-code">Tips to Modify the Code</h2>
<p>All you need to make your own formula like =GPT_COVER_LETTER_CREATOR() are the following modifications:</p>
<h3 id="heading-to-fetchdata">To FetchData</h3>
<p>You can change System Content Description should to address your needs, like "You write an expert cover letter for software developers".</p>
<p>Add one more instruction in the messages array:</p>
<pre><code class="lang-json"><span class="hljs-comment">// from this </span>
[{
          <span class="hljs-attr">"role"</span>: <span class="hljs-string">"system"</span>,
          <span class="hljs-attr">"content"</span>: systemContent,
        },
        {
          <span class="hljs-attr">"role"</span>: <span class="hljs-string">"user"</span>,
          <span class="hljs-attr">"content"</span>: userContent
        },
        ], 

<span class="hljs-comment">// to </span>
[{
          <span class="hljs-attr">"role"</span>: <span class="hljs-string">"system"</span>,
          <span class="hljs-attr">"content"</span>: <span class="hljs-string">"You write an expert cover letter for software developers"</span>,
        },
        {
          <span class="hljs-attr">"role"</span>: <span class="hljs-string">"user"</span>,
          <span class="hljs-attr">"content"</span>: <span class="hljs-string">"Write me a cover letter for this given job advertisement"</span>
        },
        {
          <span class="hljs-attr">"role"</span>: <span class="hljs-string">"user"</span>,
          <span class="hljs-attr">"content"</span>: userContent <span class="hljs-comment">// this is job ad from spreadsheet</span>
        },
        ],
</code></pre>
<p>You can also add another list item to include your skills and experiences.</p>
<h3 id="heading-other-functions">Other Functions</h3>
<p>Just make sure that your source cell/columns and target cell/columns are accurately indexed (for instance, if you're not using the first column as the source cell and the second to save the data).</p>
<h2 id="heading-summary">Summary</h2>
<p>In this tutorial, you learned how to use Google Apps Script to fetch ChatGPT responses from the API and save them into spreadsheets using custom formulas and custom menus.</p>
<p>We started by creating a new Google Apps Script project and adding the ChatGPT API. Then, we wrote a script that would fetch a ChatGPT response for a given prompt. We saved the response using a custom formula into a spreadsheet cell.</p>
<p>We also created a custom menu item that would allow us to fetch a ChatGPT response from any cell in the spreadsheet. This menu item would open a button to fetch the response.</p>
<p>The final step was to share the spreadsheet with others so that they could use the custom formulas and menus to fetch ChatGPT responses.</p>
<p>I hope you enjoyed this article and found it helpful. If you have any questions, just let me know.</p>
<p>I am <strong>Nibesh Khadka,</strong> Freelancer specializing in automating Google products with Apps Script. Contact me if you need my services at me@nibeshkhadka.com.</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
