├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── cell.go ├── cell_data.go ├── client_secret.json.enc ├── dimension_properties.go ├── error_value.go ├── extended_value.go ├── grid_data.go ├── grid_properties.go ├── properties.go ├── row_data.go ├── service.go ├── service_test.go ├── sheet.go ├── sheet_data.go ├── sheet_properties.go ├── sheet_test.go ├── spreadsheet.go ├── tab_color.go ├── update_request.go ├── utils.go └── utils_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | client_secret.json -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - "1.9" 4 | - "1.10" 5 | - "1.11" 6 | - "master" 7 | matrix: 8 | allow_failures: 9 | - go: "master" 10 | before_install: 11 | - go get github.com/mattn/goveralls 12 | - go get golang.org/x/tools/cmd/cover 13 | - openssl aes-256-cbc -K $encrypted_ecf501240fa0_key -iv $encrypted_ecf501240fa0_iv 14 | -in client_secret.json.enc -out client_secret.json -d 15 | script: 16 | - $HOME/gopath/bin/goveralls -service=travis-ci 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright © 2016 Iwark 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | spreadsheet 2 | === 3 | [![Build Status](https://travis-ci.org/Iwark/spreadsheet.svg?branch=v2)](https://travis-ci.org/Iwark/spreadsheet) 4 | [![Coverage Status](https://coveralls.io/repos/github/Iwark/spreadsheet/badge.svg?branch=v2)](https://coveralls.io/github/Iwark/spreadsheet?branch=v2) 5 | [![GoReport](https://goreportcard.com/badge/Iwark/spreadsheet)](http://goreportcard.com/report/Iwark/spreadsheet) 6 | [![GoDoc](https://godoc.org/gopkg.in/Iwark/spreadsheet.v2?status.svg)](https://godoc.org/gopkg.in/Iwark/spreadsheet.v2) 7 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) 8 | 9 | Package `spreadsheet` provides fast and easy-to-use access to the Google Sheets API for reading and updating spreadsheets. 10 | 11 | Any pull-request is welcome. 12 | 13 | ## Installation 14 | 15 | ``` 16 | go get gopkg.in/Iwark/spreadsheet.v2 17 | ``` 18 | 19 | ## Preparation 20 | 21 | This package uses oauth2 client for authentication. You need to get service account key from [Google Developer Console](https://console.developers.google.com/project). Place the ``client_secret.json`` to the root of your project. 22 | 23 | ## Usage 24 | 25 | First you need **service** to start using this package. 26 | 27 | ```go 28 | data, err := ioutil.ReadFile("client_secret.json") 29 | checkError(err) 30 | 31 | conf, err := google.JWTConfigFromJSON(data, spreadsheet.Scope) 32 | checkError(err) 33 | 34 | client := conf.Client(context.TODO()) 35 | service := spreadsheet.NewServiceWithClient(client) 36 | ``` 37 | 38 | Or there is a shortcut which does the same things: 39 | 40 | ```go 41 | service, err := spreadsheet.NewService() 42 | ``` 43 | 44 | ### Fetching a spreadsheet 45 | 46 | ```go 47 | spreadsheetID := "1mYiA2T4_QTFUkAXk0BE3u7snN2o5FgSRqxmRrn_Dzh4" 48 | spreadsheet, err := service.FetchSpreadsheet(spreadsheetID) 49 | ``` 50 | 51 | ### Create a spreadsheet 52 | 53 | ```go 54 | ss, err := service.CreateSpreadsheet(spreadsheet.Spreadsheet{ 55 | Properties: spreadsheet.Properties{ 56 | Title: "spreadsheet title", 57 | }, 58 | }) 59 | ``` 60 | 61 | ### Find a sheet 62 | 63 | ```go 64 | // get a sheet by the index. 65 | sheet, err := spreadsheet.SheetByIndex(0) 66 | 67 | // get a sheet by the ID. 68 | sheet, err := spreadsheet.SheetByID(0) 69 | 70 | // get a sheet by the title. 71 | sheet, err := spreadsheet.SheetByTitle("SheetTitle") 72 | ``` 73 | 74 | ### Get cells 75 | 76 | ```go 77 | // get the B1 cell content 78 | sheet.Rows[0][1].Value 79 | 80 | // get the A2 cell content 81 | sheet.Columns[0][1].Value 82 | ``` 83 | 84 | ### Update cell content 85 | 86 | ```go 87 | row := 1 88 | column := 2 89 | sheet.Update(row, column, "hogehoge") 90 | sheet.Update(3, 2, "fugafuga") 91 | 92 | // Make sure call Synchronize to reflect the changes. 93 | err := sheet.Synchronize() 94 | ``` 95 | 96 | ### Expand a sheet 97 | 98 | ```go 99 | err := service.ExpandSheet(sheet, 20, 10) // Expand the sheet to 20 rows and 10 columns 100 | ``` 101 | 102 | ### Delete Rows / Columns 103 | 104 | ```go 105 | err := sheet.DeleteRows(0, 3) // Delete first three rows in the sheet 106 | 107 | err := sheet.DeleteColumns(1, 4) // Delete columns B:D 108 | ``` 109 | 110 | More usage can be found at the [godoc](https://godoc.org/gopkg.in/Iwark/spreadsheet.v2). 111 | 112 | ## Example 113 | 114 | ```go 115 | package main 116 | 117 | import ( 118 | "fmt" 119 | "io/ioutil" 120 | 121 | "gopkg.in/Iwark/spreadsheet.v2" 122 | "golang.org/x/net/context" 123 | "golang.org/x/oauth2/google" 124 | ) 125 | 126 | func main() { 127 | data, err := ioutil.ReadFile("client_secret.json") 128 | checkError(err) 129 | conf, err := google.JWTConfigFromJSON(data, spreadsheet.Scope) 130 | checkError(err) 131 | client := conf.Client(context.TODO()) 132 | 133 | service := spreadsheet.NewServiceWithClient(client) 134 | spreadsheet, err := service.FetchSpreadsheet("1mYiA2T4_QTFUkAXk0BE3u7snN2o5FgSRqxmRrn_Dzh4") 135 | checkError(err) 136 | sheet, err := spreadsheet.SheetByIndex(0) 137 | checkError(err) 138 | for _, row := range sheet.Rows { 139 | for _, cell := range row { 140 | fmt.Println(cell.Value) 141 | } 142 | } 143 | 144 | // Update cell content 145 | sheet.Update(0, 0, "hogehoge") 146 | 147 | // Make sure call Synchronize to reflect the changes 148 | err = sheet.Synchronize() 149 | checkError(err) 150 | } 151 | 152 | func checkError(err error) { 153 | if err != nil { 154 | panic(err.Error()) 155 | } 156 | } 157 | ``` 158 | 159 | ## License 160 | 161 | Spreadsheet is released under the MIT License. 162 | -------------------------------------------------------------------------------- /cell.go: -------------------------------------------------------------------------------- 1 | package spreadsheet 2 | 3 | import "fmt" 4 | 5 | // Cell describes a cell data 6 | type Cell struct { 7 | Row uint 8 | Column uint 9 | Value string 10 | Note string 11 | rawValue ExtendedValue 12 | effectiveValue ExtendedValue 13 | 14 | modifiedFields string 15 | } 16 | 17 | // Pos returns the cell's position like "A1" 18 | func (cell *Cell) Pos() string { 19 | return numberToLetter(int(cell.Column)+1) + fmt.Sprintf("%d", cell.Row+1) 20 | } 21 | 22 | // RawValue returns the raw value of a cell as entered by a user. 23 | // Cells with formulas, for example, return the formula rather than the value of that formula. 24 | func (cell *Cell) RawValue() ExtendedValue { 25 | return cell.rawValue 26 | } 27 | 28 | // EffectiveValue is the effective value of a cell. 29 | // Cells with formulas will return the value of that formula. 30 | func (cell *Cell) EffectiveValue() ExtendedValue { 31 | return cell.effectiveValue 32 | } 33 | -------------------------------------------------------------------------------- /cell_data.go: -------------------------------------------------------------------------------- 1 | package spreadsheet 2 | 3 | // CellData is data about a specific cell. 4 | type CellData struct { 5 | UserEnteredValue ExtendedValue `json:"userEnteredValue"` 6 | EffectiveValue ExtendedValue `json:"effectiveValue"` 7 | FormattedValue string `json:"formattedValue"` 8 | // UserEnteredFormat *CellFormat `json:"userEnteredFormat"` 9 | // EffectiveFormat *CellFormat `json:"effectiveFormat"` 10 | Hyperlink string `json:"hyperlink"` 11 | Note string `json:"note"` 12 | // TextFormatRuns []*TextFormatRun `json:"textFormatRuns"` 13 | // DataValidation *DataValidationRule `json:"dataValidation"` 14 | // PivotTable *PivotTable `json:"pivotTable"` 15 | } 16 | -------------------------------------------------------------------------------- /client_secret.json.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Iwark/spreadsheet/7677e8164883b5ffe769ef7c7cb26c172020e063/client_secret.json.enc -------------------------------------------------------------------------------- /dimension_properties.go: -------------------------------------------------------------------------------- 1 | package spreadsheet 2 | 3 | // DimensionProperties is properties about a dimension. 4 | type DimensionProperties struct { 5 | HiddenByFilter bool `json:"hiddenByFilter"` 6 | HiddenByUser bool `json:"hiddenByUser"` 7 | PixelSize uint `json:"pixelSize"` 8 | // DeveloperMetadata []*DeveloperMetadata `json:"developerMetadata"` 9 | } 10 | -------------------------------------------------------------------------------- /error_value.go: -------------------------------------------------------------------------------- 1 | package spreadsheet 2 | 3 | // ErrorValue is an error in a cell. 4 | type ErrorValue struct { 5 | Type string `json:"type"` 6 | Message string `json:"message"` 7 | } 8 | -------------------------------------------------------------------------------- /extended_value.go: -------------------------------------------------------------------------------- 1 | package spreadsheet 2 | 3 | // ExtendedValue is the kinds of value that a cell in a spreadsheet can have. 4 | type ExtendedValue struct { 5 | NumberValue float64 `json:"numberValue"` 6 | StringValue string `json:"stringValue"` 7 | BoolValue bool `json:"boolValue"` 8 | FormulaValue string `json:"formulaValue"` 9 | ErrorValue ErrorValue `json:"errorValue"` 10 | } 11 | -------------------------------------------------------------------------------- /grid_data.go: -------------------------------------------------------------------------------- 1 | package spreadsheet 2 | 3 | // GridData is data in the grid, as well as metadata about the dimensions. 4 | type GridData struct { 5 | StartRow uint `json:"startRow"` 6 | StartColumn uint `json:"startColumn"` 7 | RowData []RowData `json:"rowData"` 8 | RowMetadata []*DimensionProperties `json:"rowMetadata"` 9 | ColumnMetadata []*DimensionProperties `json:"columnMetadata"` 10 | } 11 | -------------------------------------------------------------------------------- /grid_properties.go: -------------------------------------------------------------------------------- 1 | package spreadsheet 2 | 3 | // GridProperties is properties of a grid. 4 | type GridProperties struct { 5 | RowCount uint `json:"rowCount"` 6 | ColumnCount uint `json:"columnCount"` 7 | FrozenRowCount uint `json:"frozenRowCount"` 8 | FrozenColumnCount uint `json:"frozenColumnCount"` 9 | HideGridlines bool `json:"hideGridlines"` 10 | } 11 | -------------------------------------------------------------------------------- /properties.go: -------------------------------------------------------------------------------- 1 | package spreadsheet 2 | 3 | // Properties is properties of a spreadsheet. 4 | type Properties struct { 5 | Title string `json:"title"` 6 | Locale string `json:"locale"` 7 | AutoRecalc string `json:"autoRecalc"` 8 | TimeZone string `json:"timezone"` 9 | // DefaultFormat *CellFormat `defaultFormat` 10 | } 11 | -------------------------------------------------------------------------------- /row_data.go: -------------------------------------------------------------------------------- 1 | package spreadsheet 2 | 3 | // RowData is data about each cell in a row. 4 | type RowData struct { 5 | Values []CellData `json:"values"` 6 | } 7 | -------------------------------------------------------------------------------- /service.go: -------------------------------------------------------------------------------- 1 | package spreadsheet 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "net/url" 10 | "sync" 11 | "time" 12 | 13 | "golang.org/x/oauth2" 14 | "golang.org/x/oauth2/google" 15 | ) 16 | 17 | const ( 18 | baseURL = "https://sheets.googleapis.com/v4" 19 | 20 | // Scope is the API scope for viewing and managing your Google Spreadsheet data. 21 | // Useful for generating JWT values. 22 | Scope = "https://spreadsheets.google.com/feeds" 23 | 24 | // SecretFileName is used to get client. 25 | SecretFileName = "client_secret.json" 26 | ) 27 | 28 | // NewService makes a new service with the secret file. 29 | func NewService() (s *Service, err error) { 30 | data, err := ioutil.ReadFile(SecretFileName) 31 | if err != nil { 32 | return 33 | } 34 | 35 | conf, err := google.JWTConfigFromJSON(data, Scope) 36 | if err != nil { 37 | return 38 | } 39 | 40 | s = NewServiceWithClient(conf.Client(oauth2.NoContext)) 41 | return 42 | } 43 | 44 | // NewServiceWithClient makes a new service by the client. 45 | func NewServiceWithClient(client *http.Client) *Service { 46 | return &Service{ 47 | baseURL: baseURL, 48 | client: client, 49 | m: new(sync.RWMutex), 50 | configForSpreadsheetByID: make(map[string]spreadsheetConfig, 0), 51 | } 52 | } 53 | 54 | // Service represents a Sheets API service instance. 55 | // Service is the main entry point into using this package. 56 | type Service struct { 57 | baseURL string 58 | client *http.Client 59 | m *sync.RWMutex 60 | configForSpreadsheetByID map[string]spreadsheetConfig 61 | } 62 | 63 | // CreateSpreadsheet creates a spreadsheet with the given title 64 | func (s *Service) CreateSpreadsheet(spreadsheet Spreadsheet) (resp Spreadsheet, err error) { 65 | sheets := make([]map[string]interface{}, 1) 66 | for s := range spreadsheet.Sheets { 67 | sheet := spreadsheet.Sheets[s] 68 | sheets = append(sheets, map[string]interface{}{"properties": map[string]interface{}{"title": sheet.Properties.Title}}) 69 | } 70 | body, err := s.post("/spreadsheets", map[string]interface{}{ 71 | "properties": map[string]interface{}{ 72 | "title": spreadsheet.Properties.Title, 73 | }, 74 | "sheets": sheets, 75 | }) 76 | if err != nil { 77 | return 78 | } 79 | err = json.Unmarshal([]byte(body), &resp) 80 | if err != nil { 81 | return 82 | } 83 | return s.FetchSpreadsheet(resp.ID) 84 | } 85 | 86 | type spreadsheetConfig struct { 87 | cacheInterval time.Duration 88 | lastCachedAt time.Time 89 | cachedSpreadsheet Spreadsheet 90 | } 91 | 92 | // FetchSpreadsheetOption is the option for FetchSpreadsheet function 93 | type FetchSpreadsheetOption func(*spreadsheetConfig) 94 | 95 | // WithCache gives a cacheInterval option for FetchSpreadsheet function 96 | func WithCache(interval time.Duration) FetchSpreadsheetOption { 97 | return func(config *spreadsheetConfig) { 98 | config.cacheInterval = interval 99 | } 100 | } 101 | 102 | // FetchSpreadsheet fetches the spreadsheet by the id. 103 | func (s *Service) FetchSpreadsheet(id string, options ...FetchSpreadsheetOption) (spreadsheet Spreadsheet, err error) { 104 | s.m.RLock() 105 | config := s.configForSpreadsheetByID[id] 106 | s.m.RUnlock() 107 | for _, o := range options { 108 | o(&config) 109 | } 110 | 111 | if config.cacheInterval > 0 && time.Now().Sub(config.lastCachedAt.Add(config.cacheInterval)) <= 0 { 112 | // use cache 113 | return config.cachedSpreadsheet, nil 114 | } 115 | 116 | fields := "spreadsheetId,properties.title,sheets(properties,data.rowData.values(userEnteredValue,effectiveValue,formattedValue,note))" 117 | fields = url.QueryEscape(fields) 118 | path := fmt.Sprintf("/spreadsheets/%s?fields=%s", id, fields) 119 | body, err := s.get(path) 120 | if err != nil { 121 | return 122 | } 123 | err = json.Unmarshal(body, &spreadsheet) 124 | if err != nil { 125 | return 126 | } 127 | spreadsheet.service = s 128 | 129 | if config.cacheInterval > 0 { 130 | config.cachedSpreadsheet = spreadsheet 131 | config.cachedSpreadsheet.cached = true 132 | config.lastCachedAt = time.Now() 133 | s.m.Lock() 134 | s.configForSpreadsheetByID[id] = config 135 | s.m.Unlock() 136 | } 137 | return 138 | } 139 | 140 | // ReloadSpreadsheet reloads the spreadsheet 141 | func (s *Service) ReloadSpreadsheet(spreadsheet *Spreadsheet) (err error) { 142 | newSpreadsheet, err := s.FetchSpreadsheet(spreadsheet.ID) 143 | if err != nil { 144 | return 145 | } 146 | spreadsheet.Properties = newSpreadsheet.Properties 147 | spreadsheet.Sheets = newSpreadsheet.Sheets 148 | return 149 | } 150 | 151 | // AddSheet adds a sheet 152 | func (s *Service) AddSheet(spreadsheet *Spreadsheet, sheetProperties SheetProperties) (err error) { 153 | r, err := newUpdateRequest(spreadsheet) 154 | if err != nil { 155 | return 156 | } 157 | err = r.AddSheet(sheetProperties).Do() 158 | if err != nil { 159 | return 160 | } 161 | err = s.ReloadSpreadsheet(spreadsheet) 162 | return 163 | } 164 | 165 | // DuplicateSheet duplicates the contents of a sheet 166 | func (s *Service) DuplicateSheet(spreadsheet *Spreadsheet, sheet *Sheet, index int, title string) (err error) { 167 | r, err := newUpdateRequest(spreadsheet) 168 | if err != nil { 169 | return 170 | } 171 | err = r.DuplicateSheet(sheet, index, title).Do() 172 | if err != nil { 173 | return 174 | } 175 | err = s.ReloadSpreadsheet(spreadsheet) 176 | return 177 | } 178 | 179 | // DeleteSheet deletes the sheet 180 | func (s *Service) DeleteSheet(spreadsheet *Spreadsheet, sheetID uint) (err error) { 181 | r, err := newUpdateRequest(spreadsheet) 182 | if err != nil { 183 | return 184 | } 185 | err = r.DeleteSheet(sheetID).Do() 186 | if err != nil { 187 | return 188 | } 189 | err = s.ReloadSpreadsheet(spreadsheet) 190 | return 191 | } 192 | 193 | // SyncSheet updates sheet 194 | func (s *Service) SyncSheet(sheet *Sheet) (err error) { 195 | if sheet.newMaxRow > sheet.Properties.GridProperties.RowCount || 196 | sheet.newMaxColumn > sheet.Properties.GridProperties.ColumnCount { 197 | err = s.ExpandSheet(sheet, sheet.newMaxRow, sheet.newMaxColumn) 198 | if err != nil { 199 | return 200 | } 201 | } 202 | r, err := newUpdateRequest(sheet.Spreadsheet) 203 | if err != nil { 204 | return 205 | } 206 | err = r.UpdateCells(sheet).Do() 207 | if err != nil { 208 | return 209 | } 210 | sheet.modifiedCells = []*Cell{} 211 | sheet.Properties.GridProperties.RowCount = sheet.newMaxRow 212 | sheet.Properties.GridProperties.ColumnCount = sheet.newMaxColumn 213 | return 214 | } 215 | 216 | // ExpandSheet expands the range of the sheet 217 | func (s *Service) ExpandSheet(sheet *Sheet, row, column uint) (err error) { 218 | props := sheet.Properties 219 | props.GridProperties.RowCount = row 220 | props.GridProperties.ColumnCount = column 221 | 222 | r, err := newUpdateRequest(sheet.Spreadsheet) 223 | if err != nil { 224 | return 225 | } 226 | err = r.UpdateSheetProperties(sheet, &props).Do() 227 | if err != nil { 228 | return 229 | } 230 | sheet.newMaxRow = row 231 | sheet.newMaxColumn = column 232 | return 233 | } 234 | 235 | // DeleteRows deletes rows from the sheet 236 | func (s *Service) DeleteRows(sheet *Sheet, start, end int) (err error) { 237 | sheet.Properties.GridProperties.RowCount -= uint(end - start) 238 | sheet.newMaxRow -= uint(end - start) 239 | r, err := newUpdateRequest(sheet.Spreadsheet) 240 | if err != nil { 241 | return 242 | } 243 | err = r.DeleteDimension(sheet, "ROWS", start, end).Do() 244 | return 245 | } 246 | 247 | // DeleteColumns deletes columns from the sheet 248 | func (s *Service) DeleteColumns(sheet *Sheet, start, end int) (err error) { 249 | sheet.Properties.GridProperties.ColumnCount -= uint(end - start) 250 | sheet.newMaxRow -= uint(end - start) 251 | r, err := newUpdateRequest(sheet.Spreadsheet) 252 | if err != nil { 253 | return 254 | } 255 | err = r.DeleteDimension(sheet, "COLUMNS", start, end).Do() 256 | return 257 | } 258 | 259 | func (s *Service) get(path string) (body []byte, err error) { 260 | resp, err := s.client.Get(baseURL + path) 261 | if err != nil { 262 | return 263 | } 264 | body, err = ioutil.ReadAll(resp.Body) 265 | resp.Body.Close() 266 | if err != nil { 267 | return 268 | } 269 | err = s.checkError(body) 270 | return 271 | } 272 | 273 | func (s *Service) post(path string, params map[string]interface{}) (body string, err error) { 274 | reqBody, err := json.Marshal(params) 275 | if err != nil { 276 | return 277 | } 278 | resp, err := s.client.Post(baseURL+path, "application/json", bytes.NewReader(reqBody)) 279 | if err != nil { 280 | return 281 | } 282 | bytes, err := ioutil.ReadAll(resp.Body) 283 | resp.Body.Close() 284 | if err != nil { 285 | return 286 | } 287 | err = s.checkError(bytes) 288 | if err != nil { 289 | return 290 | } 291 | body = string(bytes) 292 | return 293 | } 294 | 295 | func (s *Service) checkError(body []byte) (err error) { 296 | var res map[string]interface{} 297 | err = json.Unmarshal(body, &res) 298 | if err != nil { 299 | return 300 | } 301 | resErr, hasErr := res["error"].(map[string]interface{}) 302 | if !hasErr { 303 | return 304 | } 305 | code := resErr["code"].(float64) 306 | message := resErr["message"].(string) 307 | status := resErr["status"].(string) 308 | err = fmt.Errorf("error status: %s, code:%d, message: %s", status, int(code), message) 309 | return 310 | } 311 | -------------------------------------------------------------------------------- /service_test.go: -------------------------------------------------------------------------------- 1 | package spreadsheet 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/suite" 8 | ) 9 | 10 | // https://docs.google.com/spreadsheets/d/1mYiA2T4_QTFUkAXk0BE3u7snN2o5FgSRqxmRrn_Dzh4/edit#gid=0 11 | const spreadsheetID = "1mYiA2T4_QTFUkAXk0BE3u7snN2o5FgSRqxmRrn_Dzh4" 12 | const TestSheet2ID = 133999772 13 | 14 | type TestSuite struct { 15 | suite.Suite 16 | service *Service 17 | } 18 | 19 | func (suite *TestSuite) SetupSuite() { 20 | var err error 21 | suite.service, err = NewService() 22 | suite.Require().NoError(err) 23 | } 24 | 25 | func (suite *TestSuite) TestCreateSpreadsheet() { 26 | title := "testspreadsheet" 27 | spreadsheet, err := suite.service.CreateSpreadsheet(Spreadsheet{ 28 | Properties: Properties{ 29 | Title: title, 30 | }, 31 | }) 32 | suite.Require().NoError(err) 33 | suite.Equal(spreadsheet.Properties.Title, title) 34 | sheet, _ := spreadsheet.SheetByIndex(0) 35 | suite.service.ExpandSheet(sheet, 20, 10) 36 | } 37 | 38 | func (suite *TestSuite) TestCreateSpreadsheetWithSheets() { 39 | title := "testspreadsheet" 40 | sheetTitles := []string{"sheet 1", "sheet 2"} 41 | spreadsheet, err := suite.service.CreateSpreadsheet(Spreadsheet{ 42 | Properties: Properties{ 43 | Title: title, 44 | }, 45 | Sheets: []Sheet{ 46 | Sheet{ 47 | Properties: SheetProperties{ 48 | Title: sheetTitles[0], 49 | }, 50 | }, 51 | Sheet{ 52 | Properties: SheetProperties{ 53 | Title: sheetTitles[1], 54 | }, 55 | }, 56 | }, 57 | }) 58 | suite.Require().NoError(err) 59 | suite.Equal(spreadsheet.Properties.Title, title) 60 | suite.Equal(spreadsheet.Sheets[0].Properties.Title, sheetTitles[0]) 61 | suite.Equal(spreadsheet.Sheets[1].Properties.Title, sheetTitles[1]) 62 | _, err = spreadsheet.SheetByIndex(0) 63 | suite.Require().NoError(err) 64 | _, err = spreadsheet.SheetByIndex(1) 65 | suite.Require().NoError(err) 66 | } 67 | 68 | func (suite *TestSuite) TestFetchSpreadsheet() { 69 | spreadsheet, err := suite.service.FetchSpreadsheet(spreadsheetID) 70 | suite.Require().NoError(err) 71 | suite.Equal(spreadsheetID, spreadsheet.ID) 72 | suite.Require().Equal(2, len(spreadsheet.Sheets)) 73 | 74 | sheet := spreadsheet.Sheets[0] 75 | suite.Equal(uint(0), sheet.Properties.ID) 76 | suite.Equal("TestSheet", sheet.Properties.Title) 77 | suite.Equal(uint(0), sheet.Properties.Index) 78 | suite.Equal("GRID", sheet.Properties.SheetType) 79 | suite.True(len(sheet.Rows) >= 3) 80 | suite.True(len(sheet.Columns) >= 3) 81 | suite.Equal(uint(2), sheet.Rows[1][2].Column) 82 | for _, gridData := range sheet.Data.GridData { 83 | for i, meta := range gridData.RowMetadata { 84 | if gridData.StartRow+uint(i) == 4 { 85 | suite.True(meta.HiddenByUser) 86 | } else { 87 | suite.False(meta.HiddenByUser) 88 | } 89 | } 90 | } 91 | 92 | // Test SheetByID 93 | sheet2, err := spreadsheet.SheetByID(TestSheet2ID) 94 | suite.Require().NoError(err) 95 | suite.Equal(uint(TestSheet2ID), sheet2.Properties.ID) 96 | } 97 | 98 | func (suite *TestSuite) TestFetchSpreadsheetWithCache() { 99 | spreadsheet1, err := suite.service.FetchSpreadsheet(spreadsheetID, WithCache(3*time.Second)) 100 | suite.Require().NoError(err) 101 | suite.False(spreadsheet1.cached) 102 | spreadsheet2, err := suite.service.FetchSpreadsheet(spreadsheetID) 103 | suite.Require().NoError(err) 104 | suite.True(spreadsheet2.cached) 105 | } 106 | 107 | func (suite *TestSuite) TestAdd_DeleteSheet() { 108 | spreadsheet, err := suite.service.FetchSpreadsheet(spreadsheetID) 109 | suite.Require().NoError(err) 110 | err = suite.service.AddSheet(&spreadsheet, SheetProperties{ 111 | Title: "TestAddedSheet", 112 | Index: 1, 113 | }) 114 | suite.Require().NoError(err) 115 | 116 | sheet, err := spreadsheet.SheetByTitle("TestAddedSheet") 117 | suite.Require().NoError(err) 118 | 119 | err = suite.service.DeleteSheet(&spreadsheet, sheet.Properties.ID) 120 | suite.Require().NoError(err) 121 | } 122 | 123 | func (suite *TestSuite) TestSyncSheet() { 124 | spreadsheet, err := suite.service.FetchSpreadsheet(spreadsheetID) 125 | suite.Require().NoError(err) 126 | sheet, err := spreadsheet.SheetByTitle("TestSheet") 127 | suite.Require().NoError(err) 128 | sheet.Update(1, 6, "=SUM(D1:D2)") 129 | err = suite.service.SyncSheet(sheet) 130 | suite.NoError(err) 131 | } 132 | 133 | func (suite *TestSuite) TestDeleteRows() { 134 | spreadsheet, err := suite.service.FetchSpreadsheet(spreadsheetID) 135 | suite.Require().NoError(err) 136 | sheet, err := spreadsheet.SheetByTitle("TestSheet2") 137 | suite.Require().NoError(err) 138 | rowCount := sheet.Properties.GridProperties.RowCount 139 | 140 | err = sheet.DeleteRows(0, 1) 141 | suite.NoError(err) 142 | suite.Equal(rowCount-1, sheet.Properties.GridProperties.RowCount) 143 | } 144 | 145 | func TestRun(t *testing.T) { 146 | suite.Run(t, new(TestSuite)) 147 | } 148 | -------------------------------------------------------------------------------- /sheet.go: -------------------------------------------------------------------------------- 1 | package spreadsheet 2 | 3 | import ( 4 | "encoding/json" 5 | "strings" 6 | ) 7 | 8 | // Sheet is a sheet in a spreadsheet. 9 | type Sheet struct { 10 | Properties SheetProperties `json:"properties"` 11 | Data SheetData `json:"data"` 12 | // Merges []*GridRange `json:"merges"` 13 | // ConditionalFormats []*ConditionalFormatRule `json:"conditionalFormats"` 14 | // FilterViews []*FilterView `json:"filterViews"` 15 | // ProtectedRanges []*ProtectedRange `json:"protectedRanges"` 16 | // BasicFilter *BasicFilter `json:"basicFilter"` 17 | // Charts []*EmbeddedChart `json:"charts"` 18 | // BandedRanges []*BandedRange `json:"bandedRanges"` 19 | 20 | Spreadsheet *Spreadsheet `json:"-"` 21 | Rows [][]Cell `json:"-"` 22 | Columns [][]Cell `json:"-"` 23 | 24 | modifiedCells []*Cell 25 | newMaxRow uint 26 | newMaxColumn uint 27 | } 28 | 29 | // UnmarshalJSON embeds rows and columns to the sheet. 30 | func (sheet *Sheet) UnmarshalJSON(data []byte) error { 31 | type Alias Sheet 32 | a := (*Alias)(sheet) 33 | if err := json.Unmarshal(data, a); err != nil { 34 | return err 35 | } 36 | var maxRow, maxColumn int 37 | cells := []Cell{} 38 | for _, gridData := range sheet.Data.GridData { 39 | for rowNum, row := range gridData.RowData { 40 | for columnNum, cellData := range row.Values { 41 | r := gridData.StartRow + uint(rowNum) 42 | if int(r) > maxRow { 43 | maxRow = int(r) 44 | } 45 | c := gridData.StartColumn + uint(columnNum) 46 | if int(c) > maxColumn { 47 | maxColumn = int(c) 48 | } 49 | cell := Cell{ 50 | Row: r, 51 | Column: c, 52 | Value: cellData.FormattedValue, 53 | Note: cellData.Note, 54 | rawValue: cellData.UserEnteredValue, 55 | effectiveValue: cellData.EffectiveValue, 56 | } 57 | cells = append(cells, cell) 58 | } 59 | } 60 | } 61 | sheet.Rows, sheet.Columns = newCells(uint(maxRow), uint(maxColumn)) 62 | 63 | for _, cell := range cells { 64 | sheet.Rows[cell.Row][cell.Column] = cell 65 | sheet.Columns[cell.Column][cell.Row] = cell 66 | } 67 | 68 | sheet.modifiedCells = []*Cell{} 69 | sheet.newMaxRow = sheet.Properties.GridProperties.RowCount 70 | sheet.newMaxColumn = sheet.Properties.GridProperties.ColumnCount 71 | 72 | return nil 73 | } 74 | 75 | func (sheet *Sheet) updateCellField(row, column int, updater func(c *Cell) string) { 76 | if uint(row)+1 > sheet.newMaxRow { 77 | sheet.newMaxRow = uint(row) + 1 78 | } 79 | if uint(column)+1 > sheet.newMaxColumn { 80 | sheet.newMaxColumn = uint(column) + 1 81 | } 82 | 83 | var cell *Cell 84 | if uint(len(sheet.Rows)) < sheet.newMaxRow+1 || 85 | uint(len(sheet.Columns)) < sheet.newMaxColumn+1 { 86 | sheet.Rows = appendCells(sheet.Rows, sheet.newMaxRow, sheet.newMaxColumn, func(i, t uint) Cell { 87 | return Cell{Row: i, Column: t} 88 | }) 89 | sheet.Columns = appendCells(sheet.Columns, sheet.newMaxColumn, sheet.newMaxRow, func(i, t uint) Cell { 90 | return Cell{Row: t, Column: i} 91 | }) 92 | cell = &Cell{ 93 | Row: uint(row), 94 | Column: uint(column), 95 | } 96 | } else { 97 | cellCopy := sheet.Rows[row][column] 98 | cell = &cellCopy 99 | } 100 | 101 | var found bool 102 | for _, modifiedCell := range sheet.modifiedCells { 103 | if modifiedCell.Row == uint(row) && modifiedCell.Column == uint(column) { 104 | cell = modifiedCell 105 | found = true 106 | break 107 | } 108 | } 109 | 110 | tag := updater(cell) 111 | if len(cell.modifiedFields) == 0 { 112 | cell.modifiedFields = tag 113 | } else if strings.Index(cell.modifiedFields, tag) == -1 { 114 | cell.modifiedFields += "," + tag 115 | } 116 | 117 | cellVal := *cell 118 | cellVal.modifiedFields = "" 119 | sheet.Rows[row][column] = cellVal 120 | sheet.Columns[column][row] = cellVal 121 | 122 | if !found { 123 | sheet.modifiedCells = append(sheet.modifiedCells, cell) 124 | } 125 | } 126 | 127 | // Update updates cell changes 128 | func (sheet *Sheet) Update(row, column int, val string) { 129 | sheet.updateCellField(row, column, func(c *Cell) string { 130 | c.Value = val 131 | return "userEnteredValue" 132 | }) 133 | } 134 | 135 | // UpdateNote updates a cell's note 136 | func (sheet *Sheet) UpdateNote(row, column int, note string) { 137 | sheet.updateCellField(row, column, func(c *Cell) string { 138 | c.Note = note 139 | return "note" 140 | }) 141 | } 142 | 143 | // DeleteRows deletes rows from the sheet 144 | func (sheet *Sheet) DeleteRows(start, end int) (err error) { 145 | err = sheet.Spreadsheet.service.DeleteRows(sheet, start, end) 146 | return 147 | } 148 | 149 | // DeleteColumns deletes columns from the sheet 150 | func (sheet *Sheet) DeleteColumns(start, end int) (err error) { 151 | err = sheet.Spreadsheet.service.DeleteColumns(sheet, start, end) 152 | return 153 | } 154 | 155 | // Synchronize reflects the changes of the sheet. 156 | func (sheet *Sheet) Synchronize() (err error) { 157 | err = sheet.Spreadsheet.service.SyncSheet(sheet) 158 | return 159 | } 160 | 161 | func newCells(maxRow, maxColumn uint) (rows, columns [][]Cell) { 162 | rows = make([][]Cell, maxRow+1) 163 | for i := uint(0); i < maxRow+1; i++ { 164 | rows[i] = make([]Cell, 0, maxColumn+1) 165 | for t := uint(0); t < maxColumn+1; t++ { 166 | rows[i] = append(rows[i], Cell{Row: i, Column: t}) 167 | } 168 | } 169 | columns = make([][]Cell, maxColumn+1) 170 | for i := uint(0); i < maxColumn+1; i++ { 171 | columns[i] = make([]Cell, 0, maxRow+1) 172 | for t := uint(0); t < maxRow+1; t++ { 173 | columns[i] = append(columns[i], Cell{Row: t, Column: i}) 174 | } 175 | } 176 | return 177 | } 178 | 179 | func appendCells(cells [][]Cell, maxRow, maxColumn uint, cell func(uint, uint) Cell) [][]Cell { 180 | for i := uint(0); i < maxRow; i++ { 181 | if len(cells) == 0 || int(i) > len(cells)-1 { 182 | row := make([]Cell, 0, maxColumn) 183 | for t := uint(0); t < maxColumn; t++ { 184 | row = append(row, cell(i, t)) 185 | } 186 | cells = append(cells, row) 187 | } else { 188 | for t := uint(len(cells[i]) - 1); t < maxColumn; t++ { 189 | cells[i] = append(cells[i], cell(i, t)) 190 | } 191 | } 192 | } 193 | return cells 194 | } 195 | -------------------------------------------------------------------------------- /sheet_data.go: -------------------------------------------------------------------------------- 1 | package spreadsheet 2 | 3 | import "encoding/json" 4 | 5 | type sheetData struct { 6 | GridData []GridData `json:"data"` 7 | } 8 | 9 | // SheetData is data of the sheet 10 | type SheetData struct { 11 | sheetData 12 | } 13 | 14 | // UnmarshalJSON let SheetData to be unmarshaled 15 | func (d *SheetData) UnmarshalJSON(data []byte) error { 16 | if err := json.Unmarshal(data, &d.GridData); err != nil { 17 | return err 18 | } 19 | return nil 20 | } 21 | -------------------------------------------------------------------------------- /sheet_properties.go: -------------------------------------------------------------------------------- 1 | package spreadsheet 2 | 3 | // SheetProperties is properties of a sheet. 4 | type SheetProperties struct { 5 | ID uint `json:"sheetId,omitempty"` 6 | Title string `json:"title,omitempty"` 7 | Index uint `json:"index,omitempty"` 8 | SheetType string `json:"sheetType,omitempty"` 9 | GridProperties GridProperties `json:"gridProperties,omitempty"` 10 | Hidden bool `json:"hidden,omitempty"` 11 | TabColor TabColor `json:"tabColor,omitempty"` 12 | RightToLeft bool `json:"rightToLeft,omitempty"` 13 | } 14 | -------------------------------------------------------------------------------- /sheet_test.go: -------------------------------------------------------------------------------- 1 | package spreadsheet 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestNewCells(t *testing.T) { 10 | assert := assert.New(t) 11 | rows, columns := newCells(2, 3) 12 | assert.Equal(2+1, len(rows)) 13 | assert.Equal(3+1, len(columns)) 14 | 15 | assert.Equal(3+1, len(rows[1])) 16 | assert.Equal(2+1, len(columns[2])) 17 | 18 | assert.Equal(uint(1), rows[1][2].Row) 19 | assert.Equal(uint(2), rows[1][2].Column) 20 | 21 | assert.Equal(uint(0), columns[2][0].Row) 22 | assert.Equal(uint(2), columns[2][2].Column) 23 | } 24 | 25 | func benchmarkUpdate(t int, b *testing.B) { 26 | for f := 0; f < b.N; f++ { 27 | s := Sheet{} 28 | b.ReportAllocs() 29 | for i := 0; i < t; i++ { 30 | s.Update(i, i, "") 31 | } 32 | } 33 | } 34 | 35 | func BenchmarkUpdate1(b *testing.B) { benchmarkUpdate(1, b) } 36 | func BenchmarkUpdate10(b *testing.B) { benchmarkUpdate(10, b) } 37 | func BenchmarkUpdate100(b *testing.B) { benchmarkUpdate(100, b) } 38 | func BenchmarkUpdate1000(b *testing.B) { benchmarkUpdate(1000, b) } 39 | -------------------------------------------------------------------------------- /spreadsheet.go: -------------------------------------------------------------------------------- 1 | package spreadsheet 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | ) 7 | 8 | // Spreadsheet represents a spreadsheet. 9 | type Spreadsheet struct { 10 | ID string `json:"spreadsheetId"` 11 | Properties Properties `json:"properties"` 12 | Sheets []Sheet `json:"sheets"` 13 | // NamedRanges []*NamedRange `json:"namedRanges"` 14 | 15 | service *Service 16 | cached bool 17 | } 18 | 19 | // UnmarshalJSON embeds spreadsheet to sheets. 20 | func (spreadsheet *Spreadsheet) UnmarshalJSON(data []byte) error { 21 | type Alias Spreadsheet 22 | a := (*Alias)(spreadsheet) 23 | if err := json.Unmarshal(data, a); err != nil { 24 | return err 25 | } 26 | for i := range spreadsheet.Sheets { 27 | spreadsheet.Sheets[i].Spreadsheet = spreadsheet 28 | } 29 | return nil 30 | } 31 | 32 | // SheetByIndex gets a sheet by the given index. 33 | func (spreadsheet *Spreadsheet) SheetByIndex(index uint) (sheet *Sheet, err error) { 34 | for i, s := range spreadsheet.Sheets { 35 | if s.Properties.Index == index { 36 | sheet = &spreadsheet.Sheets[i] 37 | return 38 | } 39 | } 40 | err = errors.New("sheet not found by the index") 41 | return 42 | } 43 | 44 | // SheetByID gets a sheet by the given ID. 45 | func (spreadsheet *Spreadsheet) SheetByID(id uint) (sheet *Sheet, err error) { 46 | for i, s := range spreadsheet.Sheets { 47 | if s.Properties.ID == id { 48 | sheet = &spreadsheet.Sheets[i] 49 | return 50 | } 51 | } 52 | err = errors.New("sheet not found by the id") 53 | return 54 | } 55 | 56 | // SheetByTitle gets a sheet by the given title. 57 | func (spreadsheet *Spreadsheet) SheetByTitle(title string) (sheet *Sheet, err error) { 58 | for i, s := range spreadsheet.Sheets { 59 | if s.Properties.Title == title { 60 | sheet = &spreadsheet.Sheets[i] 61 | return 62 | } 63 | } 64 | err = errors.New("sheet not found by the title") 65 | return 66 | } 67 | -------------------------------------------------------------------------------- /tab_color.go: -------------------------------------------------------------------------------- 1 | package spreadsheet 2 | 3 | // TabColor is color of a tab. 4 | type TabColor struct { 5 | Red float32 `json:"red"` 6 | Green float32 `json:"green"` 7 | Blue float32 `json:"blue"` 8 | Alpha float32 `json:"alpha"` 9 | } 10 | -------------------------------------------------------------------------------- /update_request.go: -------------------------------------------------------------------------------- 1 | package spreadsheet 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | func newUpdateRequest(spreadsheet *Spreadsheet) (r *updateRequest, err error) { 10 | if spreadsheet == nil { 11 | err = errors.New("spreadsheet must not be nil") 12 | return 13 | } 14 | r = &updateRequest{ 15 | spreadsheet: spreadsheet, 16 | body: map[string][]map[string]interface{}{ 17 | "requests": make([]map[string]interface{}, 0, 1), 18 | }, 19 | } 20 | return 21 | } 22 | 23 | type updateRequest struct { 24 | spreadsheet *Spreadsheet 25 | body map[string][]map[string]interface{} 26 | } 27 | 28 | func (r *updateRequest) Do() (err error) { 29 | if len(r.body["requests"]) == 0 { 30 | err = errors.New("Requests must not be empty") 31 | return 32 | } 33 | path := fmt.Sprintf("/spreadsheets/%s:batchUpdate", r.spreadsheet.ID) 34 | params := make(map[string]interface{}, len(r.body)) 35 | for k, v := range r.body { 36 | params[k] = v 37 | } 38 | _, err = r.spreadsheet.service.post(path, params) 39 | return 40 | } 41 | 42 | func (r *updateRequest) UpdateSpreadsheetProperties() { 43 | 44 | } 45 | 46 | func (r *updateRequest) UpdateSheetProperties(sheet *Sheet, sheetProperties *SheetProperties) (ret *updateRequest) { 47 | ret = r 48 | params := map[string]interface{}{ 49 | "sheetId": sheet.Properties.ID, 50 | } 51 | fields := []string{} 52 | if sheetProperties.Title != sheet.Properties.Title { 53 | params["title"] = sheetProperties.Title 54 | fields = append(fields, "title") 55 | } 56 | if sheetProperties.Index != sheet.Properties.Index { 57 | params["index"] = sheetProperties.Index 58 | fields = append(fields, "index") 59 | } 60 | gridParams := make(map[string]interface{}, 0) 61 | props := sheetProperties.GridProperties 62 | currentProps := sheet.Properties.GridProperties 63 | if props.RowCount != currentProps.RowCount { 64 | gridParams["rowCount"] = props.RowCount 65 | fields = append(fields, "gridProperties.rowCount") 66 | } 67 | if props.ColumnCount != currentProps.ColumnCount { 68 | gridParams["columnCount"] = props.ColumnCount 69 | fields = append(fields, "gridProperties.columnCount") 70 | } 71 | if props.FrozenRowCount != currentProps.FrozenRowCount { 72 | gridParams["frozenRowCount"] = props.FrozenRowCount 73 | fields = append(fields, "gridProperties.frozenRowCount") 74 | } 75 | if props.FrozenColumnCount != currentProps.FrozenColumnCount { 76 | gridParams["frozenColumnCount"] = props.FrozenColumnCount 77 | fields = append(fields, "gridProperties.frozenColumnCount") 78 | } 79 | if props.HideGridlines != currentProps.HideGridlines { 80 | gridParams["hideGridlines"] = props.HideGridlines 81 | fields = append(fields, "gridProperties.hideGridlines") 82 | } 83 | if len(gridParams) > 0 { 84 | params["gridProperties"] = gridParams 85 | } 86 | if sheetProperties.Hidden != sheet.Properties.Hidden { 87 | params["hidden"] = sheetProperties.Hidden 88 | fields = append(fields, "hidden") 89 | } 90 | if sheetProperties.TabColor != sheet.Properties.TabColor { 91 | params["tabColor"] = sheetProperties.TabColor 92 | fields = append(fields, "tabColor") 93 | } 94 | if sheetProperties.RightToLeft != sheet.Properties.RightToLeft { 95 | params["rightToLeft"] = sheet.Properties.RightToLeft 96 | fields = append(fields, "rightToLeft") 97 | } 98 | if len(fields) == 0 { 99 | return 100 | } 101 | r.body["requests"] = append(r.body["requests"], map[string]interface{}{ 102 | "updateSheetProperties": map[string]interface{}{ 103 | "properties": params, 104 | "fields": strings.Join(fields, ","), 105 | }, 106 | }) 107 | return 108 | } 109 | 110 | func (r *updateRequest) UpdateDimensionProperties() { 111 | 112 | } 113 | 114 | func (r *updateRequest) UpdateNamedRange() { 115 | 116 | } 117 | 118 | func (r *updateRequest) RepeatCell() { 119 | 120 | } 121 | 122 | func (r *updateRequest) AddNamedRange() { 123 | 124 | } 125 | 126 | func (r *updateRequest) DeleteNamedRange() { 127 | 128 | } 129 | 130 | func (r *updateRequest) AddSheet(sheetProperties SheetProperties) *updateRequest { 131 | r.body["requests"] = append(r.body["requests"], map[string]interface{}{ 132 | "addSheet": map[string]interface{}{ 133 | "properties": sheetProperties, 134 | }, 135 | }) 136 | return r 137 | } 138 | 139 | func (r *updateRequest) DeleteSheet(sheetID uint) *updateRequest { 140 | r.body["requests"] = append(r.body["requests"], map[string]interface{}{ 141 | "deleteSheet": map[string]interface{}{ 142 | "sheetId": sheetID, 143 | }, 144 | }) 145 | return r 146 | } 147 | 148 | func (r *updateRequest) AutoFill() { 149 | 150 | } 151 | 152 | func (r *updateRequest) CutPaste() { 153 | 154 | } 155 | 156 | func (r *updateRequest) CopyPaste() { 157 | 158 | } 159 | 160 | func (r *updateRequest) MergeCells() { 161 | 162 | } 163 | 164 | func (r *updateRequest) UnmergeCells() { 165 | 166 | } 167 | 168 | func (r *updateRequest) UpdateBorders() { 169 | 170 | } 171 | 172 | func (r *updateRequest) UpdateCells(sheet *Sheet) *updateRequest { 173 | for _, cell := range sheet.modifiedCells { 174 | values := map[string]interface{}{} 175 | for _, field := range strings.Split(cell.modifiedFields, ",") { 176 | switch field { 177 | case "userEnteredValue": 178 | values["userEnteredValue"] = map[string]string{ 179 | cellValueType(cell.Value): cell.Value, 180 | } 181 | case "note": 182 | values["note"] = cell.Note 183 | } 184 | } 185 | r.body["requests"] = append(r.body["requests"], map[string]interface{}{ 186 | "updateCells": map[string]interface{}{ 187 | "rows": []map[string]interface{}{ 188 | map[string]interface{}{ 189 | "values": []map[string]interface{}{ 190 | values, 191 | }, 192 | }, 193 | }, 194 | "fields": cell.modifiedFields, 195 | "start": map[string]interface{}{ 196 | "sheetId": sheet.Properties.ID, 197 | "rowIndex": cell.Row, 198 | "columnIndex": cell.Column, 199 | }, 200 | }, 201 | }) 202 | } 203 | return r 204 | } 205 | 206 | func (r *updateRequest) AddFilterView() { 207 | 208 | } 209 | 210 | func (r *updateRequest) AppendCells() { 211 | 212 | } 213 | 214 | func (r *updateRequest) ClearBasicFilter() { 215 | 216 | } 217 | 218 | // DeleteDemension deletes rows or columns 219 | func (r *updateRequest) DeleteDimension(sheet *Sheet, dimension string, start, end int) (ret *updateRequest) { 220 | r.body["requests"] = append(r.body["requests"], map[string]interface{}{ 221 | "deleteDimension": map[string]interface{}{ 222 | "range": map[string]interface{}{ 223 | "sheetId": sheet.Properties.ID, 224 | "dimension": dimension, 225 | "startIndex": start, 226 | "endIndex": end, 227 | }, 228 | }, 229 | }) 230 | return r 231 | } 232 | 233 | func (r *updateRequest) DeleteEmbeddedObject() { 234 | 235 | } 236 | 237 | func (r *updateRequest) DeleteFilterView() { 238 | 239 | } 240 | 241 | func (r *updateRequest) DuplicateFilterView() { 242 | 243 | } 244 | 245 | // DuplicateSheet duplicates the contents of a sheet 246 | func (r *updateRequest) DuplicateSheet(sheet *Sheet, index int, title string) (ret *updateRequest) { 247 | r.body["requests"] = append(r.body["requests"], map[string]interface{}{ 248 | "duplicateSheet": map[string]interface{}{ 249 | "sourceSheetId": sheet.Properties.ID, 250 | "insertSheetIndex": index, 251 | "newSheetName": title, 252 | }, 253 | }) 254 | return r 255 | } 256 | 257 | func (r *updateRequest) FindReplace() { 258 | 259 | } 260 | 261 | func (r *updateRequest) InsertDimension() { 262 | 263 | } 264 | 265 | func (r *updateRequest) MoveDimension() { 266 | 267 | } 268 | 269 | func (r *updateRequest) UpdateEmbeddedObjectPosition() { 270 | 271 | } 272 | 273 | func (r *updateRequest) PasteData() { 274 | 275 | } 276 | 277 | func (r *updateRequest) TextToColumns() { 278 | 279 | } 280 | 281 | func (r *updateRequest) UpdateFilterView() { 282 | 283 | } 284 | 285 | func (r *updateRequest) AppendDimension() { 286 | 287 | } 288 | 289 | func (r *updateRequest) AddConditionalFormatRule() { 290 | 291 | } 292 | 293 | func (r *updateRequest) UpdateConditionalFormatRule() { 294 | 295 | } 296 | 297 | func (r *updateRequest) DeleteConditionalFormatRule() { 298 | 299 | } 300 | 301 | func (r *updateRequest) SortRange() { 302 | 303 | } 304 | 305 | func (r *updateRequest) SetDataValidation() { 306 | 307 | } 308 | 309 | func (r *updateRequest) SetBasicFilter() { 310 | 311 | } 312 | 313 | func (r *updateRequest) AddProtectedRange() { 314 | 315 | } 316 | 317 | func (r *updateRequest) UpdateProtectedRange() { 318 | 319 | } 320 | 321 | func (r *updateRequest) DeleteProtectedRange() { 322 | 323 | } 324 | 325 | func (r *updateRequest) AutoResizeDimensions() { 326 | 327 | } 328 | 329 | func (r *updateRequest) AddChart() { 330 | 331 | } 332 | 333 | func (r *updateRequest) UpdateChartSpec() { 334 | 335 | } 336 | 337 | func (r *updateRequest) UpdateBanding() { 338 | 339 | } 340 | 341 | func (r *updateRequest) AddBanding() { 342 | 343 | } 344 | 345 | func (r *updateRequest) DeleteBanding() { 346 | 347 | } 348 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package spreadsheet 2 | 3 | import ( 4 | "math" 5 | "strconv" 6 | ) 7 | 8 | func numberToLetter(num int) string { 9 | if num <= 0 { 10 | return "" 11 | } 12 | 13 | return numberToLetter(int((num-1)/26)) + string(byte(65+(num-1)%26)) 14 | } 15 | 16 | func cellValueType(val string) string { 17 | if len(val) == 0 { 18 | return "stringValue" 19 | } 20 | 21 | if string(val[0]) == "=" { 22 | return "formulaValue" 23 | } 24 | if _, err := strconv.Atoi(val); err == nil { 25 | return "numberValue" 26 | } 27 | if floatVal, err := strconv.ParseFloat(val, 64); err == nil && isNumericFloat(floatVal) { 28 | return "numberValue" 29 | } 30 | if val == "TRUE" || val == "FALSE" { 31 | return "boolValue" 32 | } 33 | 34 | return "stringValue" 35 | } 36 | 37 | func isNumericFloat(val float64) bool { 38 | if math.IsInf(val, 1) || math.IsInf(val, -1) || math.IsNaN(val) { 39 | return false 40 | } 41 | 42 | return true 43 | } 44 | -------------------------------------------------------------------------------- /utils_test.go: -------------------------------------------------------------------------------- 1 | package spreadsheet 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestNumberToLetter(t *testing.T) { 11 | assert := assert.New(t) 12 | assert.Equal("C", numberToLetter(3)) 13 | assert.Equal("Z", numberToLetter(26)) 14 | assert.Equal("AB", numberToLetter(28)) 15 | assert.Equal("AZ", numberToLetter(52)) 16 | assert.Equal("AAC", numberToLetter(705)) 17 | assert.Equal("YZ", numberToLetter(676)) 18 | assert.Equal("ZA", numberToLetter(677)) 19 | } 20 | 21 | func TestCellValueType(t *testing.T) { 22 | assert := assert.New(t) 23 | assert.Equal("stringValue", cellValueType("")) 24 | assert.Equal("formulaValue", cellValueType("=ABS(-2)")) 25 | assert.Equal("numberValue", cellValueType("-2")) 26 | assert.Equal("numberValue", cellValueType("-2.23333")) 27 | assert.Equal("boolValue", cellValueType("TRUE")) 28 | assert.Equal("stringValue", cellValueType("test")) 29 | assert.Equal("stringValue", cellValueType("inf")) 30 | assert.Equal("stringValue", cellValueType("Infinity")) 31 | assert.Equal("stringValue", cellValueType("-inf")) 32 | assert.Equal("stringValue", cellValueType("-Infinity")) 33 | assert.Equal("stringValue", cellValueType("NaN")) 34 | } 35 | 36 | func TestIsNumericFloat(t *testing.T) { 37 | assert := assert.New(t) 38 | assert.True(isNumericFloat(-2.23333)) 39 | assert.True(isNumericFloat(0.01234)) 40 | assert.False(isNumericFloat(math.Inf(1))) 41 | assert.False(isNumericFloat(math.Inf(-1))) 42 | assert.False(isNumericFloat(math.NaN())) 43 | } 44 | 45 | func BenchmarkNumberToLetter(b *testing.B) { 46 | b.ReportAllocs() 47 | for i := 0; i < b.N; i++ { 48 | _ = numberToLetter(i) 49 | } 50 | } 51 | --------------------------------------------------------------------------------