Skip to content

Commit 4c8bb5a

Browse files
committed
add asset trends in statistics & analysis (#314)
1 parent d3abb27 commit 4c8bb5a

52 files changed

Lines changed: 1912 additions & 261 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

cmd/webserver.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,7 @@ func startWebServer(c *core.CliContext) error {
385385
apiV1Route.GET("/transactions/reconciliation_statements.json", bindApi(api.Transactions.TransactionReconciliationStatementHandler))
386386
apiV1Route.GET("/transactions/statistics.json", bindApi(api.Transactions.TransactionStatisticsHandler))
387387
apiV1Route.GET("/transactions/statistics/trends.json", bindApi(api.Transactions.TransactionStatisticsTrendsHandler))
388+
apiV1Route.GET("/transactions/statistics/asset_trends.json", bindApi(api.Transactions.TransactionStatisticsAssetTrendsHandler))
388389
apiV1Route.GET("/transactions/amounts.json", bindApi(api.Transactions.TransactionAmountsHandler))
389390
apiV1Route.GET("/transactions/get.json", bindApi(api.Transactions.TransactionGetHandler))
390391
apiV1Route.POST("/transactions/add.json", bindApi(api.Transactions.TransactionCreateHandler))

pkg/api/transactions.go

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,7 @@ func (a *TransactionsApi) TransactionReconciliationStatementHandler(c *core.WebC
340340
minTransactionTime = utils.GetMinTransactionTimeFromUnixTime(reconciliationStatementRequest.StartTime)
341341
}
342342

343-
transactionsWithAccountBalance, totalInflows, totalOutflows, openingBalance, closingBalance, err := a.transactions.GetAllTransactionsWithAccountBalanceByMaxTime(c, uid, pageCountForAccountStatement, maxTransactionTime, minTransactionTime, reconciliationStatementRequest.AccountId, account.Category)
343+
transactionsWithAccountBalance, totalInflows, totalOutflows, openingBalance, closingBalance, err := a.transactions.GetAllTransactionsInOneAccountWithAccountBalanceByMaxTime(c, uid, pageCountForAccountStatement, maxTransactionTime, minTransactionTime, reconciliationStatementRequest.AccountId, account.Category)
344344

345345
if err != nil {
346346
log.Errorf(c, "[transactions.TransactionReconciliationStatementHandler] failed to get transactions from \"%d\" to \"%d\" for user \"uid:%d\", because %s", reconciliationStatementRequest.StartTime, reconciliationStatementRequest.EndTime, uid, err.Error())
@@ -532,6 +532,71 @@ func (a *TransactionsApi) TransactionStatisticsTrendsHandler(c *core.WebContext)
532532
return statisticTrendsResp, nil
533533
}
534534

535+
// TransactionStatisticsAssetTrendsHandler returns transaction statistics asset trends of current user
536+
func (a *TransactionsApi) TransactionStatisticsAssetTrendsHandler(c *core.WebContext) (any, *errs.Error) {
537+
var statisticAssetTrendsReq models.TransactionStatisticAssetTrendsRequest
538+
err := c.ShouldBindQuery(&statisticAssetTrendsReq)
539+
540+
if err != nil {
541+
log.Warnf(c, "[transactions.TransactionStatisticsAssetTrendsHandler] parse request failed, because %s", err.Error())
542+
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
543+
}
544+
545+
utcOffset, err := c.GetClientTimezoneOffset()
546+
547+
if err != nil {
548+
log.Warnf(c, "[transactions.TransactionStatisticsAssetTrendsHandler] cannot get client timezone offset, because %s", err.Error())
549+
return nil, errs.ErrClientTimezoneOffsetInvalid
550+
}
551+
552+
uid := c.GetCurrentUid()
553+
554+
maxTransactionTime := int64(0)
555+
556+
if statisticAssetTrendsReq.EndTime > 0 {
557+
maxTransactionTime = utils.GetMaxTransactionTimeFromUnixTime(statisticAssetTrendsReq.EndTime)
558+
}
559+
560+
minTransactionTime := int64(0)
561+
562+
if statisticAssetTrendsReq.StartTime > 0 {
563+
minTransactionTime = utils.GetMinTransactionTimeFromUnixTime(statisticAssetTrendsReq.StartTime)
564+
}
565+
566+
accountDailyBalances, err := a.transactions.GetAllAccountsDailyOpeningAndClosingBalance(c, uid, maxTransactionTime, minTransactionTime, utcOffset)
567+
568+
if err != nil {
569+
log.Errorf(c, "[transactions.TransactionStatisticsAssetTrendsHandler] failed to get transactions from \"%d\" to \"%d\" for user \"uid:%d\", because %s", statisticAssetTrendsReq.StartTime, statisticAssetTrendsReq.EndTime, uid, err.Error())
570+
return nil, errs.Or(err, errs.ErrOperationFailed)
571+
}
572+
573+
statisticAssetTrendsResp := make(models.TransactionStatisticAssetTrendsResponseItemSlice, 0)
574+
575+
for yearMonthDay, dailyAccountBalances := range accountDailyBalances {
576+
dailyStatisticResp := &models.TransactionStatisticAssetTrendsResponseItem{
577+
Year: yearMonthDay / 10000,
578+
Month: (yearMonthDay % 10000) / 100,
579+
Day: yearMonthDay % 100,
580+
Items: make([]*models.TransactionStatisticAssetTrendsResponseDataItem, len(dailyAccountBalances)),
581+
}
582+
583+
for i := 0; i < len(dailyAccountBalances); i++ {
584+
accountBalance := dailyAccountBalances[i]
585+
dailyStatisticResp.Items[i] = &models.TransactionStatisticAssetTrendsResponseDataItem{
586+
AccountId: accountBalance.AccountId,
587+
AccountOpeningBalance: accountBalance.AccountOpeningBalance,
588+
AccountClosingBalance: accountBalance.AccountClosingBalance,
589+
}
590+
}
591+
592+
statisticAssetTrendsResp = append(statisticAssetTrendsResp, dailyStatisticResp)
593+
}
594+
595+
sort.Sort(statisticAssetTrendsResp)
596+
597+
return statisticAssetTrendsResp, nil
598+
}
599+
535600
// TransactionAmountsHandler returns transaction amounts of current user
536601
func (a *TransactionsApi) TransactionAmountsHandler(c *core.WebContext) (any, *errs.Error) {
537602
var transactionAmountsReq models.TransactionAmountsRequest

pkg/models/transaction.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,12 @@ type TransactionStatisticTrendsRequest struct {
275275
UseTransactionTimezone bool `form:"use_transaction_timezone"`
276276
}
277277

278+
// TransactionStatisticAssetTrendsRequest represents all parameters of transaction statistic asset trends request
279+
type TransactionStatisticAssetTrendsRequest struct {
280+
StartTime int64 `form:"start_time"`
281+
EndTime int64 `form:"end_time"`
282+
}
283+
278284
// TransactionAmountsRequest represents all parameters of transaction amounts request
279285
type TransactionAmountsRequest struct {
280286
Query string `form:"query"`
@@ -403,6 +409,21 @@ type TransactionStatisticTrendsResponseItem struct {
403409
Items []*TransactionStatisticResponseItem `json:"items"`
404410
}
405411

412+
// TransactionStatisticAssetTrendsResponseItem represents the data within each statistic interval
413+
type TransactionStatisticAssetTrendsResponseItem struct {
414+
Year int32 `json:"year"`
415+
Month int32 `json:"month"`
416+
Day int32 `json:"day"`
417+
Items []*TransactionStatisticAssetTrendsResponseDataItem `json:"items"`
418+
}
419+
420+
// TransactionStatisticAssetTrendsResponseDataItem represents an asset trends data item
421+
type TransactionStatisticAssetTrendsResponseDataItem struct {
422+
AccountId int64 `json:"accountId,string"`
423+
AccountOpeningBalance int64 `json:"accountOpeningBalance"`
424+
AccountClosingBalance int64 `json:"accountClosingBalance"`
425+
}
426+
406427
// TransactionAmountsResponseItem represents an item of transaction amounts
407428
type TransactionAmountsResponseItem struct {
408429
StartTime int64 `json:"startTime"`
@@ -600,6 +621,32 @@ func (s TransactionStatisticTrendsResponseItemSlice) Less(i, j int) bool {
600621
return s[i].Month < s[j].Month
601622
}
602623

624+
// TransactionStatisticAssetTrendsResponseItemSlice represents the slice data structure of TransactionStatisticAssetTrendsResponseItem
625+
type TransactionStatisticAssetTrendsResponseItemSlice []*TransactionStatisticAssetTrendsResponseItem
626+
627+
// Len returns the count of items
628+
func (s TransactionStatisticAssetTrendsResponseItemSlice) Len() int {
629+
return len(s)
630+
}
631+
632+
// Swap swaps two items
633+
func (s TransactionStatisticAssetTrendsResponseItemSlice) Swap(i, j int) {
634+
s[i], s[j] = s[j], s[i]
635+
}
636+
637+
// Less reports whether the first item is less than the second one
638+
func (s TransactionStatisticAssetTrendsResponseItemSlice) Less(i, j int) bool {
639+
if s[i].Year != s[j].Year {
640+
return s[i].Year < s[j].Year
641+
}
642+
643+
if s[i].Month != s[j].Month {
644+
return s[i].Month < s[j].Month
645+
}
646+
647+
return s[i].Day < s[j].Day
648+
}
649+
603650
// TransactionAmountsResponseItemAmountInfoSlice represents the slice data structure of TransactionAmountsResponseItemAmountInfo
604651
type TransactionAmountsResponseItemAmountInfoSlice []*TransactionAmountsResponseItemAmountInfo
605652

pkg/models/transaction_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,61 @@ func TestTransactionStatisticTrendsResponseItemSliceLess(t *testing.T) {
164164
assert.Equal(t, int32(9), transactionTrendsSlice[4].Month)
165165
}
166166

167+
func TestTransactionStatisticAssetTrendsResponseItemSliceLess(t *testing.T) {
168+
var transactionTrendsSlice TransactionStatisticAssetTrendsResponseItemSlice
169+
transactionTrendsSlice = append(transactionTrendsSlice, &TransactionStatisticAssetTrendsResponseItem{
170+
Year: 2024,
171+
Month: 9,
172+
Day: 1,
173+
})
174+
transactionTrendsSlice = append(transactionTrendsSlice, &TransactionStatisticAssetTrendsResponseItem{
175+
Year: 2024,
176+
Month: 9,
177+
Day: 2,
178+
})
179+
transactionTrendsSlice = append(transactionTrendsSlice, &TransactionStatisticAssetTrendsResponseItem{
180+
Year: 2024,
181+
Month: 10,
182+
Day: 1,
183+
})
184+
transactionTrendsSlice = append(transactionTrendsSlice, &TransactionStatisticAssetTrendsResponseItem{
185+
Year: 2022,
186+
Month: 10,
187+
Day: 1,
188+
})
189+
transactionTrendsSlice = append(transactionTrendsSlice, &TransactionStatisticAssetTrendsResponseItem{
190+
Year: 2023,
191+
Month: 1,
192+
Day: 1,
193+
})
194+
transactionTrendsSlice = append(transactionTrendsSlice, &TransactionStatisticAssetTrendsResponseItem{
195+
Year: 2024,
196+
Month: 2,
197+
Day: 2,
198+
})
199+
200+
sort.Sort(transactionTrendsSlice)
201+
202+
assert.Equal(t, int32(2022), transactionTrendsSlice[0].Year)
203+
assert.Equal(t, int32(10), transactionTrendsSlice[0].Month)
204+
assert.Equal(t, int32(1), transactionTrendsSlice[0].Day)
205+
assert.Equal(t, int32(2023), transactionTrendsSlice[1].Year)
206+
assert.Equal(t, int32(1), transactionTrendsSlice[1].Month)
207+
assert.Equal(t, int32(1), transactionTrendsSlice[1].Day)
208+
assert.Equal(t, int32(2024), transactionTrendsSlice[2].Year)
209+
assert.Equal(t, int32(2), transactionTrendsSlice[2].Month)
210+
assert.Equal(t, int32(2), transactionTrendsSlice[2].Day)
211+
assert.Equal(t, int32(2024), transactionTrendsSlice[3].Year)
212+
assert.Equal(t, int32(9), transactionTrendsSlice[3].Month)
213+
assert.Equal(t, int32(1), transactionTrendsSlice[3].Day)
214+
assert.Equal(t, int32(2024), transactionTrendsSlice[4].Year)
215+
assert.Equal(t, int32(9), transactionTrendsSlice[4].Month)
216+
assert.Equal(t, int32(2), transactionTrendsSlice[4].Day)
217+
assert.Equal(t, int32(2024), transactionTrendsSlice[5].Year)
218+
assert.Equal(t, int32(10), transactionTrendsSlice[5].Month)
219+
assert.Equal(t, int32(1), transactionTrendsSlice[5].Day)
220+
}
221+
167222
func TestTransactionAmountsResponseItemAmountInfoSliceLess(t *testing.T) {
168223
var amountInfoSlice TransactionAmountsResponseItemAmountInfoSlice
169224
amountInfoSlice = append(amountInfoSlice, &TransactionAmountsResponseItemAmountInfo{

pkg/models/user_app_cloud_setting.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ var ALL_ALLOWED_CLOUD_SYNC_APP_SETTING_KEY_TYPES = map[string]UserApplicationClo
4343
"statistics.defaultCategoricalChartDataRangeType": USER_APPLICATION_CLOUD_SETTING_TYPE_NUMBER,
4444
"statistics.defaultTrendChartType": USER_APPLICATION_CLOUD_SETTING_TYPE_NUMBER,
4545
"statistics.defaultTrendChartDataRangeType": USER_APPLICATION_CLOUD_SETTING_TYPE_NUMBER,
46+
"statistics.defaultAssetTrendsChartType": USER_APPLICATION_CLOUD_SETTING_TYPE_NUMBER,
47+
"statistics.defaultAssetTrendsChartDataRangeType": USER_APPLICATION_CLOUD_SETTING_TYPE_NUMBER,
4648
}
4749

4850
// UserApplicationCloudSetting represents user application cloud setting stored in database

pkg/services/transactions.go

Lines changed: 129 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,8 @@ func (s *TransactionService) GetAllSpecifiedTransactions(c core.Context, uid int
107107
return allTransactions, nil
108108
}
109109

110-
// GetAllTransactionsWithAccountBalanceByMaxTime returns account statement within time range
111-
func (s *TransactionService) GetAllTransactionsWithAccountBalanceByMaxTime(c core.Context, uid int64, pageCount int32, maxTransactionTime int64, minTransactionTime int64, accountId int64, accountCategory models.AccountCategory) ([]*models.TransactionWithAccountBalance, int64, int64, int64, int64, error) {
110+
// GetAllTransactionsInOneAccountWithAccountBalanceByMaxTime returns account statement within time range
111+
func (s *TransactionService) GetAllTransactionsInOneAccountWithAccountBalanceByMaxTime(c core.Context, uid int64, pageCount int32, maxTransactionTime int64, minTransactionTime int64, accountId int64, accountCategory models.AccountCategory) ([]*models.TransactionWithAccountBalance, int64, int64, int64, int64, error) {
112112
if maxTransactionTime <= 0 {
113113
maxTransactionTime = utils.GetMaxTransactionTimeFromUnixTime(time.Now().Unix())
114114
}
@@ -158,7 +158,7 @@ func (s *TransactionService) GetAllTransactionsWithAccountBalanceByMaxTime(c cor
158158
} else if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN {
159159
accumulatedBalance = accumulatedBalance + transaction.Amount
160160
} else {
161-
log.Errorf(c, "[transactions.GetAllTransactionsWithAccountBalanceByMaxTime] trasaction type (%d) is invalid (id:%d)", transaction.TransactionId, transaction.Type)
161+
log.Errorf(c, "[transactions.GetAllTransactionsInOneAccountWithAccountBalanceByMaxTime] trasaction type (%d) is invalid (id:%d)", transaction.TransactionId, transaction.Type)
162162
return nil, 0, 0, 0, 0, errs.ErrTransactionTypeInvalid
163163
}
164164

@@ -197,6 +197,132 @@ func (s *TransactionService) GetAllTransactionsWithAccountBalanceByMaxTime(c cor
197197
return allTransactionsAndAccountBalance, totalInflows, totalOutflows, openingBalance, accumulatedBalance, nil
198198
}
199199

200+
// GetAllAccountsDailyOpeningAndClosingBalance returns daily opening and closing balance of all accounts within time range
201+
func (s *TransactionService) GetAllAccountsDailyOpeningAndClosingBalance(c core.Context, uid int64, maxTransactionTime int64, minTransactionTime int64, utcOffset int16) (map[int32][]*models.TransactionWithAccountBalance, error) {
202+
if maxTransactionTime <= 0 {
203+
maxTransactionTime = utils.GetMaxTransactionTimeFromUnixTime(time.Now().Unix())
204+
}
205+
206+
clientLocation := time.FixedZone("Client Timezone", int(utcOffset)*60)
207+
var allTransactions []*models.Transaction
208+
209+
for maxTransactionTime > 0 {
210+
transactions, err := s.GetTransactionsByMaxTime(c, uid, maxTransactionTime, 0, 0, nil, nil, nil, false, models.TRANSACTION_TAG_FILTER_HAS_ANY, "", "", 1, pageCountForLoadTransactionAmounts, false, false)
211+
212+
if err != nil {
213+
return nil, err
214+
}
215+
216+
allTransactions = append(allTransactions, transactions...)
217+
218+
if len(transactions) < pageCountForLoadTransactionAmounts {
219+
maxTransactionTime = 0
220+
break
221+
}
222+
223+
maxTransactionTime = transactions[len(transactions)-1].TransactionTime - 1
224+
}
225+
226+
accountDailyLastBalances := make(map[string]*models.TransactionWithAccountBalance)
227+
accountDailyBalances := make(map[int32][]*models.TransactionWithAccountBalance)
228+
229+
if len(allTransactions) < 1 {
230+
return accountDailyBalances, nil
231+
}
232+
233+
accumulatedBalances := make(map[int64]int64)
234+
accumulatedBalancesBeforeStartTime := make(map[int64]int64)
235+
236+
for i := len(allTransactions) - 1; i >= 0; i-- {
237+
transaction := allTransactions[i]
238+
accumulatedBalance := accumulatedBalances[transaction.AccountId]
239+
lastAccumulatedBalance := accumulatedBalances[transaction.AccountId]
240+
241+
if transaction.Type == models.TRANSACTION_DB_TYPE_MODIFY_BALANCE {
242+
accumulatedBalance = accumulatedBalance + transaction.RelatedAccountAmount
243+
} else if transaction.Type == models.TRANSACTION_DB_TYPE_INCOME {
244+
accumulatedBalance = accumulatedBalance + transaction.Amount
245+
} else if transaction.Type == models.TRANSACTION_DB_TYPE_EXPENSE {
246+
accumulatedBalance = accumulatedBalance - transaction.Amount
247+
} else if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT {
248+
accumulatedBalance = accumulatedBalance - transaction.Amount
249+
} else if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN {
250+
accumulatedBalance = accumulatedBalance + transaction.Amount
251+
} else {
252+
log.Errorf(c, "[transactions.GetAllTransactionsWithAccountBalanceByMaxTime] trasaction type (%d) is invalid (id:%d)", transaction.TransactionId, transaction.Type)
253+
return nil, errs.ErrTransactionTypeInvalid
254+
}
255+
256+
accumulatedBalances[transaction.AccountId] = accumulatedBalance
257+
258+
if transaction.TransactionTime < minTransactionTime {
259+
accumulatedBalancesBeforeStartTime[transaction.AccountId] = accumulatedBalance
260+
continue
261+
}
262+
263+
yearMonthDay := utils.FormatUnixTimeToNumericYearMonthDay(utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime), clientLocation)
264+
groupKey := fmt.Sprintf("%d_%d", yearMonthDay, transaction.AccountId)
265+
dailyAccountBalance, exists := accountDailyLastBalances[groupKey]
266+
267+
if exists {
268+
dailyAccountBalance.AccountClosingBalance = accumulatedBalance
269+
} else {
270+
dailyAccountBalance = &models.TransactionWithAccountBalance{
271+
Transaction: &models.Transaction{
272+
AccountId: transaction.AccountId,
273+
},
274+
AccountOpeningBalance: lastAccumulatedBalance,
275+
AccountClosingBalance: accumulatedBalance,
276+
}
277+
accountDailyLastBalances[groupKey] = dailyAccountBalance
278+
}
279+
}
280+
281+
firstTransactionTime := allTransactions[len(allTransactions)-1].TransactionTime
282+
283+
if minTransactionTime > firstTransactionTime {
284+
firstTransactionTime = minTransactionTime
285+
}
286+
287+
firstYearMonthDay := utils.FormatUnixTimeToNumericYearMonthDay(utils.GetUnixTimeFromTransactionTime(firstTransactionTime), clientLocation)
288+
289+
// fill in the opening balance for accounts that do not have transactions on the first day
290+
for accountId, accumulatedBalance := range accumulatedBalancesBeforeStartTime {
291+
if accumulatedBalance == 0 {
292+
continue
293+
}
294+
295+
groupKey := fmt.Sprintf("%d_%d", firstYearMonthDay, accountId)
296+
297+
if _, exists := accountDailyLastBalances[groupKey]; exists {
298+
continue
299+
}
300+
301+
accountDailyLastBalances[groupKey] = &models.TransactionWithAccountBalance{
302+
Transaction: &models.Transaction{
303+
AccountId: accountId,
304+
},
305+
AccountOpeningBalance: accumulatedBalance,
306+
AccountClosingBalance: accumulatedBalance,
307+
}
308+
}
309+
310+
for groupKey, transactionWithAccountBalance := range accountDailyLastBalances {
311+
groupKeyParts := strings.Split(groupKey, "_")
312+
yearMonthDay, _ := utils.StringToInt32(groupKeyParts[0])
313+
dailyAccountBalances, exists := accountDailyBalances[yearMonthDay]
314+
315+
if !exists {
316+
dailyAccountBalances = make([]*models.TransactionWithAccountBalance, 0)
317+
}
318+
319+
dailyAccountBalances = append(dailyAccountBalances, transactionWithAccountBalance)
320+
accountDailyBalances[yearMonthDay] = dailyAccountBalances
321+
}
322+
323+
return accountDailyBalances, nil
324+
}
325+
200326
// GetTransactionsByMaxTime returns transactions before given time
201327
func (s *TransactionService) GetTransactionsByMaxTime(c core.Context, uid int64, maxTransactionTime int64, minTransactionTime int64, transactionType models.TransactionType, categoryIds []int64, accountIds []int64, tagIds []int64, noTags bool, tagFilterType models.TransactionTagFilterType, amountFilter string, keyword string, page int32, count int32, needOneMoreItem bool, noDuplicated bool) ([]*models.Transaction, error) {
202328
if uid <= 0 {

pkg/utils/datetimes.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,17 @@ func FormatUnixTimeToNumericYearMonth(unixTime int64, timezone *time.Location) i
155155
return int32(t.Year())*100 + int32(t.Month())
156156
}
157157

158+
// FormatUnixTimeToNumericYearMonthDay returns numeric year, month and day of specified unix time
159+
func FormatUnixTimeToNumericYearMonthDay(unixTime int64, timezone *time.Location) int32 {
160+
t := parseFromUnixTime(unixTime)
161+
162+
if timezone != nil {
163+
t = t.In(timezone)
164+
}
165+
166+
return int32(t.Year())*10000 + int32(t.Month())*100 + int32(t.Day())
167+
}
168+
158169
// FormatUnixTimeToNumericLocalDateTime returns numeric year, month, day, hour, minute and second of specified unix time
159170
func FormatUnixTimeToNumericLocalDateTime(unixTime int64, timezone *time.Location) int64 {
160171
t := parseFromUnixTime(unixTime)

0 commit comments

Comments
 (0)