2023/11/16

Gg / 透過Google Apps Script隨機分配Google Forms問卷

這個是 Gg / 如何在Google Forms 隨機抽取題目(選擇題) 的再折衷方式 
原因除了隨機產生的問卷時間可能會很久 
表單資料的工作表會很長一大串,事後整理也不方便 
而且有沒有必要這樣隨機組合問題,也是一個需要考量的重點 
所以這次採取再折衷的方法 
這個方式是借鏡別人的模式 我不確定實際流程是否一樣 
但是概念跟目的應該是相似的 
也就是透過網頁點取"取得問卷連結"來隨機獲得事先建立的問卷

   

整體架構分為2個部分:Google Apps Scritp-程式部分、Google spreadsheets-題庫部分

A. Google Apps Scritp-程式部分

程式碼.gs

因為是部署成網路應用程式,所以必須使用 doGet()取得form.html的內容,這樣執行程式時才會有網頁內容 
include() 是為了讓 form.html可以讀取在後端的style.css.html 
getForm()配合form.html的按鈕的觸發事件執行 google.script.run.withSuccessHandler(showForm).getForm(),將資料回傳到form.html的 showForm()
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
function doGet(e) {
  var output = HtmlService.createTemplateFromFile("form").evaluate().setTitle("問卷系統");
  output.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);//可以將發布成html介面的程式  崁入其他網站
  return output;
}

function include(filename) {
  return HtmlService.createHtmlOutputFromFile(filename).getContent();
}

function getForm() {
  // https://docs.google.com/spreadsheets/d/********************************************/
  // sheet name 問卷表單
  var ss = SpreadsheetApp.openById("********************************************").getSheetByName("問卷表單"); 

  var Row = ss.getLastRow();
  //Logger.log('Row- '+Row);

  var g = ss.getRange("D2").getValue()+1;     //取得在D2儲存上一次的序號  +1=列號
  Logger.log("g-"+g);
  
  if(g !==""){
    //Math.floor(Math.random()*(max-min+1))+min
      var i = Math.floor(Math.random()*(Row-1+1))+1; // 本次序號
      Logger.log("本次序號 i-"+i);
      
      //如果跟D2一樣 則重新取得i
      while( g == i || i==1){
        i = Math.floor(Math.random()*(Row-1+1))+1;
        Logger.log("重新取號 i-"+i);
      }
      ss.getRange("D2").setValue(i-1);
  }else{
    //如果 D2是空值 則直接取值
    var i = Math.floor(Math.random()*(Row-1+1))+1; // 本次序號
    Logger.log("初始序號-"+i);
    ss.getRange("D2").setValue(i-1);
  }
  //回傳網址    
  var msg = '<h3><a href="' +ss.getRange("C"+i).getValue() +'" target="_blank">問卷連結</a></h3>'
  Logger.log(msg);

  return msg;

}
 

form.html

透過btnl綁定按鈕send的觸發事件來執行google.script.run.withSuccessHandler(showForm).getForm(),執行後端程式 getForm() 當 getForm()回傳資料時,由showForm()取得後端回傳資料,寫入div output之中
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>檔案上傳系統</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
    <base target="_top">
    <?!= include('style.css'); ?>
  </head>
  <body>
    <div class="container">
        <div class="row">
            <div class="col-8 mx-auto box">
                <h1 class="mb-5 text-center"><strong>取得隨機問卷</strong></h1>
                    <input type="submit" id="send" class="btn btn-primary btn-lg btn-block pt-2 pb-2" value="取得問卷連結">
                <div id="output"></div>
            </div>
        </div>
    </div>
  </body>
  <script>
  var btn1= document.getElementById("send");
  btn1.addEventListener("click", function(e){
      console.log("btn1");
      btn1.disabled = true;
      document.getElementById("output").innerHTML = "";
      document.getElementById("output").style.display='none';
      google.script.run.withSuccessHandler(showForm).getForm();
    }
  )
  //寫入網頁
  function showForm(form){    
    console.log(form);
    btn1.disabled = false;
    document.getElementById("output").innerHTML = "";
    document.getElementById("output").innerHTML = form;
    document.getElementById("output").style.display='block';
  }  
  </script>
</html>
 

style.css.html

設定整體的字形,以及div output的樣式 因為已經在form.html載入bootstrap來設定 所以也就沒有再特別設定什麼
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<style>
    body{
        font-family: "Microsoft JhengHei";
    }
  #output{
    display:  none;
    text-align: center;
    padding:20px;
    border: 2px solid #FF0000;
    //width: 520px;
    height:100px;
  }

  .box{
    padding: 50px;
    margin-top: 50px;
    border: 1px solid #fff;
    border-radius: 30px;
    box-shadow: 0px 20px 50px 0px rgba(0,0,0,0.2);
  }
</style>
 

B. Google spreadsheets-題庫部分

基本結構與命名,必須跟程式碼.gs相互配合 D欄是為了記錄前一次的編號,配合程式來避免新取得的編號是重複的

後記

無論是作為問卷或者試卷,這樣的方式可能會比較理想 相對可以確保各表單內容的品質 事後整理也會比較方便

