diff --git a/config/routes.go b/config/routes.go index e95b5f2b7d16e92d8df7ca478308a4a05aff7ddf..7a89f6ede653cfc92f55e2740afab6f56a8e819a 100644 --- a/config/routes.go +++ b/config/routes.go @@ -13,4 +13,8 @@ func GddRoutes(route fiber.Router) { route.Get("gdd/cfs", controllers.GetCfsGDD) route.Get("gdd/analog", controllers.GetAnalogYear) route.Get("gdd/confidence", controllers.GetConfidenceInterval) + + route.Get("freezing-dates", controllers.GetFreezingDates) + + route.Get("gdd/csv", controllers.GetCSVFile) } diff --git a/controllers/data_download_controller.go b/controllers/data_download_controller.go new file mode 100644 index 0000000000000000000000000000000000000000..d94b0655bf0df3f33531cf350bffb8fdc786199f --- /dev/null +++ b/controllers/data_download_controller.go @@ -0,0 +1,42 @@ +package controllers + +import ( + "dawn-weather/models" + "dawn-weather/services" + + "github.com/gofiber/fiber/v2" +) + +// GetCSVFile godoc +// @Summary Get gdd data csv +// @Tags Gdd +// @Description Get gdd data csv +// @Accept json +// @Produce text/csv +// @Failure 400 {object} errors.StandardError +// @Param analog query boolean false "Add analog data to csv" +// @Param cfs query boolean false "Add cfs data to csv" +// @Param cfs_upper query boolean false "Add cfs upper bound data to csv" +// @Param cfs_lower query boolean false "Add cfs lower bound data to csv" +// @Param comparison query boolean false "Add comparison year data to csv" +// @Param first_freezing query boolean false "Add first freezing date data to csv" +// @Param gefs query boolean false "Add gefs data to csv" +// @Param last_freezing query boolean false "Add last freezing date data to csv" +// @Param maximum query boolean false "Add maximum boundary of confidence interval data to csv" +// @Param minimum query boolean false "Add minimum boundary of confidence interval data to csv" +// @Param normals query boolean false "Add thirty year normal data to csv" +// @Param primary query boolean false "Add primary GDD data to csv" +// @Param comparison_year query int false "Comparison year to use" +// @Param product query string true "Crop to calculate gdd for" Enums(corn, soybean, sunflower, tomato, sugar_beet, peanut, cotton, potato, wheat, pea, oat, spring_wheat, rice, sorghum) +// @Param range query number false "Confidence interval percentage" +// @Param temperature query int false "Freezing date temperature" +// @Param year query int false "Year to get primary data for" +// @Param latitude query number true "Latitude to search for" +// @Param longitude query number true "Longitude to search for" +// @Router /api/weather/gdd/csv [get] +func GetCSVFile(c *fiber.Ctx) error { + request := models.CSVRequest{}.Build(c) + fileText := services.GetDataDownload(request) + e := c.Status(fiber.StatusOK).SendString(fileText) + return e +} diff --git a/controllers/freezing_dates_controller.go b/controllers/freezing_dates_controller.go new file mode 100644 index 0000000000000000000000000000000000000000..25f02cd9f07cbe4696868a6af9a1ccd2af2b58a1 --- /dev/null +++ b/controllers/freezing_dates_controller.go @@ -0,0 +1,25 @@ +package controllers + +import ( + "dawn-weather/models" + "dawn-weather/services" + + "github.com/gofiber/fiber/v2" +) + +// GetFreezingDates godoc +// @Summary get freezing dates +// @Tags Freezing Dates +// @Description get freezing dates +// @Accept json +// @Produce json +// @Success 200 {object} models.FreezingDateResponse +// @Failure 400 {object} errors.StandardError +// @Param latitude query number true "Latitude to search for" +// @Param longitude query number true "Longitude to search for" +// @Param freezing_temp query number true "Freezing temperature to use" +// @Router /api/weather/freezing-dates [get] +func GetFreezingDates(c *fiber.Ctx) error { + request := models.FreezingDateRequest{}.Build(c) + return c.Status(fiber.StatusOK).JSON(services.GetFreezingDate(request)) +} diff --git a/controllers/gdd_controller.go b/controllers/gdd_controller.go index b343cfc3a2d4ef008f663efbdb5ef979d2c9b413..8619d734eddadf1665b02f300bf244ef54589f42 100644 --- a/controllers/gdd_controller.go +++ b/controllers/gdd_controller.go @@ -3,6 +3,7 @@ package controllers import ( "dawn-weather/models" "dawn-weather/services" + "github.com/gofiber/fiber/v2" ) diff --git a/docs/docs.go b/docs/docs.go index 4058d36cbf9f606ababdbee58d38b74f35fbaa43..9f7bc0f339ecf1e4bd57d9a3c515ed5a7b5786d5 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -27,6 +27,58 @@ var doc = `{ "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { + "/api/weather/freezing-dates": { + "get": { + "description": "get freezing dates", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Freezing Dates" + ], + "summary": "get freezing dates", + "parameters": [ + { + "type": "number", + "description": "Latitude to search for", + "name": "latitude", + "in": "query", + "required": true + }, + { + "type": "number", + "description": "Longitude to search for", + "name": "longitude", + "in": "query", + "required": true + }, + { + "type": "number", + "description": "Freezing temperature to use", + "name": "freezing_temp", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.FreezingDateResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/errors.StandardError" + } + } + } + } + }, "/api/weather/gdd/analog": { "get": { "description": "Get analog year", @@ -72,6 +124,81 @@ var doc = `{ } } }, + "/api/weather/gdd/cfs": { + "get": { + "description": "Get GDD values calculated from CFS", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Gdd" + ], + "summary": "Get GDD values calculated from CFS", + "parameters": [ + { + "enum": [ + "corn", + "soybean", + "sunflower", + "tomato", + "sugar_beet", + "peanut", + "cotton", + "potato", + "wheat", + "pea", + "oat", + "spring_wheat", + "rice", + "sorghum" + ], + "type": "string", + "description": "Crop to calculate gdd for", + "name": "product", + "in": "query", + "required": true + }, + { + "type": "number", + "description": "Latitude to search for", + "name": "latitude", + "in": "query", + "required": true + }, + { + "type": "number", + "description": "Longitude to search for", + "name": "longitude", + "in": "query", + "required": true + }, + { + "type": "boolean", + "description": "Accumulate gdd values", + "name": "accumulate", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.CfsGddResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/errors.StandardError" + } + } + } + } + }, "/api/weather/gdd/confidence": { "get": { "description": "Get confidence interval", @@ -86,6 +213,29 @@ var doc = `{ ], "summary": "Get confidence interval", "parameters": [ + { + "enum": [ + "corn", + "soybean", + "sunflower", + "tomato", + "sugar_beet", + "peanut", + "cotton", + "potato", + "wheat", + "pea", + "oat", + "spring_wheat", + "rice", + "sorghum" + ], + "type": "string", + "description": "Crop to calculate gdd for", + "name": "product", + "in": "query", + "required": true + }, { "type": "number", "description": "Latitude to search for", @@ -117,6 +267,170 @@ var doc = `{ } } }, + "/api/weather/gdd/csv": { + "get": { + "description": "Get gdd data csv", + "consumes": [ + "application/json" + ], + "produces": [ + "text/csv" + ], + "tags": [ + "Gdd" + ], + "summary": "Get gdd data csv", + "parameters": [ + { + "type": "boolean", + "description": "Add analog data to csv", + "name": "analog", + "in": "query" + }, + { + "type": "boolean", + "description": "Add cfs data to csv", + "name": "cfs", + "in": "query" + }, + { + "type": "boolean", + "description": "Add cfs upper bound data to csv", + "name": "cfs_upper", + "in": "query" + }, + { + "type": "boolean", + "description": "Add cfs lower bound data to csv", + "name": "cfs_lower", + "in": "query" + }, + { + "type": "boolean", + "description": "Add comparison year data to csv", + "name": "comparison", + "in": "query" + }, + { + "type": "boolean", + "description": "Add first freezing date data to csv", + "name": "first_freezing", + "in": "query" + }, + { + "type": "boolean", + "description": "Add gefs data to csv", + "name": "gefs", + "in": "query" + }, + { + "type": "boolean", + "description": "Add last freezing date data to csv", + "name": "last_freezing", + "in": "query" + }, + { + "type": "boolean", + "description": "Add maximum boundary of confidence interval data to csv", + "name": "maximum", + "in": "query" + }, + { + "type": "boolean", + "description": "Add minimum boundary of confidence interval data to csv", + "name": "minimum", + "in": "query" + }, + { + "type": "boolean", + "description": "Add thirty year normal data to csv", + "name": "normals", + "in": "query" + }, + { + "type": "boolean", + "description": "Add primary GDD data to csv", + "name": "primary", + "in": "query" + }, + { + "type": "integer", + "description": "Comparison year to use", + "name": "comparison_year", + "in": "query" + }, + { + "enum": [ + "corn", + "soybean", + "sunflower", + "tomato", + "sugar_beet", + "peanut", + "cotton", + "potato", + "wheat", + "pea", + "oat", + "spring_wheat", + "rice", + "sorghum" + ], + "type": "string", + "description": "Crop to calculate gdd for", + "name": "product", + "in": "query", + "required": true + }, + { + "type": "number", + "description": "Confidence interval percentage", + "name": "range", + "in": "query" + }, + { + "type": "integer", + "description": "Freezing date temperature", + "name": "temperature", + "in": "query" + }, + { + "type": "integer", + "description": "Year to get primary data for", + "name": "year", + "in": "query" + }, + { + "type": "number", + "description": "Latitude to search for", + "name": "latitude", + "in": "query", + "required": true + }, + { + "type": "number", + "description": "Longitude to search for", + "name": "longitude", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/errors.StandardError" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/errors.StandardError" + } + } + } + } + }, "/api/weather/gdd/daily": { "get": { "description": "get gdd values", @@ -390,22 +704,57 @@ var doc = `{ } } }, + "models.CfsGddResponse": { + "type": "object", + "properties": { + "closest_latitude": { + "type": "number" + }, + "closest_longitude": { + "type": "number" + }, + "first_date": { + "type": "string" + }, + "gdd_values": { + "type": "array", + "items": { + "type": "number" + } + }, + "lower_values": { + "type": "array", + "items": { + "type": "number" + } + }, + "product": { + "type": "string" + }, + "upper_values": { + "type": "array", + "items": { + "type": "number" + } + } + } + }, "models.ConfidenceIntervalResposne": { "type": "object", "properties": { - "closestLatitude": { + "closest_latitude": { "type": "number" }, - "closestLongitude": { + "closest_longitude": { "type": "number" }, - "lowerBound": { + "lower_bound": { "type": "array", "items": { "type": "number" } }, - "upperBound": { + "upper_bound": { "type": "array", "items": { "type": "number" @@ -413,6 +762,29 @@ var doc = `{ } } }, + "models.FreezingDateResponse": { + "type": "object", + "properties": { + "closest_latitude": { + "type": "number" + }, + "closest_longitude": { + "type": "number" + }, + "first_date_counts": { + "type": "object", + "additionalProperties": { + "type": "integer" + } + }, + "last_date_counts": { + "type": "object", + "additionalProperties": { + "type": "integer" + } + } + } + }, "models.GddResponse": { "type": "object", "properties": { @@ -457,8 +829,20 @@ var doc = `{ "last_date": { "type": "string" }, + "lower_values": { + "type": "array", + "items": { + "type": "number" + } + }, "product": { "type": "string" + }, + "upper_values": { + "type": "array", + "items": { + "type": "number" + } } } } diff --git a/docs/swagger.json b/docs/swagger.json index 270dcfa80555a5ff9cfbc599773d7a870a2fea73..8ad42559d8cc70fa7d056061fa67bed8da4ace11 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -12,6 +12,58 @@ "host": "localhost:8080", "basePath": "/", "paths": { + "/api/weather/freezing-dates": { + "get": { + "description": "get freezing dates", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Freezing Dates" + ], + "summary": "get freezing dates", + "parameters": [ + { + "type": "number", + "description": "Latitude to search for", + "name": "latitude", + "in": "query", + "required": true + }, + { + "type": "number", + "description": "Longitude to search for", + "name": "longitude", + "in": "query", + "required": true + }, + { + "type": "number", + "description": "Freezing temperature to use", + "name": "freezing_temp", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.FreezingDateResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/errors.StandardError" + } + } + } + } + }, "/api/weather/gdd/analog": { "get": { "description": "Get analog year", @@ -57,6 +109,81 @@ } } }, + "/api/weather/gdd/cfs": { + "get": { + "description": "Get GDD values calculated from CFS", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Gdd" + ], + "summary": "Get GDD values calculated from CFS", + "parameters": [ + { + "enum": [ + "corn", + "soybean", + "sunflower", + "tomato", + "sugar_beet", + "peanut", + "cotton", + "potato", + "wheat", + "pea", + "oat", + "spring_wheat", + "rice", + "sorghum" + ], + "type": "string", + "description": "Crop to calculate gdd for", + "name": "product", + "in": "query", + "required": true + }, + { + "type": "number", + "description": "Latitude to search for", + "name": "latitude", + "in": "query", + "required": true + }, + { + "type": "number", + "description": "Longitude to search for", + "name": "longitude", + "in": "query", + "required": true + }, + { + "type": "boolean", + "description": "Accumulate gdd values", + "name": "accumulate", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.CfsGddResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/errors.StandardError" + } + } + } + } + }, "/api/weather/gdd/confidence": { "get": { "description": "Get confidence interval", @@ -71,6 +198,29 @@ ], "summary": "Get confidence interval", "parameters": [ + { + "enum": [ + "corn", + "soybean", + "sunflower", + "tomato", + "sugar_beet", + "peanut", + "cotton", + "potato", + "wheat", + "pea", + "oat", + "spring_wheat", + "rice", + "sorghum" + ], + "type": "string", + "description": "Crop to calculate gdd for", + "name": "product", + "in": "query", + "required": true + }, { "type": "number", "description": "Latitude to search for", @@ -102,6 +252,170 @@ } } }, + "/api/weather/gdd/csv": { + "get": { + "description": "Get gdd data csv", + "consumes": [ + "application/json" + ], + "produces": [ + "text/csv" + ], + "tags": [ + "Gdd" + ], + "summary": "Get gdd data csv", + "parameters": [ + { + "type": "boolean", + "description": "Add analog data to csv", + "name": "analog", + "in": "query" + }, + { + "type": "boolean", + "description": "Add cfs data to csv", + "name": "cfs", + "in": "query" + }, + { + "type": "boolean", + "description": "Add cfs upper bound data to csv", + "name": "cfs_upper", + "in": "query" + }, + { + "type": "boolean", + "description": "Add cfs lower bound data to csv", + "name": "cfs_lower", + "in": "query" + }, + { + "type": "boolean", + "description": "Add comparison year data to csv", + "name": "comparison", + "in": "query" + }, + { + "type": "boolean", + "description": "Add first freezing date data to csv", + "name": "first_freezing", + "in": "query" + }, + { + "type": "boolean", + "description": "Add gefs data to csv", + "name": "gefs", + "in": "query" + }, + { + "type": "boolean", + "description": "Add last freezing date data to csv", + "name": "last_freezing", + "in": "query" + }, + { + "type": "boolean", + "description": "Add maximum boundary of confidence interval data to csv", + "name": "maximum", + "in": "query" + }, + { + "type": "boolean", + "description": "Add minimum boundary of confidence interval data to csv", + "name": "minimum", + "in": "query" + }, + { + "type": "boolean", + "description": "Add thirty year normal data to csv", + "name": "normals", + "in": "query" + }, + { + "type": "boolean", + "description": "Add primary GDD data to csv", + "name": "primary", + "in": "query" + }, + { + "type": "integer", + "description": "Comparison year to use", + "name": "comparison_year", + "in": "query" + }, + { + "enum": [ + "corn", + "soybean", + "sunflower", + "tomato", + "sugar_beet", + "peanut", + "cotton", + "potato", + "wheat", + "pea", + "oat", + "spring_wheat", + "rice", + "sorghum" + ], + "type": "string", + "description": "Crop to calculate gdd for", + "name": "product", + "in": "query", + "required": true + }, + { + "type": "number", + "description": "Confidence interval percentage", + "name": "range", + "in": "query" + }, + { + "type": "integer", + "description": "Freezing date temperature", + "name": "temperature", + "in": "query" + }, + { + "type": "integer", + "description": "Year to get primary data for", + "name": "year", + "in": "query" + }, + { + "type": "number", + "description": "Latitude to search for", + "name": "latitude", + "in": "query", + "required": true + }, + { + "type": "number", + "description": "Longitude to search for", + "name": "longitude", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/errors.StandardError" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/errors.StandardError" + } + } + } + } + }, "/api/weather/gdd/daily": { "get": { "description": "get gdd values", @@ -375,22 +689,57 @@ } } }, + "models.CfsGddResponse": { + "type": "object", + "properties": { + "closest_latitude": { + "type": "number" + }, + "closest_longitude": { + "type": "number" + }, + "first_date": { + "type": "string" + }, + "gdd_values": { + "type": "array", + "items": { + "type": "number" + } + }, + "lower_values": { + "type": "array", + "items": { + "type": "number" + } + }, + "product": { + "type": "string" + }, + "upper_values": { + "type": "array", + "items": { + "type": "number" + } + } + } + }, "models.ConfidenceIntervalResposne": { "type": "object", "properties": { - "closestLatitude": { + "closest_latitude": { "type": "number" }, - "closestLongitude": { + "closest_longitude": { "type": "number" }, - "lowerBound": { + "lower_bound": { "type": "array", "items": { "type": "number" } }, - "upperBound": { + "upper_bound": { "type": "array", "items": { "type": "number" @@ -398,6 +747,29 @@ } } }, + "models.FreezingDateResponse": { + "type": "object", + "properties": { + "closest_latitude": { + "type": "number" + }, + "closest_longitude": { + "type": "number" + }, + "first_date_counts": { + "type": "object", + "additionalProperties": { + "type": "integer" + } + }, + "last_date_counts": { + "type": "object", + "additionalProperties": { + "type": "integer" + } + } + } + }, "models.GddResponse": { "type": "object", "properties": { @@ -442,8 +814,20 @@ "last_date": { "type": "string" }, + "lower_values": { + "type": "array", + "items": { + "type": "number" + } + }, "product": { "type": "string" + }, + "upper_values": { + "type": "array", + "items": { + "type": "number" + } } } } diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 1c95e8169f7b2cd7079439323e7276bbea54e9d7..f1fff78dcb5ef60d5ecfe21b1f3f4b90e91e173d 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -25,21 +25,59 @@ definitions: closest_longitude: type: number type: object + models.CfsGddResponse: + properties: + closest_latitude: + type: number + closest_longitude: + type: number + first_date: + type: string + gdd_values: + items: + type: number + type: array + lower_values: + items: + type: number + type: array + product: + type: string + upper_values: + items: + type: number + type: array + type: object models.ConfidenceIntervalResposne: properties: - closestLatitude: + closest_latitude: type: number - closestLongitude: + closest_longitude: type: number - lowerBound: + lower_bound: items: type: number type: array - upperBound: + upper_bound: items: type: number type: array type: object + models.FreezingDateResponse: + properties: + closest_latitude: + type: number + closest_longitude: + type: number + first_date_counts: + additionalProperties: + type: integer + type: object + last_date_counts: + additionalProperties: + type: integer + type: object + type: object models.GddResponse: properties: closest_latitude: @@ -69,8 +107,16 @@ definitions: type: array last_date: type: string + lower_values: + items: + type: number + type: array product: type: string + upper_values: + items: + type: number + type: array type: object host: localhost:8080 info: @@ -81,6 +127,41 @@ info: title: Dawn Weather Server version: "1.0" paths: + /api/weather/freezing-dates: + get: + consumes: + - application/json + description: get freezing dates + parameters: + - description: Latitude to search for + in: query + name: latitude + required: true + type: number + - description: Longitude to search for + in: query + name: longitude + required: true + type: number + - description: Freezing temperature to use + in: query + name: freezing_temp + required: true + type: number + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.FreezingDateResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/errors.StandardError' + summary: get freezing dates + tags: + - Freezing Dates /api/weather/gdd/analog: get: consumes: @@ -111,12 +192,87 @@ paths: summary: Get analog year tags: - Gdd + /api/weather/gdd/cfs: + get: + consumes: + - application/json + description: Get GDD values calculated from CFS + parameters: + - description: Crop to calculate gdd for + enum: + - corn + - soybean + - sunflower + - tomato + - sugar_beet + - peanut + - cotton + - potato + - wheat + - pea + - oat + - spring_wheat + - rice + - sorghum + in: query + name: product + required: true + type: string + - description: Latitude to search for + in: query + name: latitude + required: true + type: number + - description: Longitude to search for + in: query + name: longitude + required: true + type: number + - description: Accumulate gdd values + in: query + name: accumulate + required: true + type: boolean + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.CfsGddResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/errors.StandardError' + summary: Get GDD values calculated from CFS + tags: + - Gdd /api/weather/gdd/confidence: get: consumes: - application/json description: Get confidence interval parameters: + - description: Crop to calculate gdd for + enum: + - corn + - soybean + - sunflower + - tomato + - sugar_beet + - peanut + - cotton + - potato + - wheat + - pea + - oat + - spring_wheat + - rice + - sorghum + in: query + name: product + required: true + type: string - description: Latitude to search for in: query name: latitude @@ -141,6 +297,120 @@ paths: summary: Get confidence interval tags: - Gdd + /api/weather/gdd/csv: + get: + consumes: + - application/json + description: Get gdd data csv + parameters: + - description: Add analog data to csv + in: query + name: analog + type: boolean + - description: Add cfs data to csv + in: query + name: cfs + type: boolean + - description: Add cfs upper bound data to csv + in: query + name: cfs_upper + type: boolean + - description: Add cfs lower bound data to csv + in: query + name: cfs_lower + type: boolean + - description: Add comparison year data to csv + in: query + name: comparison + type: boolean + - description: Add first freezing date data to csv + in: query + name: first_freezing + type: boolean + - description: Add gefs data to csv + in: query + name: gefs + type: boolean + - description: Add last freezing date data to csv + in: query + name: last_freezing + type: boolean + - description: Add maximum boundary of confidence interval data to csv + in: query + name: maximum + type: boolean + - description: Add minimum boundary of confidence interval data to csv + in: query + name: minimum + type: boolean + - description: Add thirty year normal data to csv + in: query + name: normals + type: boolean + - description: Add primary GDD data to csv + in: query + name: primary + type: boolean + - description: Comparison year to use + in: query + name: comparison_year + type: integer + - description: Crop to calculate gdd for + enum: + - corn + - soybean + - sunflower + - tomato + - sugar_beet + - peanut + - cotton + - potato + - wheat + - pea + - oat + - spring_wheat + - rice + - sorghum + in: query + name: product + required: true + type: string + - description: Confidence interval percentage + in: query + name: range + type: number + - description: Freezing date temperature + in: query + name: temperature + type: integer + - description: Year to get primary data for + in: query + name: year + type: integer + - description: Latitude to search for + in: query + name: latitude + required: true + type: number + - description: Longitude to search for + in: query + name: longitude + required: true + type: number + produces: + - text/csv + responses: + "200": + description: OK + schema: + $ref: '#/definitions/errors.StandardError' + "400": + description: Bad Request + schema: + $ref: '#/definitions/errors.StandardError' + summary: Get gdd data csv + tags: + - Gdd /api/weather/gdd/daily: get: consumes: diff --git a/go.mod b/go.mod index 01b2d5776a497a53a1166acad63779ee6ad34ce1..17e4177e0d7784332c510f8bc893cfb3b5ed8406 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/go-ozzo/ozzo-validation v3.6.0+incompatible // indirect github.com/gofiber/adaptor/v2 v2.1.9 // indirect github.com/gofiber/fiber/v2 v2.16.0 + github.com/google/uuid v1.3.0 // indirect github.com/klauspost/compress v1.13.3 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/prometheus/client_golang v1.11.0 // indirect diff --git a/go.sum b/go.sum index 7ed8de744baaea6801be0362a8e621208185ea1f..67138a70881b7d86405a4213021b4207ab494e17 100644 --- a/go.sum +++ b/go.sum @@ -271,6 +271,8 @@ github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLe github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= diff --git a/models/cfs.go b/models/cfs.go index fdfd0711366282aa240f77120fa8cc40247d3832..36ddbd8d5bb211fd6e307aa226dff606b861a147 100644 --- a/models/cfs.go +++ b/models/cfs.go @@ -7,5 +7,7 @@ type CfsGddResponse struct { ClosestLatitude float64 `json:"closest_latitude"` ClosestLongitude float64 `json:"closest_longitude"` GddValues []float64 `json:"gdd_values"` + UpperBound []float64 `json:"upper_values"` + LowerBound []float64 `json:"lower_values"` FirstDate time.Time `json:"first_date"` } diff --git a/models/csv.go b/models/csv.go new file mode 100644 index 0000000000000000000000000000000000000000..cb3dd7bdb19dbbf41430ca3150429b95bb2c2b15 --- /dev/null +++ b/models/csv.go @@ -0,0 +1,116 @@ +package models + +import ( + "dawn-weather/errors" + "strconv" + "time" + + validation "github.com/go-ozzo/ozzo-validation" + "github.com/gofiber/fiber/v2" +) + +// type GddResponse struct { +// Product string `json:"product"` +// ClosestLatitude float64 `json:"closest_latitude"` +// ClosestLongitude float64 `json:"closest_longitude"` +// GddValues []float64 `json:"gdd_values"` +// LastDate time.Time `json:"last_date"` +// } + +type CSVRequest struct { + Analog bool `json:"analog"` + Cfs bool `json:"cfs"` + CfsLower bool `json:"cfs_lower"` + CfsUpper bool `json:"cfs_upper"` + Comparison bool `json:"comparison"` + FirstFreezing bool `json:"first_freezing"` + Gefs bool `json:"gefs"` + LastFreezing bool `json:"last_freezing"` + Maximum bool `json:"maximum"` + Minimum bool `json:"minimum"` + Normals bool `json:"normals"` + Primary bool `json:"primary"` + ComparisonYear int `json:"comparison_year"` + Product string `json:"product"` + Range int `json:"range"` + Temperature int `json:"temperature"` + Year int `json:"year"` + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` +} + +func (r CSVRequest) Validate() error { + return validation.ValidateStruct(&r, + validation.Field(&r.Year, validation.Required, validation.Min(1981), validation.Max(time.Now().Year())), + validation.Field(&r.Product, validation.Required, validation.Length(1, 100)), + validation.Field(&r.Latitude, validation.Required, validation.Min(-90.0), validation.Max(90.0)), + validation.Field(&r.Longitude, validation.Required, validation.Min(-180.0), validation.Max(180.0)), + ) +} + +// func (r GddRequest) ValidateNoYear() error { +// return validation.ValidateStruct(&r, +// validation.Field(&r.Product, validation.Required, validation.Length(1, 100)), +// validation.Field(&r.Latitude, validation.Required, validation.Min(-90.0), validation.Max(90.0)), +// validation.Field(&r.Longitude, validation.Required, validation.Min(-180.0), validation.Max(180.0)), +// ) +// } + +func parseBool(str string) bool { + a, e := strconv.ParseBool(str) + if e != nil { + return false + } + return a +} + +func atoi(str string) int { + a, e := strconv.Atoi(str) + if e != nil { + return 0 + } + return a +} + +func (r CSVRequest) Build(c *fiber.Ctx) CSVRequest { + + latitude, _ := strconv.ParseFloat(c.Query("latitude", "-1000.0"), 64) + longitude, _ := strconv.ParseFloat(c.Query("longitude", "-1000.0"), 64) + + newRequest := CSVRequest{ + Analog: parseBool(c.Query("analog")), + Cfs: parseBool(c.Query("cfs")), + CfsLower: parseBool(c.Query("cfs_lower")), + CfsUpper: parseBool(c.Query("cfs_upper")), + Comparison: parseBool(c.Query("comparison")), + FirstFreezing: parseBool(c.Query("first_freezing")), + Gefs: parseBool(c.Query("gefs")), + LastFreezing: parseBool(c.Query("last_freezing")), + Maximum: parseBool(c.Query("maximum")), + Minimum: parseBool(c.Query("minimum")), + Normals: parseBool(c.Query("normals")), + Primary: parseBool(c.Query("primary")), + + ComparisonYear: atoi(c.Query("comparison_year", "0")), + Product: c.Query("product", "NONE"), + Range: atoi(c.Query("range", "0")), + Temperature: atoi(c.Query("temperature", "0")), + Year: atoi(c.Query("year", "0")), + Latitude: latitude, + Longitude: longitude, + } + + if newRequest.Validate() != nil { + panic(errors.BAD_REQUEST) + } + + return newRequest +} + +// func (r GddRequest) BuildLocation() entities.Location { +// l := entities.Location{ +// Type: "Point", +// Coordinates: []float64{r.Longitude, r.Latitude}, +// } +// return l +// } diff --git a/models/freezing_dates.go b/models/freezing_dates.go new file mode 100644 index 0000000000000000000000000000000000000000..5d67b8443c3923cf51bda3584f08eaff831bb2c0 --- /dev/null +++ b/models/freezing_dates.go @@ -0,0 +1,63 @@ +package models + +import ( + "dawn-weather/errors" + "dawn-weather/persistence/entities" + "strconv" + "time" + + validation "github.com/go-ozzo/ozzo-validation" + "github.com/gofiber/fiber/v2" +) + +type DateCount struct { + Date time.Time `json:"date"` + Count int `json:"count"` +} + +type FreezingDateResponse struct { + ClosestLatitude float64 `json:"closest_latitude"` + ClosestLongitude float64 `json:"closest_longitude"` + FirstDateCounts map[string]int `json:"first_date_counts"` + LastDateCounts map[string]int `json:"last_date_counts"` +} + +type FreezingDateRequest struct { + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` + FreezingTemp int `json:"freezing_temp"` +} + +func (r FreezingDateRequest) Validate() error { + return validation.ValidateStruct(&r, + validation.Field(&r.Latitude, validation.Required, validation.Min(-90.0), validation.Max(90.0)), + validation.Field(&r.Longitude, validation.Required, validation.Min(-180.0), validation.Max(180.0)), + validation.Field(&r.FreezingTemp, validation.Required, validation.Min(25), validation.Max(32)), + ) +} + +func (r FreezingDateRequest) Build(c *fiber.Ctx) FreezingDateRequest { + latitude, _ := strconv.ParseFloat(c.Query("latitude", "-10000"), 64) + longitude, _ := strconv.ParseFloat(c.Query("longitude", "-10000"), 64) + freezingTemp, _ := strconv.Atoi(c.Query("freezing_temp", "0")) + + rNew := FreezingDateRequest{ + Latitude: latitude, + Longitude: longitude, + FreezingTemp: freezingTemp, + } + + if rNew.Validate() != nil { + panic(errors.BAD_REQUEST) + } + + return rNew +} + +func (r FreezingDateRequest) BuildLocation() entities.Location { + l := entities.Location{ + Type: "Point", + Coordinates: []float64{r.Longitude, r.Latitude}, + } + return l +} diff --git a/models/gefs.go b/models/gefs.go index b7cd5620fcb3fe45ecd1c909c8fed6070ceaf564..34da997be5cce39f7b48e975f0cd3f64c1cad451 100644 --- a/models/gefs.go +++ b/models/gefs.go @@ -7,6 +7,8 @@ type GefsGddResponse struct { ClosestLatitude float64 `json:"closest_latitude"` ClosestLongitude float64 `json:"closest_longitude"` GddValues []float64 `json:"gdd_values"` + UpperBound []float64 `json:"upper_values"` + LowerBound []float64 `json:"lower_values"` LastDate time.Time `json:"last_date"` FirstDate time.Time `json:"first_date"` } diff --git a/persistence/entities/freezing_dates.go b/persistence/entities/freezing_dates.go new file mode 100644 index 0000000000000000000000000000000000000000..ab115cf56c168169b0550449d4f21434ecaadc51 --- /dev/null +++ b/persistence/entities/freezing_dates.go @@ -0,0 +1,9 @@ +package entities + +type FreezingDates struct { + ID string `bson:"_id,omitempty"` + Location Location `bson:"location,omitempty"` + FirstDates [][]int `bson:"first_dates,omitempty"` + LastDates [][]int `bson:"last_dates,omitempty"` + AllowedTemps []int `bson:"allowed_temps,omitempty"` +} diff --git a/persistence/entities/gdd.go b/persistence/entities/gdd.go index b0a1de06315811a7079c2b35ea08474048d26679..f17aff3b98f8a0e27a5b6e32a6c07f68c4396530 100644 --- a/persistence/entities/gdd.go +++ b/persistence/entities/gdd.go @@ -26,13 +26,17 @@ type GefsGdd struct { Location Location `bson:"location,omitempty"` MinTemp float64 `bson:"min_temp,omitempty"` MaxTemp float64 `bson:"max_temp,omitempty"` + VarMin float64 `bson:"var_min,omitempty"` + VarMax float64 `bson:"var_max,omitempty"` Date primitive.DateTime `bson:"date,omitempty"` } type CfsGdd struct { - ID string `bson:"_id,omitempty"` - Location Location `bson:"location,omitempty"` - MinTemps []float64 `bson:"min_temps,omitempty"` - MaxTemps []float64 `bson:"max_temps,omitempty"` - StartDate primitive.DateTime `bson:"start_date,omitempty"` + ID string `bson:"_id,omitempty"` + Location Location `bson:"location,omitempty"` + MinTemps []float64 `bson:"min_temps,omitempty"` + MaxTemps []float64 `bson:"max_temps,omitempty"` + VarMin []float64 `bson:"var_mins,omitempty"` + VarMax []float64 `bson:"var_maxs,omitempty"` + Date primitive.DateTime `bson:"date,omitempty"` } diff --git a/persistence/mongodb.go b/persistence/mongodb.go index faf0902cfecd6713ec168cf174ccc4afdda1daa4..45c5336061241cbc88c9a438ac497e60d524e1b3 100644 --- a/persistence/mongodb.go +++ b/persistence/mongodb.go @@ -169,3 +169,17 @@ func FindAnalogYear(location entities.Location) models.AnalogResponse { return results } + +func FindFreezingDates(location entities.Location) entities.FreezingDates { + coll := Conn.Database(viper.GetString("db.database")).Collection("freezing_dates") + + filter := buildLocationRequest(location, nil) + + var g entities.FreezingDates + err := coll.FindOne(Ctx, filter).Decode(&g) + if err != nil { + panic(errors.NO_DATA_FOUND) + } + + return g +} diff --git a/services/data_download_service.go b/services/data_download_service.go new file mode 100644 index 0000000000000000000000000000000000000000..723782382c341a647011cbf850053fc4517287bf --- /dev/null +++ b/services/data_download_service.go @@ -0,0 +1,181 @@ +package services + +import ( + "dawn-weather/errors" + "dawn-weather/models" + "dawn-weather/persistence" + "dawn-weather/persistence/entities" + "encoding/csv" + "fmt" + "io/ioutil" + "log" + "math" + "os" + "strconv" + "time" + + "github.com/google/uuid" +) + +func fillKeys(request models.CSVRequest) []string { + keys := []string{} + + if request.Analog { + keys = append(keys, "Date") + } + if request.Analog { + keys = append(keys, "Analog Year GDD") + } + // if request.Cfs { + // keys = append(keys, "CFS GDD") + // } + // if request.CfsLower { + // keys = append(keys, "CFS Lower Boundary GDD") + // } + // if request.CfsUpper { + // keys = append(keys, "CFS Upper Boundary GDD") + // } + // if request.Comparison { + // keys = append(keys, "Comparison Year GDD") + // } + // if request.FirstFreezing { + // keys = append(keys, "First Freezing Date Counts") + // } + // if request.Gefs { + // keys = append(keys, "GEFS GDD") + // } + // if request.LastFreezing { + // keys = append(keys, "Last Freezing Date Counts") + // } + // if request.Maximum { + // keys = append(keys, "Confidence Interval Upper Boundary") + // } + // if request.Minimum { + // keys = append(keys, "Confidence Interval Lower Boundary") + // } + if request.Normals { + keys = append(keys, "Thirty Year Normal GDD") + } + if request.Primary { + keys = append(keys, "Primary GDD ("+strconv.Itoa(request.Year)+")") + } + return keys +} + +func timeToString(timeValue time.Time) string { + day := timeValue.Day() + month := int(timeValue.Month()) + timeString := fmt.Sprintf("%02d", month) + "-" + fmt.Sprintf("%02d", day) + return timeString +} + +func fillDates() []time.Time { + dates := []time.Time{} + date := time.Date(1981, time.January, 1, 0, 0, 0, 0, time.UTC) + + for i := 0; i < 365; i++ { + dates = append(dates, date) + date = date.Add(time.Duration(24) * time.Hour) + } + + return dates +} + +func getPrimary(request models.CSVRequest, dates []time.Time) []float64 { + gddRequest := models.GddRequest{ + Year: request.Year, + Product: request.Product, + Latitude: request.Latitude, + Longitude: request.Longitude, + Accumulate: true, + } + + gddValues := GetGddValues(gddRequest).GddValues + for len(gddValues) < 365 { + gddValues = append(gddValues, math.NaN()) + } + return gddValues +} + +func getNormals(request models.CSVRequest, dates []time.Time) []float64 { + gddRequest := models.GddRequest{ + Year: request.Year, + Product: request.Product, + Latitude: request.Latitude, + Longitude: request.Longitude, + Accumulate: true, + } + + gddValues := GetNormalValues(gddRequest).GddValues + for len(gddValues) < 365 { + gddValues = append(gddValues, math.NaN()) + } + return gddValues +} + +func getAnalogYear(request models.CSVRequest, dates []time.Time) []float64 { + + location := entities.Location{ + Type: "Point", + Coordinates: []float64{request.Longitude, request.Latitude}, + } + + analogYear := persistence.FindAnalogYear(location).AnalogYear + + gddRequest := models.GddRequest{ + Year: analogYear, + Product: request.Product, + Latitude: request.Latitude, + Longitude: request.Longitude, + Accumulate: true, + } + + gddValues := GetNormalValues(gddRequest).GddValues + for len(gddValues) < 365 { + gddValues = append(gddValues, math.NaN()) + } + return gddValues +} + +func GetDataDownload(request models.CSVRequest) string { + + fileId := uuid.New() + + f, err := os.Create(fileId.String() + ".csv") + + if err != nil { + panic(errors.BAD_REQUEST) + } + + w := csv.NewWriter(f) + + keys := fillKeys(request) + dates := fillDates() + primary := getPrimary(request, dates) + normals := getNormals(request, dates) + analog := getAnalogYear(request, dates) + + records := [][]string{keys} + + for i := 1; i < 366; i++ { + records = append(records, + []string{timeToString(dates[i-1]), + fmt.Sprintf("%f", analog[i-1]), + fmt.Sprintf("%f", primary[i-1]), + fmt.Sprintf("%f", normals[i-1]), + }) + } + + for _, record := range records { + if err := w.Write(record); err != nil { + log.Fatalln("error writing record to file", err) + } + } + + w.Flush() + + fileText, err := ioutil.ReadFile(fileId.String() + ".csv") + f.Close() + os.Remove(fileId.String() + ".csv") + return string(fileText) +} diff --git a/services/freezing_date_service.go b/services/freezing_date_service.go new file mode 100644 index 0000000000000000000000000000000000000000..72add6366de082a08e676b44664f5ca655f099d0 --- /dev/null +++ b/services/freezing_date_service.go @@ -0,0 +1,87 @@ +package services + +import ( + "dawn-weather/errors" + "dawn-weather/models" + "dawn-weather/persistence" + "dawn-weather/persistence/entities" + "fmt" + "time" +) + +var may31st = 150 +var sept1st = 243 +var nov30th = 330 + +func isLeapYear(year int) bool { + if (year % 4) == 0 { + if (year % 100) == 0 { + if (year % 400) == 0 { + return true + } else { + return false + } + } else { + return true + } + } else { + return false + } +} + +func GetFreezingDate(request models.FreezingDateRequest) models.FreezingDateResponse { + + // product := enums.GetProductFromString(request.Product) + var freezingDates entities.FreezingDates + freezingDates = persistence.FindFreezingDates(request.BuildLocation()) + + firstDate := time.Date(1981, time.January, 1, 0, 0, 0, 0, time.UTC) + + tempIdx := -1 + + for i := 0; i < len(freezingDates.AllowedTemps); i++ { + if freezingDates.AllowedTemps[i] == request.FreezingTemp { + tempIdx = i + } + } + + if tempIdx == -1 { + panic(errors.BAD_REQUEST) + } + + firstDates := freezingDates.FirstDates[tempIdx] + lastDates := freezingDates.LastDates[tempIdx] + + firstDateCounts := make(map[string]int) + for _, row := range firstDates { + if row == -1 { + continue + } + date := firstDate.Add(time.Duration(24*row) * time.Hour) + day := date.Day() + month := int(date.Month()) + str := fmt.Sprintf("%02d", month) + "-" + fmt.Sprintf("%02d", day) + firstDateCounts[str]++ + } + + lastDateCounts := make(map[string]int) + for _, row := range lastDates { + if row == -1 { + continue + } + date := firstDate.Add(time.Duration(24*row) * time.Hour) + day := date.Day() + month := int(date.Month()) + str := fmt.Sprintf("%02d", month) + "-" + fmt.Sprintf("%02d", day) + lastDateCounts[str]++ + } + + response := models.FreezingDateResponse{ + FirstDateCounts: firstDateCounts, + LastDateCounts: lastDateCounts, + ClosestLatitude: freezingDates.Location.Coordinates[1], + ClosestLongitude: freezingDates.Location.Coordinates[0], + } + + return response +} diff --git a/services/nomads_service.go b/services/nomads_service.go index b480821b0ab889dcb4a1879fb3e9a9ae62464c1e..f7678277339c15c4f53432c732d76c421cca4a19 100644 --- a/services/nomads_service.go +++ b/services/nomads_service.go @@ -5,6 +5,7 @@ import ( "dawn-weather/models/enums" "dawn-weather/persistence" "dawn-weather/utils" + "math" ) func GetGefsGddValues(request models.GddRequest) models.GefsGddResponse { @@ -14,14 +15,31 @@ func GetGefsGddValues(request models.GddRequest) models.GefsGddResponse { var returnGdds models.GefsGddResponse var gdds []float64 + var lowerBound []float64 + var upperBound []float64 for i := 0; i < 10; i++ { temp := g[i] + variance := (temp.VarMin + temp.VarMax) + variance *= math.Pow(0.5, 2) + std := math.Pow(variance, 0.5) + value := utils.CalculateSingleGdd(temp.MinTemp, temp.MaxTemp, product) + + lowerBoundValue := (value - (std / (math.Pow(3, 0.5)) * 1.960)) + upperBoundValue := (value + (std / (math.Pow(3, 0.5)) * 1.960)) + + lowerBoundValue = utils.ClipMinFloat(lowerBoundValue, 0.0) + upperBoundValue = utils.ClipMinFloat(upperBoundValue, 0.0) + if request.Accumulate && i > 0 { value += gdds[len(gdds)-1] + lowerBoundValue += lowerBound[len(gdds)-1] + upperBoundValue += upperBound[len(gdds)-1] } gdds = append(gdds, value) + lowerBound = append(lowerBound, lowerBoundValue) + upperBound = append(upperBound, upperBoundValue) } returnGdds = models.GefsGddResponse{ @@ -29,6 +47,8 @@ func GetGefsGddValues(request models.GddRequest) models.GefsGddResponse { ClosestLatitude: location.Coordinates[1], ClosestLongitude: location.Coordinates[0], GddValues: gdds, + UpperBound: upperBound, + LowerBound: lowerBound, FirstDate: g[0].Date.Time().UTC(), LastDate: g[9].Date.Time().UTC(), } @@ -42,13 +62,31 @@ func GetCfsGddValues(request models.GddRequest) models.CfsGddResponse { var returnGdds models.CfsGddResponse var gdds []float64 + var lowerBound []float64 + var upperBound []float64 for i := 0; i < len(g.MaxTemps); i++ { + + variance := (g.VarMin[i] + g.VarMax[i]) + variance *= math.Pow(0.5, 2) + std := math.Pow(variance, 0.5) + value := utils.CalculateSingleGdd(g.MinTemps[i], g.MaxTemps[i], product) + + lowerBoundValue := (value - (std / (math.Pow(4, 0.5)) * 1.960)) + upperBoundValue := (value + (std / (math.Pow(4, 0.5)) * 1.960)) + + lowerBoundValue = utils.ClipMinFloat(lowerBoundValue, 0.0) + upperBoundValue = utils.ClipMinFloat(upperBoundValue, 0.0) + if request.Accumulate && i > 0 { value += gdds[len(gdds)-1] + lowerBoundValue += lowerBound[len(gdds)-1] + upperBoundValue += upperBound[len(gdds)-1] } gdds = append(gdds, value) + lowerBound = append(lowerBound, lowerBoundValue) + upperBound = append(upperBound, upperBoundValue) } returnGdds = models.CfsGddResponse{ @@ -56,7 +94,9 @@ func GetCfsGddValues(request models.GddRequest) models.CfsGddResponse { ClosestLatitude: location.Coordinates[1], ClosestLongitude: location.Coordinates[0], GddValues: gdds, - FirstDate: g.StartDate.Time().UTC(), + LowerBound: lowerBound, + UpperBound: upperBound, + FirstDate: g.Date.Time().UTC(), } return returnGdds }