Commit 2c237577d349dea55b20fab5275e82438839b5a6
1 parent
a7bf420dd5
Exists in
master
fixed int formating bug
Showing
3 changed files
with
26 additions
and
1 deletions
Show diff stats
date_util.go
1 | package webutility | 1 | package webutility |
2 | 2 | ||
3 | import ( | 3 | import ( |
4 | "fmt" | 4 | "fmt" |
5 | "strings" | 5 | "strings" |
6 | "time" | 6 | "time" |
7 | ) | 7 | ) |
8 | 8 | ||
9 | const ( | 9 | const ( |
10 | YYYYMMDD_sl = "2006/01/02" | 10 | YYYYMMDD_sl = "2006/01/02" |
11 | YYYYMMDD_ds = "2006-01-02" | 11 | YYYYMMDD_ds = "2006-01-02" |
12 | YYYYMMDD_dt = "2006.01.02." | 12 | YYYYMMDD_dt = "2006.01.02." |
13 | 13 | ||
14 | DDMMYYYY_sl = "02/01/2006" | 14 | DDMMYYYY_sl = "02/01/2006" |
15 | DDMMYYYY_ds = "02-01-2006" | 15 | DDMMYYYY_ds = "02-01-2006" |
16 | DDMMYYYY_dt = "02.01.2006." | 16 | DDMMYYYY_dt = "02.01.2006." |
17 | 17 | ||
18 | YYYYMMDD_HHMMSS_sl = "2006/01/02 15:04:05" | 18 | YYYYMMDD_HHMMSS_sl = "2006/01/02 15:04:05" |
19 | YYYYMMDD_HHMMSS_ds = "2006-01-02 15:04:05" | 19 | YYYYMMDD_HHMMSS_ds = "2006-01-02 15:04:05" |
20 | YYYYMMDD_HHMMSS_dt = "2006.01.02. 15:04:05" | 20 | YYYYMMDD_HHMMSS_dt = "2006.01.02. 15:04:05" |
21 | 21 | ||
22 | DDMMYYYY_HHMMSS_sl = "02/01/2006 15:04:05" | 22 | DDMMYYYY_HHMMSS_sl = "02/01/2006 15:04:05" |
23 | DDMMYYYY_HHMMSS_ds = "02-01-2006 15:04:05" | 23 | DDMMYYYY_HHMMSS_ds = "02-01-2006 15:04:05" |
24 | DDMMYYYY_HHMMSS_dt = "02.01.2006. 15:04:05" | 24 | DDMMYYYY_HHMMSS_dt = "02.01.2006. 15:04:05" |
25 | ) | 25 | ) |
26 | 26 | ||
27 | var ( | 27 | var ( |
28 | regularYear = [12]int64{31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31} | 28 | regularYear = [12]int64{31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31} |
29 | leapYear = [12]int64{31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31} | 29 | leapYear = [12]int64{31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31} |
30 | ) | 30 | ) |
31 | 31 | ||
32 | func Systime() int64 { | 32 | func Systime() int64 { |
33 | return time.Now().Unix() | 33 | return time.Now().Unix() |
34 | } | 34 | } |
35 | 35 | ||
36 | func DateToEpoch(date, format string) int64 { | 36 | func DateToEpoch(date, format string) int64 { |
37 | t, err := time.Parse(format, date) | 37 | t, err := time.Parse(format, date) |
38 | if err != nil { | 38 | if err != nil { |
39 | fmt.Println(err.Error()) | 39 | fmt.Println(err.Error()) |
40 | return 0 | 40 | return 0 |
41 | } | 41 | } |
42 | return t.Unix() | 42 | return t.Unix() |
43 | } | 43 | } |
44 | 44 | ||
45 | func EpochToDate(e int64, format string) string { | 45 | func EpochToDate(e int64, format string) string { |
46 | return time.Unix(e, 0).Format(format) | 46 | return time.Unix(e, 0).Format(format) |
47 | } | 47 | } |
48 | 48 | ||
49 | func EpochToDayMonthYear(timestamp int64) (d, m, y int64) { | 49 | func EpochToDayMonthYear(timestamp int64) (d, m, y int64) { |
50 | datestring := EpochToDate(timestamp, DDMMYYYY_sl) | 50 | datestring := EpochToDate(timestamp, DDMMYYYY_sl) |
51 | parts := strings.Split(datestring, "/") | 51 | parts := strings.Split(datestring, "/") |
52 | d = StringToInt64(parts[0]) | 52 | d = StringToInt64(parts[0]) |
53 | m = StringToInt64(parts[1]) | 53 | m = StringToInt64(parts[1]) |
54 | y = StringToInt64(parts[2]) | 54 | y = StringToInt64(parts[2]) |
55 | return d, m, y | 55 | return d, m, y |
56 | } | 56 | } |
57 | 57 | ||
58 | func DaysInMonth(year, month int64) int64 { | 58 | func DaysInMonth(year, month int64) int64 { |
59 | if month < 1 || month > 12 { | 59 | if month < 1 || month > 12 { |
60 | return 0 | 60 | return 0 |
61 | } | 61 | } |
62 | if IsLeapYear(year) { | 62 | if IsLeapYear(year) { |
63 | return leapYear[month-1] | 63 | return leapYear[month-1] |
64 | } | 64 | } |
65 | return regularYear[month-1] | 65 | return regularYear[month-1] |
66 | } | 66 | } |
67 | 67 | ||
68 | func IsLeapYear(year int64) bool { | 68 | func IsLeapYear(year int64) bool { |
69 | return year%4 == 0 && !((year%100 == 0) && (year%400 != 0)) | 69 | return year%4 == 0 && !((year%100 == 0) && (year%400 != 0)) |
70 | } | 70 | } |
71 | |||
72 | // FirstDayOfNextMonthEpoch ... | ||
73 | func FirstDayOfNextMonthEpoch(e int64) int64 { | ||
74 | d, m, y := EpochToDayMonthYear(e) | ||
75 | m++ | ||
76 | if m > 12 { | ||
77 | m = 1 | ||
78 | y++ | ||
79 | } | ||
80 | d = 1 | ||
81 | |||
82 | date := fmt.Sprintf("%02d/%02d/%d", d, m, y) | ||
83 | |||
84 | return DateToEpoch(date, DDMMYYYY_sl) | ||
85 | } | ||
71 | 86 |
document/document.go
1 | package document | 1 | package document |
2 | 2 | ||
3 | import ( | 3 | import ( |
4 | "errors" | 4 | "errors" |
5 | "fmt" | 5 | "fmt" |
6 | "io" | 6 | "io" |
7 | "mime" | 7 | "mime" |
8 | "net/http" | 8 | "net/http" |
9 | "os" | 9 | "os" |
10 | "strings" | 10 | "strings" |
11 | "time" | 11 | "time" |
12 | 12 | ||
13 | web "git.to-net.rs/marko.tikvic/webutility" | 13 | web "git.to-net.rs/marko.tikvic/webutility" |
14 | ) | 14 | ) |
15 | 15 | ||
16 | // Document ... | 16 | // Document ... |
17 | type Document struct { | 17 | type Document struct { |
18 | ID int64 `json:"id"` | 18 | ID int64 `json:"id"` |
19 | FileName string `json:"fileName"` | 19 | FileName string `json:"fileName"` |
20 | Extension string `json:"extension"` | 20 | Extension string `json:"extension"` |
21 | ContentType string `json:"contentType"` | 21 | ContentType string `json:"contentType"` |
22 | Size int64 `json:"fileSize"` | 22 | Size int64 `json:"fileSize"` |
23 | UploadedBy string `json:"uploadedBy"` | 23 | UploadedBy string `json:"uploadedBy"` |
24 | LastModifiedBy string `json:"lastModifiedBy"` | 24 | LastModifiedBy string `json:"lastModifiedBy"` |
25 | TimeUploaded int64 `json:"timeUploaded"` | 25 | TimeUploaded int64 `json:"timeUploaded"` |
26 | TimeLastModified int64 `json:"timeLastModified"` | 26 | TimeLastModified int64 `json:"timeLastModified"` |
27 | RoleAccessLevel int64 `json:"accessLevel"` | 27 | RoleAccessLevel int64 `json:"accessLevel"` |
28 | Description string `json:"description"` | 28 | Description string `json:"description"` |
29 | Download *DownloadLink `json:"download"` | 29 | Download *DownloadLink `json:"download"` |
30 | Path string `json:"-"` | 30 | Path string `json:"-"` |
31 | directory string | 31 | directory string |
32 | data []byte | 32 | data []byte |
33 | } | 33 | } |
34 | 34 | ||
35 | // OpenFileAsDocument ... | 35 | // OpenFileAsDocument ... |
36 | func OpenFileAsDocument(path string) (*Document, error) { | 36 | func OpenFileAsDocument(path string) (*Document, error) { |
37 | d := &Document{Path: path} | 37 | d := &Document{Path: path} |
38 | 38 | ||
39 | f, err := os.Open(d.Path) | 39 | f, err := os.Open(d.Path) |
40 | if err != nil { | 40 | if err != nil { |
41 | return nil, err | 41 | return nil, err |
42 | } | 42 | } |
43 | defer f.Close() | 43 | defer f.Close() |
44 | 44 | ||
45 | stats, err := f.Stat() | 45 | stats, err := f.Stat() |
46 | if err != nil { | 46 | if err != nil { |
47 | return nil, err | 47 | return nil, err |
48 | } | 48 | } |
49 | 49 | ||
50 | d.FileName = stats.Name() | 50 | d.FileName = stats.Name() |
51 | d.Size = stats.Size() | 51 | d.Size = stats.Size() |
52 | d.Extension = FileExtension(d.FileName) | 52 | d.Extension = FileExtension(d.FileName) |
53 | 53 | ||
54 | d.data = make([]byte, d.Size) | 54 | d.data = make([]byte, d.Size) |
55 | if _, err = f.Read(d.data); err != nil { | 55 | if _, err = f.Read(d.data); err != nil { |
56 | return nil, err | 56 | return nil, err |
57 | } | 57 | } |
58 | 58 | ||
59 | return d, err | 59 | return d, err |
60 | } | 60 | } |
61 | 61 | ||
62 | // DownloadLink ... | 62 | // DownloadLink ... |
63 | type DownloadLink struct { | 63 | type DownloadLink struct { |
64 | Method string `json:"method"` | 64 | Method string `json:"method"` |
65 | URL string `json:"url"` | 65 | URL string `json:"url"` |
66 | } | 66 | } |
67 | 67 | ||
68 | // SetDownloadInfo ... | 68 | // SetDownloadInfo ... |
69 | func (d *Document) SetDownloadInfo(method, url string) { | 69 | func (d *Document) SetDownloadInfo(method, url string) { |
70 | d.Download = &DownloadLink{ | 70 | d.Download = &DownloadLink{ |
71 | Method: method, | 71 | Method: method, |
72 | URL: url, | 72 | URL: url, |
73 | } | 73 | } |
74 | } | 74 | } |
75 | 75 | ||
76 | // ServeDocument writes d's buffer to w and sets appropriate headers according to d's content type | 76 | // ServeDocument writes d's buffer to w and sets appropriate headers according to d's content type |
77 | // and downloadPrompt. | 77 | // and downloadPrompt. |
78 | func ServeDocument(w http.ResponseWriter, d *Document, downloadPrompt bool) error { | 78 | func ServeDocument(w http.ResponseWriter, d *Document, downloadPrompt bool) error { |
79 | f, err := os.Open(d.Path) | 79 | f, err := os.Open(d.Path) |
80 | if err != nil { | 80 | if err != nil { |
81 | return err | 81 | return err |
82 | } | 82 | } |
83 | defer f.Close() | 83 | defer f.Close() |
84 | 84 | ||
85 | web.SetContentType(w, mime.TypeByExtension(d.Extension)) | 85 | web.SetContentType(w, mime.TypeByExtension(d.Extension)) |
86 | web.SetResponseStatus(w, http.StatusOK) | 86 | web.SetResponseStatus(w, http.StatusOK) |
87 | if downloadPrompt { | 87 | if downloadPrompt { |
88 | w.Header().Set("Content-Disposition", "attachment; filename="+d.FileName) | 88 | w.Header().Set("Content-Disposition", "attachment; filename="+d.FileName) |
89 | } | 89 | } |
90 | 90 | ||
91 | buf := make([]byte, d.Size) | 91 | buf := make([]byte, d.Size) |
92 | if _, err := f.Read(buf); err != nil { | 92 | if _, err := f.Read(buf); err != nil { |
93 | return err | 93 | return err |
94 | } | 94 | } |
95 | 95 | ||
96 | w.Header().Set("Content-Length", fmt.Sprintf("%d", d.Size)) | 96 | w.Header().Set("Content-Length", fmt.Sprintf("%d", d.Size)) |
97 | web.WriteResponse(w, buf) | 97 | web.WriteResponse(w, buf) |
98 | 98 | ||
99 | return nil | 99 | return nil |
100 | } | 100 | } |
101 | 101 | ||
102 | // FileExists ... | 102 | // FileExists ... |
103 | func FileExists(path string) bool { | 103 | func FileExists(path string) bool { |
104 | temp, err := os.Open(path) | 104 | temp, err := os.Open(path) |
105 | defer temp.Close() | 105 | defer temp.Close() |
106 | 106 | ||
107 | if err != nil { | 107 | if err != nil { |
108 | return false | 108 | return false |
109 | } | 109 | } |
110 | 110 | ||
111 | return true | 111 | return true |
112 | } | 112 | } |
113 | 113 | ||
114 | // ParseDocument ... | 114 | // ParseDocument ... |
115 | func ParseDocument(req *http.Request) (doc *Document, err error) { | 115 | func ParseDocument(req *http.Request) (doc *Document, err error) { |
116 | req.ParseMultipartForm(32 << 20) | 116 | req.ParseMultipartForm(32 << 20) |
117 | file, fheader, err := req.FormFile("document") | 117 | file, fheader, err := req.FormFile("document") |
118 | if err != nil { | 118 | if err != nil { |
119 | return doc, err | 119 | return doc, err |
120 | } | 120 | } |
121 | 121 | ||
122 | claims, _ := web.GetTokenClaims(req) | 122 | claims, _ := web.GetTokenClaims(req) |
123 | owner := claims.Username | 123 | owner := claims.Username |
124 | 124 | ||
125 | fname := fheader.Filename | 125 | fname := fheader.Filename |
126 | 126 | ||
127 | fsize := fheader.Size | 127 | fsize := fheader.Size |
128 | ftype := fmt.Sprintf("%v", fheader.Header["Content-Type"][0]) | 128 | ftype := fmt.Sprintf("%v", fheader.Header["Content-Type"][0]) |
129 | 129 | ||
130 | fextn := FileExtension(fname) | 130 | fextn := FileExtension(fname) |
131 | if fextn == "" { | 131 | if fextn == "" { |
132 | return doc, errors.New("invalid extension") | 132 | return doc, errors.New("invalid extension") |
133 | } | 133 | } |
134 | 134 | ||
135 | doc = new(Document) | 135 | doc = new(Document) |
136 | 136 | ||
137 | doc.FileName = fname | 137 | doc.FileName = fname |
138 | doc.Size = fsize | 138 | doc.Size = fsize |
139 | doc.ContentType = ftype | 139 | doc.ContentType = ftype |
140 | doc.Extension = "." + fextn | 140 | doc.Extension = "." + fextn |
141 | 141 | ||
142 | t := time.Now().Unix() | 142 | t := time.Now().Unix() |
143 | doc.TimeUploaded = t | 143 | doc.TimeUploaded = t |
144 | doc.TimeLastModified = t | 144 | doc.TimeLastModified = t |
145 | 145 | ||
146 | doc.UploadedBy = owner | 146 | doc.UploadedBy = owner |
147 | doc.LastModifiedBy = owner | 147 | doc.LastModifiedBy = owner |
148 | doc.RoleAccessLevel = 0 | 148 | doc.RoleAccessLevel = 0 |
149 | 149 | ||
150 | doc.data = make([]byte, doc.Size) | 150 | doc.data = make([]byte, doc.Size) |
151 | if _, err = io.ReadFull(file, doc.data); err != nil { | 151 | if _, err = io.ReadFull(file, doc.data); err != nil { |
152 | return doc, err | 152 | return doc, err |
153 | } | 153 | } |
154 | 154 | ||
155 | return doc, nil | 155 | return doc, nil |
156 | } | 156 | } |
157 | 157 | ||
158 | // DirectoryFromPath ... | 158 | // DirectoryFromPath ... |
159 | func DirectoryFromPath(path string) (dir string) { | 159 | func DirectoryFromPath(path string) (dir string) { |
160 | parts := strings.Split(path, "/") | 160 | parts := strings.Split(path, "/") |
161 | if len(parts) == 1 { | 161 | if len(parts) == 1 { |
162 | return "" | 162 | return "" |
163 | } | 163 | } |
164 | 164 | ||
165 | dir = parts[0] | 165 | dir = parts[0] |
166 | for _, p := range parts[1 : len(parts)-1] { | 166 | for _, p := range parts[1 : len(parts)-1] { |
167 | dir += "/" + p | 167 | dir += "/" + p |
168 | } | 168 | } |
169 | 169 | ||
170 | return dir | 170 | return dir |
171 | } | 171 | } |
172 | 172 | ||
173 | // SaveToFile ... | 173 | // SaveToFile ... |
174 | func (d *Document) SaveToFile(path string) (f *os.File, err error) { | 174 | func (d *Document) SaveToFile(path string) (f *os.File, err error) { |
175 | d.Path = path | 175 | d.Path = path |
176 | 176 | ||
177 | if FileExists(path) { | 177 | if FileExists(path) { |
178 | err = fmt.Errorf("file %s alredy exists", path) | 178 | err = fmt.Errorf("file %s alredy exists", path) |
179 | return nil, err | 179 | return nil, err |
180 | } | 180 | } |
181 | 181 | ||
182 | if parentDir := DirectoryFromPath(path); parentDir != "" { | 182 | if parentDir := DirectoryFromPath(path); parentDir != "" { |
183 | if err = os.MkdirAll(parentDir, os.ModePerm); err != nil { | 183 | if err = os.MkdirAll(parentDir, os.ModePerm); err != nil { |
184 | if !os.IsExist(err) { | 184 | if !os.IsExist(err) { |
185 | return nil, err | 185 | return nil, err |
186 | } | 186 | } |
187 | } | 187 | } |
188 | } | 188 | } |
189 | 189 | ||
190 | if f, err = os.Create(path); err != nil { | 190 | if f, err = os.Create(path); err != nil { |
191 | return nil, err | 191 | return nil, err |
192 | } | 192 | } |
193 | 193 | ||
194 | if _, err = f.Write(d.data); err != nil { | 194 | if _, err = f.Write(d.data); err != nil { |
195 | f.Close() | 195 | f.Close() |
196 | d.DeleteFile() | 196 | d.DeleteFile() |
197 | return nil, err | 197 | return nil, err |
198 | } | 198 | } |
199 | f.Close() | 199 | f.Close() |
200 | 200 | ||
201 | return f, nil | 201 | return f, nil |
202 | } | 202 | } |
203 | 203 | ||
204 | func DeleteFile(path string) error { | 204 | func DeleteFile(path string) error { |
205 | return os.Remove(path) | 205 | return os.Remove(path) |
206 | } | 206 | } |
207 | 207 | ||
208 | func DeleteDocuments(docs []*Document) error { | ||
209 | for _, d := range docs { | ||
210 | if err := d.DeleteFile(); err != nil { | ||
211 | return err | ||
212 | } | ||
213 | } | ||
214 | return nil | ||
215 | } | ||
216 | |||
208 | // DeleteFile ... | 217 | // DeleteFile ... |
209 | func (d *Document) DeleteFile() error { | 218 | func (d *Document) DeleteFile() error { |
210 | return os.Remove(d.Path) | 219 | return os.Remove(d.Path) |
211 | } | 220 | } |
212 | 221 | ||
213 | func FileExtension(path string) string { | 222 | func FileExtension(path string) string { |
214 | parts := strings.Split(path, ".") // because name can contain dots | 223 | parts := strings.Split(path, ".") // because name can contain dots |
215 | if len(parts) < 2 { | 224 | if len(parts) < 2 { |
216 | return "" | 225 | return "" |
217 | } | 226 | } |
218 | return "." + parts[len(parts)-1] | 227 | return "." + parts[len(parts)-1] |
219 | } | 228 | } |
220 | 229 |
int_util.go
1 | package webutility | 1 | package webutility |
2 | 2 | ||
3 | import ( | 3 | import ( |
4 | "fmt" | 4 | "fmt" |
5 | ) | 5 | ) |
6 | 6 | ||
7 | // ClampInt64 ... | 7 | // ClampInt64 ... |
8 | func ClampInt64(v, min, max int64) int64 { | 8 | func ClampInt64(v, min, max int64) int64 { |
9 | if v < min { | 9 | if v < min { |
10 | return min | 10 | return min |
11 | } else if v > max { | 11 | } else if v > max { |
12 | return max | 12 | return max |
13 | } | 13 | } |
14 | 14 | ||
15 | return v | 15 | return v |
16 | } | 16 | } |
17 | 17 | ||
18 | // InRangeInt64 ... | 18 | // InRangeInt64 ... |
19 | func InRangeInt64(v, min, max int64) bool { | 19 | func InRangeInt64(v, min, max int64) bool { |
20 | return (v >= min && v <= max) | 20 | return (v >= min && v <= max) |
21 | } | 21 | } |
22 | 22 | ||
23 | // Int64ToString ... | 23 | // Int64ToString ... |
24 | func Int64ToString(i int64) string { | 24 | func Int64ToString(i int64) string { |
25 | return fmt.Sprintf("%d", i) | 25 | return fmt.Sprintf("%d", i) |
26 | } | 26 | } |
27 | 27 | ||
28 | // Int64PtrToString ... | 28 | // Int64PtrToString ... |
29 | func Int64PtrToString(i *int64) string { | 29 | func Int64PtrToString(i *int64) string { |
30 | if i == nil { | 30 | if i == nil { |
31 | return "" | 31 | return "" |
32 | } | 32 | } |
33 | return fmt.Sprintf("%d", *i) | 33 | return fmt.Sprintf("%d", *i) |
34 | } | 34 | } |
35 | 35 | ||
36 | // BoolToInt64 ... | 36 | // BoolToInt64 ... |
37 | func BoolToInt64(b bool) int64 { | 37 | func BoolToInt64(b bool) int64 { |
38 | if b { | 38 | if b { |
39 | return 1 | 39 | return 1 |
40 | } | 40 | } |
41 | return 0 | 41 | return 0 |
42 | } | 42 | } |
43 | 43 | ||
44 | // Int64ToBool ... | 44 | // Int64ToBool ... |
45 | func Int64ToBool(i int64) bool { | 45 | func Int64ToBool(i int64) bool { |
46 | return i != 0 | 46 | return i != 0 |
47 | } | 47 | } |
48 | 48 | ||
49 | func MaxInt(vars ...int) (max int) { | 49 | func MaxInt(vars ...int) (max int) { |
50 | max = vars[0] | 50 | max = vars[0] |
51 | for _, v := range vars { | 51 | for _, v := range vars { |
52 | if v > max { | 52 | if v > max { |
53 | max = v | 53 | max = v |
54 | } | 54 | } |
55 | } | 55 | } |
56 | return max | 56 | return max |
57 | } | 57 | } |
58 | 58 | ||
59 | func MinInt64(vars ...int64) (min int64) { | 59 | func MinInt64(vars ...int64) (min int64) { |
60 | min = vars[0] | 60 | min = vars[0] |
61 | for _, v := range vars { | 61 | for _, v := range vars { |
62 | if v < min { | 62 | if v < min { |
63 | min = v | 63 | min = v |
64 | } | 64 | } |
65 | } | 65 | } |
66 | return min | 66 | return min |
67 | } | 67 | } |
68 | 68 | ||
69 | func FormatInt64Number(i int64) string { | 69 | func FormatInt64Number(i int64) string { |
70 | res := "" | 70 | res := "" |
71 | 71 | ||
72 | for i >= 1000 { | 72 | for i >= 1000 { |
73 | rem := i % 1000 | 73 | rem := i % 1000 |
74 | i = i / 1000 | 74 | i = i / 1000 |
75 | res = res + fmt.Sprintf(".%03d", rem) | 75 | //res = res + fmt.Sprintf(".%03d", rem) |
76 | res = fmt.Sprintf(".%03d", rem) + res | ||
76 | } | 77 | } |
77 | res = fmt.Sprintf("%d", i) + res | 78 | res = fmt.Sprintf("%d", i) + res |
78 | return res | 79 | return res |
79 | } | 80 | } |
80 | 81 |