2023/11/02

Excel / VLOOKUP / LOOKUP函數 (2024/2/26 修改)

Excel 多個條件,透過VLOOKUP / LOOKUP函數尋找一個值

其實就是把多個條件合併成一個條件

例如:

條件1+條件2→社區+戶型,回傳符合條件的價格

使用VLOOKUP 函數

H2=VLOOKUP(F2&G2,CHOOSE({1,2},$A$2:$A$10&$B$2:$B$10,$C$2:$C$10),2,0)

先利用CHOOSE函數-CHOOSE(index_num, value1, [value2], …)

如果 index_num 是個陣列,評估 CHOOSE 時會評估陣列中的每個數值

白話來說就是會得到所有評估值的陣列結果,並且會依據 index_num來組合陣列

所以這個例子會得到條件1( 社區)、條件二(戶型),與對應結果(價格)形成的一個參照範圍{條件1&條件2 , 對應結果}

這樣就可以在VLOOKUP作為查閱值的參照範圍,取得傳回值

使用LOOKUP 函數

I2=LOOKUP(1,0/(A2&B2 = F2 &G2),C2)

J3=LOOKUP(1,0/($A$2:$A$10&$B$2:$B$10=F2&G2),$C$2:$C$10)

兩個公式的結構其實是一樣的,J3是陣列公式,所以除了比對值之外,都是範圍

這邊利用 LOOKUP如果找不到相同的目標值時,會返回小於目標值的最大值(不適用於 0/條件,而是區間查找,可以參考Lookup函數最經典的14種用法)

在第二個參數 0/條件

如果 條件 有符合值,會得到True,0/True=0

沒有符合值,會得到False,0/False會得到#N/A 的錯誤值

如果是陣列公式,就會得到所有的返回值,會是由0或 #N/A組合的陣列

後記

其實如果使用VBA,以程式方式來處理

可能會更直覺、直觀

2023/11/01

Excel / INDEX-SMALL-IF-ROW 萬金油公式

最近在臉書跟Line上加入了幾個excel / excel VBA的社群

除了有很多喜好伸手牌的人會問問題之外

也有很多厲害的人會嘗試去解題

有時候看到所用的 excel函數結構非常複雜,真心覺得厲害

而且看到有巧思的運用函數公式,更會讓我想進一步了解公式是如何組合的

我有空的時候也會嘗試解決一些我看得懂的問題

(必須說,有部分的人連問題都說不清楚。也或者是我程度不夠,無法理解)

發現大多數的問題都是在既有資料中,尋找符合條件的目標

因此,多數的問題可以利用 INDEX-SMALL-IF-ROW 所謂的萬金油公式(用途很多)來處理

而在excel 2019之後的版本,可以用 Filter函數+Match函數來處理,並且自動溢位(也就是不用再按Ctrl+Shift+Enter,設定為陣列公式)

例如:以下資料

條件值:以 牌照號碼 為條件,取得符合條件的其他欄位資料,而這一個條件,會有多筆資料

利用 萬金油公式

E2=INDEX(工作表!A:A,SMALL(IF(工作表!$A$2:$A$435=$C$1,ROW($2:$435),“”),ROW(A1)))

F2=INDEX(工作表!D:D,SMALL(IF(工作表!$A$2:$A$435=$C$1,ROW($2:$435),4^10),ROW(A1)))

G2~I2也都是一樣的公式結構

公式說明:由內而外,由右至左來理解

IF函數 

透過條件 工作表!$A$2:$A$435=$C$1

為True時,取得在ROW($2:$435)範圍內相對位置的值(ROW())

為False時,則為空值(“”跟4^10的結果都是一樣)

最後會得到一個{True/False}的結果陣列

SMALL函數

要找出符合資料在IF陣列結果的位置,以這個資料表來說就會是列數,並配合 ROW(A1)下向拉動,會產生1、2、3….,可以得到第1個符合的值、 第2個符合的值

INDEX函數

利用SMALL函數得到的列數值,回傳對應的欄位值,例如: 某列的在工作表!A欄的資料


利用 Filter函數

E2=FILTER(INDEX(工作表!A2:O435,,MATCH(E$1,工作表!$A$1:$O$1,0)),工作表!$A$2:$A$435=$C$1,””)

F2=FILTER(INDEX(工作表!A2:O435,,MATCH(F$1,工作表!$A$1:$O$1,0)),工作表!$A$2:$A$435=$C$1,””)

G2~I2也都是一樣的公式結構

公式說明:由內而外,由右至左來理解

MATCH函數

取得符合條件(例如: F$1)在目標資料的相對位置,也就是欄位值

INDEX函數

這時候反而是透過MATCH值,取得資料範圍內的符合欄位的所有值

Filter函數

在INDEX函數取得的資料內,透過條件 工作表!$A$2:$A$435=$C$1,篩選出符合條件的值

後記

之後可以再透過一些排除錯誤值的公式,讓結果更乾淨

如果是多個條件,尋找一個值,可以用 VLOOKUP / LOOKUP,這也有很多應用的技巧