initial commit
This commit is contained in:
commit
d5385fbb7f
|
@ -0,0 +1,366 @@
|
|||
# Created by https://www.gitignore.io/api/intellij,go,linux,osx,windows,node,python,executable,jetbrains+all,visualstudiocode,compressedarchive,git
|
||||
# Edit at https://www.gitignore.io/?templates=intellij,go,linux,osx,windows,node,python,executable,jetbrains+all,visualstudiocode,compressedarchive,git
|
||||
|
||||
### CompressedArchive ###
|
||||
|
||||
### Mostly from https://en.wikipedia.org/wiki/List_of_archive_formats
|
||||
|
||||
## Archiving and compression
|
||||
# Open source file format. Used by 7-Zip.
|
||||
*.7z
|
||||
# Mac OS X, restoration on different platforms is possible although not immediate Yes Based on 7z. Preserves Spotlight metadata, resource forks, owner/group information, dates and other data which would be otherwise lost with compression.
|
||||
*.s7z
|
||||
# Old archive versions only Proprietary format
|
||||
*.ace
|
||||
# A format that compresses and doubly encrypt the data (AES256 and CAS256) avoiding brute force attacks, also hide files in an AFA file. It has two ways to safeguard data integrity and subsequent repair of the file if has an error (repair with AstroA2P (online) or Astrotite (offline)).
|
||||
*.afa
|
||||
# A mainly Korean format designed for very large archives.
|
||||
*.alz
|
||||
# Android application package (variant of JAR file format).
|
||||
*.apk
|
||||
# ??
|
||||
*.arc
|
||||
# Originally DOS, now multiple
|
||||
*.arj
|
||||
# Open archive format, used by B1 Free Archiver (http://dev.b1.org/standard/archive-format.html)
|
||||
*.b1
|
||||
# Binary Archive with external header
|
||||
*.ba
|
||||
# Proprietary format from the ZipTV Compression Components
|
||||
*.bh
|
||||
# The Microsoft Windows native archive format, which is also used by many commercial installers such as InstallShield and WISE.
|
||||
*.cab
|
||||
# Originally DOS, now DOS and Windows Created by Yaakov Gringeler; released last in 2003 (Compressia 1.0.0.1 beta), now apparently defunct. Free trial of 30 days lets user create and extract archives; after that it is possible to extract, but not to create.
|
||||
*.car
|
||||
# Open source file format.
|
||||
*.cfs
|
||||
# Compact Pro archive, a common archiver used on Mac platforms until about Mac OS 7.5.x. Competed with StuffIt; now obsolete.
|
||||
*.cpt
|
||||
# Windows, Unix-like, Mac OS X Open source file format. Files are compressed individually with either gzip, bzip2 or lzo.
|
||||
*.dar
|
||||
# DiskDoubler Mac OS obsolete
|
||||
*.dd
|
||||
# ??
|
||||
*.dgc
|
||||
# Apple Disk Image upports "Internet-enabled" disk images, which, once downloaded, are automatically decompressed, mounted, have the contents extracted, and thrown away. Currently, Safari is the only browser that supports this form of extraction; however, the images can be manually extracted as well. This format can also be password-protected or encrypted with 128-bit or 256-bit AES encryption.
|
||||
*.dmg
|
||||
# Enterprise Java Archive archive
|
||||
*.ear
|
||||
# ETSoft compressed archive
|
||||
*.egg
|
||||
# The predecessor of DGCA.
|
||||
*.gca
|
||||
# Originally DOS Yes, but may be covered by patents DOS era format; uses arithmetic/Markov coding
|
||||
*.ha
|
||||
# MS Windows HKI
|
||||
*.hki
|
||||
# Produced by ICEOWS program. Excels at text file compression.
|
||||
*.ice
|
||||
# Java archive, compatible with ZIP files
|
||||
*.jar
|
||||
# Open sourced archiver with compression using the PAQ family of algorithms and optional encryption.
|
||||
*.kgb
|
||||
# Originally DOS, now multiple Multiple Yes The standard format on Amiga.
|
||||
*.lzh
|
||||
*.lha
|
||||
# Archiver originally used on The Amiga. Now copied by Microsoft to use in their .cab and .chm files.
|
||||
*.lzx
|
||||
# file format from NoGate Consultings, a rival from ARC-Compressor.
|
||||
*.pak
|
||||
# A disk image archive format that supports several compression methods as well as splitting the archive into smaller pieces.
|
||||
*.partimg
|
||||
# An experimental open source packager (http://mattmahoney.net/dc)
|
||||
*.paq*
|
||||
# Open source archiver supporting authenticated encryption, volume spanning, customizable object level and volume level integrity checks (form CRCs to SHA-512 and Whirlpool hashes), fast deflate based compression
|
||||
*.pea
|
||||
# The format from the PIM - a freeware compression tool by Ilia Muraviev. It uses an LZP-based compression algorithm with set of filters for executable, image and audio files.
|
||||
*.pim
|
||||
# PackIt Mac OS obsolete
|
||||
*.pit
|
||||
# Used for data in games written using the Quadruple D library for Delphi. Uses byte pair compression.
|
||||
*.qda
|
||||
# A proprietary archive format, second in popularity to .zip files.
|
||||
*.rar
|
||||
# The format from a commercial archiving package. Odd among commercial packages in that they focus on incorporating experimental algorithms with the highest possible compression (at the expense of speed and memory), such as PAQ, PPMD and PPMZ (PPMD with unlimited-length strings), as well as a proprietary algorithms.
|
||||
*.rk
|
||||
# Self Dissolving ARChive Commodore 64, Commodore 128 Commodore 64, Commodore 128 Yes SDAs refer to Self Dissolving ARC files, and are based on the Commodore 64 and Commodore 128 versions of ARC, originally written by Chris Smeets. While the files share the same extension, they are not compatible between platforms. That is, an SDA created on a Commodore 64 but run on a Commodore 128 in Commodore 128 mode will crash the machine, and vice versa. The intended successor to SDA is SFX.
|
||||
*.sda
|
||||
# A pre-Mac OS X Self-Extracting Archive format. StuffIt, Compact Pro, Disk Doubler and others could create .sea files, though the StuffIt versions were the most common.
|
||||
*.sea
|
||||
# Scifer Archive with internal header
|
||||
*.sen
|
||||
# Commodore 64, Commodore 128 SFX is a Self Extracting Archive which uses the LHArc compression algorithm. It was originally developed by Chris Smeets on the Commodore platform, and runs primarily using the CS-DOS extension for the Commodore 128. Unlike its predecessor SDA, SFX files will run on both the Commodore 64 and Commodore 128 regardless of which machine they were created on.
|
||||
*.sfx
|
||||
# An archive format designed for the Apple II series of computers. The canonical implementation is ShrinkIt, which can operate on disk images as well as files. Preferred compression algorithm is a combination of RLE and 12-bit LZW. Archives can be manipulated with the command-line NuLib tool, or the Windows-based CiderPress.
|
||||
*.shk
|
||||
# A compression format common on Apple Macintosh computers. The free StuffIt Expander is available for Windows and OS X.
|
||||
*.sit
|
||||
# The replacement for the .sit format that supports more compression methods, UNIX file permissions, long file names, very large files, more encryption options, data specific compressors (JPEG, Zip, PDF, 24-bit image, MP3). The free StuffIt Expander is available for Windows and OS X.
|
||||
*.sitx
|
||||
# A royalty-free compressing format
|
||||
*.sqx
|
||||
# The "tarball" format combines tar archives with a file-based compression scheme (usually gzip). Commonly used for source and binary distribution on Unix-like platforms, widely available elsewhere.
|
||||
*.tar.gz
|
||||
*.tgz
|
||||
*.tar.Z
|
||||
*.tar.bz2
|
||||
*.tbz2
|
||||
*.tar.lzma
|
||||
*.tlz
|
||||
# UltraCompressor 2.3 was developed to act as an alternative to the then popular PKZIP application. The main feature of the application is its ability to create large archives. This means that compressed archives with the UC2 file extension can hold almost 1 million files.
|
||||
*.uc
|
||||
*.uc0
|
||||
*.uc2
|
||||
*.ucn
|
||||
*.ur2
|
||||
*.ue2
|
||||
# Based on PAQ, RZM, CSC, CCM, and 7zip. The format consists of a PAQ, RZM, CSC, or CCM compressed file and a manifest with compression settings stored in a 7z archive.
|
||||
*.uca
|
||||
# A high compression rate archive format originally for DOS.
|
||||
*.uha
|
||||
# Web Application archive (Java-based web app)
|
||||
*.war
|
||||
# File-based disk image format developed to deploy Microsoft Windows.
|
||||
*.wim
|
||||
# XAR
|
||||
*.xar
|
||||
# Native format of the Open Source KiriKiri Visual Novel engine. Uses combination of block splitting and zlib compression. The filenames and pathes are stored in UTF-16 format. For integrity check, the Adler-32 hashsum is used. For many commercial games, the files are encrypted (and decoded on runtime) via so-called "cxdec" module, which implements xor-based encryption.
|
||||
*.xp3
|
||||
# Yamazaki zipper archive. Compression format used in DeepFreezer archiver utility created by Yamazaki Satoshi. Read and write support exists in TUGZip, IZArc and ZipZag
|
||||
*.yz1
|
||||
# The most widely used compression format on Microsoft Windows. Commonly used on Macintosh and Unix systems as well.
|
||||
*.zip
|
||||
*.zipx
|
||||
# application/x-zoo zoo Multiple Multiple Yes
|
||||
*.zoo
|
||||
# Journaling (append-only) archive format with rollback capability. Supports deduplication and incremental update based on last-modified dates. Multi-threaded. Compresses in LZ77, BWT, and context mixing formats. Open source.
|
||||
*.zpaq
|
||||
# Archiver with a compression algorithm based on the Burrows-Wheeler transform method.
|
||||
*.zz
|
||||
|
||||
|
||||
### Executable ###
|
||||
*.app
|
||||
*.bat
|
||||
*.cgi
|
||||
*.com
|
||||
*.exe
|
||||
*.gadget
|
||||
*.pif
|
||||
*.vb
|
||||
*.wsf
|
||||
|
||||
### Git ###
|
||||
# Created by git for backups. To disable backups in Git:
|
||||
# $ git config --global mergetool.keepBackup false
|
||||
*.orig
|
||||
|
||||
# Created by git when using merge tools for conflicts
|
||||
*.BACKUP.*
|
||||
*.BASE.*
|
||||
*.LOCAL.*
|
||||
*.REMOTE.*
|
||||
*_BACKUP_*.txt
|
||||
*_BASE_*.txt
|
||||
*_LOCAL_*.txt
|
||||
*_REMOTE_*.txt
|
||||
|
||||
### Go ###
|
||||
# Binaries for programs and plugins
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
### Go Patch ###
|
||||
/vendor/
|
||||
/Godeps/
|
||||
|
||||
### Intellij ###
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
|
||||
# Generated files
|
||||
.idea/**/contentModel.xml
|
||||
|
||||
# Sensitive or high-churn files
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|
||||
|
||||
# Gradle
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
|
||||
# Gradle and Maven with auto-import
|
||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||
# since they will be recreated, and may cause churn. Uncomment if using
|
||||
# auto-import.
|
||||
# .idea/modules.xml
|
||||
# .idea/*.iml
|
||||
# .idea/modules
|
||||
# *.iml
|
||||
# *.ipr
|
||||
|
||||
# CMake
|
||||
cmake-build-*/
|
||||
|
||||
# Mongo Explorer plugin
|
||||
.idea/**/mongoSettings.xml
|
||||
|
||||
# File-based project format
|
||||
*.iws
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Cursive Clojure plugin
|
||||
.idea/replstate.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
# Editor-based Rest Client
|
||||
.idea/httpRequests
|
||||
|
||||
# Android studio 3.1+ serialized cache file
|
||||
.idea/caches/build_file_checksums.ser
|
||||
|
||||
### Intellij Patch ###
|
||||
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
|
||||
|
||||
# *.iml
|
||||
# modules.xml
|
||||
# .idea/misc.xml
|
||||
# *.ipr
|
||||
|
||||
# Sonarlint plugin
|
||||
.idea/sonarlint
|
||||
|
||||
### JetBrains+all Patch ###
|
||||
# Ignores the whole .idea folder and all .iml files
|
||||
# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360
|
||||
|
||||
.idea/
|
||||
|
||||
# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
|
||||
|
||||
*.iml
|
||||
modules.xml
|
||||
.idea/misc.xml
|
||||
*.ipr
|
||||
|
||||
# Sonarlint plugin
|
||||
|
||||
### Linux ###
|
||||
*~
|
||||
|
||||
# temporary files which can be created if a process still has a handle open of a deleted file
|
||||
.fuse_hidden*
|
||||
|
||||
# KDE directory preferences
|
||||
.directory
|
||||
|
||||
# Linux trash folder which might appear on any partition or disk
|
||||
.Trash-*
|
||||
|
||||
# .nfs files are created when an open file is removed but is still being accessed
|
||||
.nfs*
|
||||
|
||||
### OSX ###
|
||||
# General
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
### VisualStudioCode ###
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
### VisualStudioCode Patch ###
|
||||
# Ignore all local history of files
|
||||
.history
|
||||
|
||||
### Windows ###
|
||||
# Windows thumbnail cache files
|
||||
Thumbs.db
|
||||
Thumbs.db:encryptable
|
||||
ehthumbs.db
|
||||
ehthumbs_vista.db
|
||||
|
||||
# Dump file
|
||||
*.stackdump
|
||||
|
||||
# Folder config file
|
||||
[Dd]esktop.ini
|
||||
|
||||
# Recycle Bin used on file shares
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# Windows Installer files
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# Windows shortcuts
|
||||
*.lnk
|
||||
|
||||
# End of https://www.gitignore.io/api/intellij,go,linux,osx,windows,node,python,executable,jetbrains+all,visualstudiocode,compressedarchive,git
|
||||
|
||||
go.sum
|
||||
/vendor
|
|
@ -0,0 +1,22 @@
|
|||
The BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2022 Sangbum Kim.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided
|
||||
that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this list of conditions
|
||||
and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and
|
||||
the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or
|
||||
promote products derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
|
||||
THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -0,0 +1,20 @@
|
|||
# miscellaneous
|
||||
|
||||
[![GoDoc](https://godoc.org/amuz.es/src/go/misc?status.png)](http://godoc.org/amuz.es/src/go/misc)
|
||||
[![Go Report](https://goreportcard.com/badge/amuz.es/src/go/misc)](https://goreportcard.com/report/amuz.es/src/go/misc)
|
||||
|
||||
## Description
|
||||
|
||||
Miscellaneous utility collections.
|
||||
|
||||
## Requirements
|
||||
|
||||
Go 1.5 or above.
|
||||
|
||||
## Installation
|
||||
|
||||
Run the following command to install the package:
|
||||
|
||||
```
|
||||
go get amuz.es/src/go/misc
|
||||
```
|
|
@ -0,0 +1,63 @@
|
|||
package backoff
|
||||
|
||||
import (
|
||||
"math"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Algorithm defines a function that calculates a time.Duration based on
|
||||
// the given retry attempt number.
|
||||
type Algorithm func(attempt uint) time.Duration
|
||||
|
||||
// Incremental creates a Algorithm that increments the initial duration
|
||||
// by the given increment for each attempt.
|
||||
func Incremental(initial, increment time.Duration) Algorithm {
|
||||
return func(attempt uint) time.Duration {
|
||||
return initial + (increment * time.Duration(attempt))
|
||||
}
|
||||
}
|
||||
|
||||
// Linear creates a Algorithm that linearly multiplies the factor
|
||||
// duration by the attempt number for each attempt.
|
||||
func Linear(factor time.Duration) Algorithm {
|
||||
return func(attempt uint) time.Duration {
|
||||
return factor * time.Duration(attempt)
|
||||
}
|
||||
}
|
||||
|
||||
// Exponential creates a Algorithm that multiplies the factor duration by
|
||||
// an exponentially increasing factor for each attempt, where the factor is
|
||||
// calculated as the given base raised to the attempt number.
|
||||
func Exponential(factor time.Duration, base float64) Algorithm {
|
||||
return func(attempt uint) time.Duration {
|
||||
return factor * time.Duration(math.Pow(base, float64(attempt)))
|
||||
}
|
||||
}
|
||||
|
||||
// BinaryExponential creates a Algorithm that multiplies the factor
|
||||
// duration by an exponentially increasing factor for each attempt, where the
|
||||
// factor is calculated as `2` raised to the attempt number (2^attempt).
|
||||
func BinaryExponential(factor time.Duration) Algorithm {
|
||||
return Exponential(factor, 2)
|
||||
}
|
||||
|
||||
// Fibonacci creates a Algorithm that multiplies the factor duration by
|
||||
// an increasing factor for each attempt, where the factor is the Nth number in
|
||||
// the Fibonacci sequence.
|
||||
func Fibonacci(factor time.Duration) Algorithm {
|
||||
return func(attempt uint) time.Duration {
|
||||
return factor * time.Duration(fibonacciNumber(attempt))
|
||||
}
|
||||
}
|
||||
|
||||
// fibonacciNumber calculates the Fibonacci sequence number for the given
|
||||
// sequence position.
|
||||
func fibonacciNumber(n uint) uint {
|
||||
if 0 == n {
|
||||
return 0
|
||||
} else if 1 == n {
|
||||
return 1
|
||||
} else {
|
||||
return fibonacciNumber(n-1) + fibonacciNumber(n-2)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
// Package backoff contains backoff algorithm helper functions.
|
||||
package backoff
|
|
@ -0,0 +1,68 @@
|
|||
package misc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type (
|
||||
handlerImpl struct {
|
||||
errorChan chan error
|
||||
ctx context.Context
|
||||
canceler context.CancelFunc
|
||||
waiter sync.WaitGroup
|
||||
}
|
||||
|
||||
// Handler is context handler with sync.WaitGroup and error notifier.
|
||||
Handler interface {
|
||||
// NotifyError is an error notifier method within a concurrency environment that avoids falling in deadlock.
|
||||
NotifyError(err error)
|
||||
// Error method is a Error channel for cancellation.
|
||||
Error() <-chan error
|
||||
// Done method is a Done channel for cancellation.
|
||||
Done() <-chan struct{}
|
||||
// GracefulWait is a graceful shutdown method while the dependents are safely released.
|
||||
// It acts like a "WaitGroup.Wait" method.
|
||||
GracefulWait()
|
||||
// IncreaseWait is an acquiring method for a dependent.
|
||||
// It acts like a "WaitGroup.Add(1)" method.
|
||||
IncreaseWait()
|
||||
// DecreaseWait is an release method for a dependent.
|
||||
// It acts like a "WaitGroup.Done" method.
|
||||
DecreaseWait()
|
||||
}
|
||||
)
|
||||
|
||||
// NewHandler returns an initialized Handler interface.
|
||||
func NewHandler(ctx context.Context) Handler {
|
||||
ctx, canceler := context.WithCancel(ctx)
|
||||
return &handlerImpl{
|
||||
ctx: ctx,
|
||||
canceler: canceler,
|
||||
errorChan: make(chan error, 5),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handlerImpl) NotifyError(err error) { h.errorChan <- err }
|
||||
func (h *handlerImpl) Error() <-chan error { return h.errorChan }
|
||||
func (h *handlerImpl) Done() <-chan struct{} { return h.ctx.Done() }
|
||||
func (h *handlerImpl) GracefulWait() {
|
||||
if h.ctx.Err() == nil {
|
||||
h.canceler()
|
||||
}
|
||||
|
||||
h.waiter.Wait()
|
||||
close(h.errorChan)
|
||||
|
||||
for remainError := range h.errorChan {
|
||||
log.Println("remain errors ", remainError)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handlerImpl) IncreaseWait() {
|
||||
h.waiter.Add(1)
|
||||
}
|
||||
func (h *handlerImpl) DecreaseWait() {
|
||||
h.waiter.Done()
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
// Package crypto contains encryption-decryption helper functions.
|
||||
package crypto
|
|
@ -0,0 +1,18 @@
|
|||
package crypto
|
||||
|
||||
// Pkcs5pad is a padding function that uses the PKCS5 method.
|
||||
func Pkcs5pad(data []byte, blocksize int) []byte {
|
||||
pad := blocksize - len(data)%blocksize
|
||||
b := make([]byte, pad, pad)
|
||||
for i := 0; i < pad; i++ {
|
||||
b[i] = uint8(pad)
|
||||
}
|
||||
return append(data, b...)
|
||||
}
|
||||
|
||||
// Pkcs5unpad is a stripping function that reverts the PKCS5 method.
|
||||
func Pkcs5unpad(data []byte) []byte {
|
||||
pad := int(data[len(data)-1])
|
||||
// FIXME: check that the padding bytes are all what we expect
|
||||
return data[:len(data)-pad]
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"github.com/minio/sio"
|
||||
"io"
|
||||
)
|
||||
|
||||
/*
|
||||
NewSecretBox returns a SecretBox object with a provided key.
|
||||
|
||||
Here is the validation code written for Python:
|
||||
import os,binascii
|
||||
key=binascii.b2a_base64(os.urandom(32),newline=False)
|
||||
print(key.decode('us-ascii'))
|
||||
*/
|
||||
func NewSecretBox(key []byte) SecretBox {
|
||||
streamConfig := sio.Config{
|
||||
Key: key,
|
||||
Rand: rand.Reader,
|
||||
}
|
||||
var (
|
||||
encryptReader = func(src io.Reader) (io.Reader, error) {
|
||||
return sio.EncryptReader(src, streamConfig)
|
||||
}
|
||||
decryptReader = func(src io.Reader) (io.Reader, error) {
|
||||
return sio.DecryptReader(src, streamConfig)
|
||||
}
|
||||
encryptWriter = func(dst io.Writer) (io.WriteCloser, error) {
|
||||
return sio.EncryptWriter(dst, streamConfig)
|
||||
}
|
||||
decryptWriter = func(dst io.Writer) (io.WriteCloser, error) {
|
||||
return sio.DecryptWriter(dst, streamConfig)
|
||||
}
|
||||
)
|
||||
return &secretBoxImpl{
|
||||
encryptReaderGen: encryptReader,
|
||||
decryptReaderGen: decryptReader,
|
||||
encryptWriterGen: encryptWriter,
|
||||
decryptWriterGen: decryptWriter,
|
||||
}
|
||||
}
|
||||
|
||||
// SecretBox is an encryption and decryption provider.
|
||||
type SecretBox interface {
|
||||
// NewEncryptReader returns EncryptReader from given io.Reader.
|
||||
NewEncryptReader(io.Reader) (io.Reader, error)
|
||||
// NewDecryptReader returns DecryptReader from given io.Reader.
|
||||
NewDecryptReader(src io.Reader) (io.Reader, error)
|
||||
// NewEncryptWriter returns EncryptWriter from given io.Writer.
|
||||
NewEncryptWriter(dst io.Writer) (io.WriteCloser, error)
|
||||
// NewDecryptWriter returns DecryptWriter from given io.Writer.
|
||||
NewDecryptWriter(dst io.Writer) (io.WriteCloser, error)
|
||||
// EncryptedSize returns encrypted data size from given raw data size.
|
||||
EncryptedSize(size uint64) (uint64, error)
|
||||
// DecryptedSize returns decrypted data size from given raw data size.
|
||||
DecryptedSize(size uint64) (uint64, error)
|
||||
}
|
||||
|
||||
type secretBoxImpl struct {
|
||||
encryptReaderGen func(src io.Reader) (io.Reader, error)
|
||||
decryptReaderGen func(src io.Reader) (io.Reader, error)
|
||||
encryptWriterGen func(dst io.Writer) (io.WriteCloser, error)
|
||||
decryptWriterGen func(dst io.Writer) (io.WriteCloser, error)
|
||||
}
|
||||
|
||||
// 인터페이스가 실제 dto랑 호환되는가
|
||||
var _ SecretBox = (*secretBoxImpl)(nil)
|
||||
|
||||
func (x *secretBoxImpl) NewEncryptReader(r io.Reader) (io.Reader, error) {
|
||||
return x.encryptReaderGen(r)
|
||||
}
|
||||
func (x *secretBoxImpl) NewDecryptReader(r io.Reader) (io.Reader, error) {
|
||||
return x.decryptReaderGen(r)
|
||||
}
|
||||
func (x *secretBoxImpl) NewEncryptWriter(w io.Writer) (io.WriteCloser, error) {
|
||||
return x.encryptWriterGen(w)
|
||||
}
|
||||
func (x *secretBoxImpl) NewDecryptWriter(w io.Writer) (io.WriteCloser, error) {
|
||||
return x.decryptWriterGen(w)
|
||||
}
|
||||
func (x *secretBoxImpl) EncryptedSize(size uint64) (uint64, error) { return sio.EncryptedSize(size) }
|
||||
func (x *secretBoxImpl) DecryptedSize(size uint64) (uint64, error) { return sio.DecryptedSize(size) }
|
|
@ -0,0 +1,392 @@
|
|||
package crypto
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/sha1"
|
||||
"crypto/subtle"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestShortEncryption1(t *testing.T) {
|
||||
key := make([]byte, 32)
|
||||
rand.Read(key)
|
||||
box := NewSecretBox(key)
|
||||
source := "h"
|
||||
hasher := sha1.New()
|
||||
_, err := strings.NewReader(source).WriteTo(hasher)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
formerHash := hasher.Sum(nil)
|
||||
t.Log("former hash ", hex.EncodeToString(formerHash))
|
||||
|
||||
encryptor, err := box.NewEncryptReader(strings.NewReader(source))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
trsnBuf := &bytes.Buffer{}
|
||||
_, err = trsnBuf.ReadFrom(encryptor)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
hasher.Reset()
|
||||
decrypted, err := box.NewDecryptWriter(hasher)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = trsnBuf.WriteTo(decrypted)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_ = decrypted.Close()
|
||||
latterHash := hasher.Sum(nil)
|
||||
t.Log("latter hash ", hex.EncodeToString(latterHash))
|
||||
if subtle.ConstantTimeCompare(formerHash, latterHash) != 1 {
|
||||
t.Fatal("hash not matched!")
|
||||
} else {
|
||||
t.Log("Hash Match!")
|
||||
}
|
||||
}
|
||||
func TestShortEncryption2(t *testing.T) {
|
||||
key := make([]byte, 32)
|
||||
rand.Read(key)
|
||||
box := NewSecretBox(key)
|
||||
source := "h"
|
||||
hasher := sha1.New()
|
||||
_, err := strings.NewReader(source).WriteTo(hasher)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
formerHash := hasher.Sum(nil)
|
||||
t.Log("former hash ", hex.EncodeToString(formerHash))
|
||||
|
||||
encryptor, err := box.NewEncryptReader(strings.NewReader(source))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
trsnBuf := &bytes.Buffer{}
|
||||
_, err = trsnBuf.ReadFrom(encryptor)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
hasher.Reset()
|
||||
decrypted, err := box.NewDecryptReader(trsnBuf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = io.Copy(hasher, decrypted)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
latterHash := hasher.Sum(nil)
|
||||
t.Log("latter hash ", hex.EncodeToString(latterHash))
|
||||
if subtle.ConstantTimeCompare(formerHash, latterHash) != 1 {
|
||||
t.Fatal("hash not matched!")
|
||||
} else {
|
||||
t.Log("Hash Match!")
|
||||
}
|
||||
}
|
||||
|
||||
func TestShortEncryption3(t *testing.T) {
|
||||
key := make([]byte, 32)
|
||||
rand.Read(key)
|
||||
box := NewSecretBox(key)
|
||||
source := "h"
|
||||
hasher := sha1.New()
|
||||
_, err := strings.NewReader(source).WriteTo(hasher)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
formerHash := hasher.Sum(nil)
|
||||
t.Log("former hash ", hex.EncodeToString(formerHash))
|
||||
|
||||
trsnBuf := &bytes.Buffer{}
|
||||
encryptor, err := box.NewEncryptWriter(trsnBuf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = strings.NewReader(source).WriteTo(encryptor)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_ = encryptor.Close()
|
||||
hasher.Reset()
|
||||
decrypted, err := box.NewDecryptReader(trsnBuf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = io.Copy(hasher, decrypted)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
latterHash := hasher.Sum(nil)
|
||||
t.Log("latter hash ", hex.EncodeToString(latterHash))
|
||||
if subtle.ConstantTimeCompare(formerHash, latterHash) != 1 {
|
||||
t.Fatal("hash not matched!")
|
||||
} else {
|
||||
t.Log("Hash Match!")
|
||||
}
|
||||
}
|
||||
|
||||
func TestShortEncryption4(t *testing.T) {
|
||||
key := make([]byte, 32)
|
||||
rand.Read(key)
|
||||
box := NewSecretBox(key)
|
||||
source := "h"
|
||||
hasher := sha1.New()
|
||||
_, err := strings.NewReader(source).WriteTo(hasher)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
formerHash := hasher.Sum(nil)
|
||||
t.Log("former hash ", hex.EncodeToString(formerHash))
|
||||
|
||||
trsnBuf := &bytes.Buffer{}
|
||||
encryptor, err := box.NewEncryptWriter(trsnBuf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = strings.NewReader(source).WriteTo(encryptor)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_ = encryptor.Close()
|
||||
hasher.Reset()
|
||||
decrypted, err := box.NewDecryptWriter(hasher)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = trsnBuf.WriteTo(decrypted)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_ = decrypted.Close()
|
||||
latterHash := hasher.Sum(nil)
|
||||
t.Log("latter hash ", hex.EncodeToString(latterHash))
|
||||
if subtle.ConstantTimeCompare(formerHash, latterHash) != 1 {
|
||||
t.Fatal("hash not matched!")
|
||||
} else {
|
||||
t.Log("Hash Match!")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLongEncryption1(t *testing.T) {
|
||||
key := make([]byte, 32)
|
||||
rand.Read(key)
|
||||
|
||||
buf := make([]byte, 1024*1024*5)
|
||||
rand.Read(buf)
|
||||
|
||||
box := NewSecretBox(key)
|
||||
hasher := sha1.New()
|
||||
_, err := hasher.Write(buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
formerHash := hasher.Sum(nil)
|
||||
t.Log("former hash ", hex.EncodeToString(formerHash))
|
||||
|
||||
encryptor, err := box.NewEncryptReader(bytes.NewReader(buf))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
trsnBuf := &bytes.Buffer{}
|
||||
_, err = trsnBuf.ReadFrom(encryptor)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
hasher.Reset()
|
||||
decrypted, err := box.NewDecryptWriter(hasher)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = trsnBuf.WriteTo(decrypted)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_ = decrypted.Close()
|
||||
latterHash := hasher.Sum(nil)
|
||||
t.Log("latter hash ", hex.EncodeToString(latterHash))
|
||||
if subtle.ConstantTimeCompare(formerHash, latterHash) != 1 {
|
||||
t.Fatal("hash not matched!")
|
||||
} else {
|
||||
t.Log("Hash Match!")
|
||||
}
|
||||
}
|
||||
func TestLongEncryption2(t *testing.T) {
|
||||
key := make([]byte, 32)
|
||||
rand.Read(key)
|
||||
box := NewSecretBox(key)
|
||||
|
||||
buf := make([]byte, 1024*1024*5)
|
||||
rand.Read(buf)
|
||||
|
||||
hasher := sha1.New()
|
||||
_, err := hasher.Write(buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
formerHash := hasher.Sum(nil)
|
||||
t.Log("former hash ", hex.EncodeToString(formerHash))
|
||||
|
||||
encryptor, err := box.NewEncryptReader(bytes.NewReader(buf))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
trsnBuf := &bytes.Buffer{}
|
||||
_, err = trsnBuf.ReadFrom(encryptor)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
hasher.Reset()
|
||||
decrypted, err := box.NewDecryptReader(trsnBuf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = io.Copy(hasher, decrypted)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
latterHash := hasher.Sum(nil)
|
||||
t.Log("latter hash ", hex.EncodeToString(latterHash))
|
||||
if subtle.ConstantTimeCompare(formerHash, latterHash) != 1 {
|
||||
t.Fatal("hash not matched!")
|
||||
} else {
|
||||
t.Log("Hash Match!")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLongEncryption3(t *testing.T) {
|
||||
key := make([]byte, 32)
|
||||
rand.Read(key)
|
||||
box := NewSecretBox(key)
|
||||
|
||||
buf := make([]byte, 1024*1024*5)
|
||||
rand.Read(buf)
|
||||
|
||||
hasher := sha1.New()
|
||||
_, err := hasher.Write(buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
formerHash := hasher.Sum(nil)
|
||||
t.Log("former hash ", hex.EncodeToString(formerHash))
|
||||
|
||||
trsnBuf := &bytes.Buffer{}
|
||||
encryptor, err := box.NewEncryptWriter(trsnBuf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = encryptor.Write(buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_ = encryptor.Close()
|
||||
hasher.Reset()
|
||||
decrypted, err := box.NewDecryptReader(trsnBuf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = io.Copy(hasher, decrypted)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
latterHash := hasher.Sum(nil)
|
||||
t.Log("latter hash ", hex.EncodeToString(latterHash))
|
||||
if subtle.ConstantTimeCompare(formerHash, latterHash) != 1 {
|
||||
t.Fatal("hash not matched!")
|
||||
} else {
|
||||
t.Log("Hash Match!")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLongEncryption4(t *testing.T) {
|
||||
key := make([]byte, 32)
|
||||
rand.Read(key)
|
||||
box := NewSecretBox(key)
|
||||
|
||||
buf := make([]byte, 1024*1024*5)
|
||||
rand.Read(buf)
|
||||
|
||||
hasher := sha1.New()
|
||||
_, err := hasher.Write(buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
formerHash := hasher.Sum(nil)
|
||||
t.Log("former hash ", hex.EncodeToString(formerHash))
|
||||
|
||||
trsnBuf := &bytes.Buffer{}
|
||||
encryptor, err := box.NewEncryptWriter(trsnBuf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = encryptor.Write(buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_ = encryptor.Close()
|
||||
hasher.Reset()
|
||||
decrypted, err := box.NewDecryptWriter(hasher)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = trsnBuf.WriteTo(decrypted)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_ = decrypted.Close()
|
||||
latterHash := hasher.Sum(nil)
|
||||
t.Log("latter hash ", hex.EncodeToString(latterHash))
|
||||
if subtle.ConstantTimeCompare(formerHash, latterHash) != 1 {
|
||||
t.Fatal("hash not matched!")
|
||||
} else {
|
||||
t.Log("Hash Match!")
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
//func TestLongEncryptionDescription(t *testing.T) {
|
||||
// var key [32]byte
|
||||
// rand.Read(key[:])
|
||||
//
|
||||
// buf := make([]byte, 1024*1024*5)
|
||||
// rand.Read(buf)
|
||||
// hasher := sha1.New()
|
||||
// hasher.Write(buf)
|
||||
// formerHash := hasher.Sum(nil)
|
||||
// t.Log("former hash ", hex.EncodeToString(formerHash))
|
||||
//
|
||||
// var encryptBuf bytes.Buffer
|
||||
// encryptor, err := NewEncryptor(&encryptBuf, &key)
|
||||
// if err != nil {
|
||||
// t.Fatal("cannot create crypto", err)
|
||||
// }
|
||||
// _, err = encryptor.Write(buf)
|
||||
// if err != nil {
|
||||
// t.Fatal("cannot create encryptorGenerator", err)
|
||||
// }
|
||||
// err = encryptor.Close()
|
||||
//
|
||||
// decryptor, err := NewDecryptor(&encryptBuf, &key)
|
||||
// if err != nil {
|
||||
// t.Fatal("cannot create decryptorGenerator", err)
|
||||
// }
|
||||
// decrypted, err := ioutil.ReadAll(decryptor)
|
||||
// if err != nil {
|
||||
// t.Fatal("error occurred while reading ", err)
|
||||
// }
|
||||
// hasher.Reset()
|
||||
// hasher.Write(decrypted)
|
||||
// latterHash := hasher.Sum(nil)
|
||||
// t.Log("former hash ", hex.EncodeToString(latterHash))
|
||||
// if subtle.ConstantTimeCompare(formerHash, latterHash) != 1 {
|
||||
// t.Fatal("hash not matched!")
|
||||
// } else {
|
||||
// t.Log("Hash Match!")
|
||||
// }
|
||||
//}
|
|
@ -0,0 +1,120 @@
|
|||
package crypto
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/cipher"
|
||||
"errors"
|
||||
"io"
|
||||
)
|
||||
|
||||
// ErrShortCiphertext is an error that has text too short.
|
||||
var ErrShortCiphertext = errors.New("input too short to be valid ciphertext")
|
||||
|
||||
/*
|
||||
NewCryptoWriter returns block cipher writer.
|
||||
from https://github.com/acasajus/dkeyczar/blob/master/streams.go
|
||||
*/
|
||||
func NewCryptoWriter(bm cipher.BlockMode, sink io.WriteCloser) io.WriteCloser {
|
||||
return &cryptoWriter{
|
||||
bm: bm,
|
||||
sink: sink,
|
||||
buffer: bytes.NewBuffer(nil),
|
||||
}
|
||||
}
|
||||
|
||||
type cryptoWriter struct {
|
||||
bm cipher.BlockMode
|
||||
buffer *bytes.Buffer
|
||||
sink io.WriteCloser
|
||||
count int
|
||||
}
|
||||
|
||||
func (c *cryptoWriter) Write(data []byte) (int, error) {
|
||||
if _, err := c.buffer.Write(data); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
bL := c.buffer.Len() - c.buffer.Len()%c.bm.BlockSize()
|
||||
tmp := c.buffer.Next(bL)
|
||||
c.bm.CryptBlocks(tmp, tmp)
|
||||
wL := 0
|
||||
for wL < len(tmp) {
|
||||
n, err := c.sink.Write(tmp[wL:])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
wL += n
|
||||
}
|
||||
c.count += wL
|
||||
return len(data), nil
|
||||
}
|
||||
|
||||
func (c *cryptoWriter) Close() error {
|
||||
tmp := Pkcs5pad(c.buffer.Next(c.buffer.Len()), c.bm.BlockSize())
|
||||
c.bm.CryptBlocks(tmp, tmp)
|
||||
wL := 0
|
||||
for wL < len(tmp) {
|
||||
n, err := c.sink.Write(tmp[wL:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
wL += n
|
||||
}
|
||||
c.count += wL
|
||||
return c.sink.Close()
|
||||
}
|
||||
|
||||
func NewCryptoReader(bm cipher.BlockMode, source io.ReadCloser) io.ReadCloser {
|
||||
return &cryptoReader{
|
||||
bm: bm,
|
||||
source: source,
|
||||
outBuf: bytes.NewBuffer(nil),
|
||||
inBuf: bytes.NewBuffer(nil),
|
||||
eof: false,
|
||||
}
|
||||
}
|
||||
|
||||
type cryptoReader struct {
|
||||
bm cipher.BlockMode
|
||||
outBuf *bytes.Buffer
|
||||
inBuf *bytes.Buffer
|
||||
source io.ReadCloser
|
||||
eof bool
|
||||
}
|
||||
|
||||
func (cr *cryptoReader) Read(data []byte) (int, error) {
|
||||
missing := len(data) - cr.outBuf.Len()
|
||||
for !cr.eof && missing > 0 {
|
||||
toRead := missing + cr.bm.BlockSize() + 1 //Always go beyond the required data to be able to unpad when eof'ed
|
||||
if off := toRead % cr.bm.BlockSize(); off > 0 {
|
||||
toRead += cr.bm.BlockSize() - off //Make sure we read in multiples of blocksize
|
||||
}
|
||||
cr.inBuf.Grow(toRead)
|
||||
n, err := io.CopyN(cr.inBuf, cr.source, int64(toRead))
|
||||
if err == io.EOF {
|
||||
cr.eof = true
|
||||
} else if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
readBytes := int(n)
|
||||
if readBytes%cr.bm.BlockSize() > 0 && cr.eof {
|
||||
return 0, ErrShortCiphertext
|
||||
}
|
||||
bytesToDec := readBytes - readBytes%cr.bm.BlockSize()
|
||||
tmpdata := cr.inBuf.Next(bytesToDec)
|
||||
cr.bm.CryptBlocks(tmpdata, tmpdata)
|
||||
if _, err := cr.outBuf.Write(tmpdata); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if cr.eof {
|
||||
pad := cr.outBuf.Bytes()[cr.outBuf.Len()-1]
|
||||
cr.outBuf.Truncate(cr.outBuf.Len() - int(pad))
|
||||
}
|
||||
missing = len(data) - cr.outBuf.Len()
|
||||
}
|
||||
return cr.outBuf.Read(data)
|
||||
}
|
||||
|
||||
func (cr *cryptoReader) Close() error {
|
||||
cr.eof = true
|
||||
return cr.source.Close()
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
package dbg
|
||||
|
||||
import "bytes"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"golang.org/x/text/width"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// HexDump returns hexadecimal data dump data.
|
||||
func HexDump(by []byte) string {
|
||||
n := len(by)
|
||||
rowcount := 0
|
||||
stop := (n / 16) * 16
|
||||
k := 0
|
||||
buf := &bytes.Buffer{}
|
||||
|
||||
for i := 0; i <= stop; i += 16 {
|
||||
k++
|
||||
if i+16 < n {
|
||||
rowcount = 16
|
||||
} else {
|
||||
rowcount = min(k*16, n) % 16
|
||||
}
|
||||
|
||||
fmt.Fprintf(buf, "%08x ", i)
|
||||
for j := 0; j < rowcount; j++ {
|
||||
if j%8 == 0 {
|
||||
fmt.Fprintf(buf, " %02x ", by[i+j])
|
||||
} else {
|
||||
fmt.Fprintf(buf, "%02x ", by[i+j])
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
for j := rowcount; j < 16; j++ {
|
||||
if j%8 == 0 {
|
||||
fmt.Fprintf(buf, " ")
|
||||
} else {
|
||||
fmt.Fprintf(buf, " ")
|
||||
}
|
||||
}
|
||||
buf.WriteRune('|')
|
||||
viewString(by[i:(i+rowcount)], buf)
|
||||
buf.WriteRune('|')
|
||||
buf.WriteRune('\n')
|
||||
buf.WriteRune('\r')
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// GuessUnicodeWidth returns the size of bytes for a single rune.
|
||||
func GuessUnicodeWidth(char rune) (realSize int) {
|
||||
prop := width.LookupRune(char)
|
||||
switch prop.Kind() {
|
||||
case width.EastAsianFullwidth:
|
||||
fallthrough
|
||||
case width.EastAsianWide:
|
||||
realSize = 2
|
||||
case width.EastAsianHalfwidth:
|
||||
fallthrough
|
||||
case width.EastAsianNarrow:
|
||||
realSize = 2
|
||||
case width.EastAsianAmbiguous:
|
||||
fallthrough
|
||||
case width.Neutral:
|
||||
fallthrough
|
||||
default:
|
||||
realSize = 1
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func viewString(b []byte, buf *bytes.Buffer) {
|
||||
for {
|
||||
if r, size := utf8.DecodeRune(b); size == 0 {
|
||||
return
|
||||
} else if r == utf8.RuneError {
|
||||
for i := 0; i < size; i++ {
|
||||
buf.WriteRune('_')
|
||||
}
|
||||
b = b[size:]
|
||||
} else if r < 32 {
|
||||
for i := 0; i < size; i++ {
|
||||
buf.WriteRune('.')
|
||||
}
|
||||
b = b[size:]
|
||||
} else {
|
||||
buf.WriteRune(r)
|
||||
pad := max(0, size-GuessUnicodeWidth(r))
|
||||
for i := 0; i < pad; i++ {
|
||||
buf.WriteRune('.')
|
||||
}
|
||||
b = b[size:]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
// Package dbg contains debugging helper functions.
|
||||
package dbg
|
|
@ -0,0 +1,10 @@
|
|||
package dttm
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
//BetweenTwoDates is a function that checks if a given date is between a start date and an end date.
|
||||
func BetweenTwoDates(target, start, end time.Time) bool {
|
||||
return (start.Before(target) || start.Equal(target)) && end.After(target)
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package dttm
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
// SeoulTZ variable exposes "Asia/Seoul" timezone.
|
||||
SeoulTZ, _ = time.LoadLocation("Asia/Seoul")
|
||||
)
|
|
@ -0,0 +1,2 @@
|
|||
// Package dttm contains date-time and locale-related data structures.
|
||||
package dttm
|
|
@ -0,0 +1,25 @@
|
|||
module amuz.es/src/go/misc
|
||||
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/ericlagergren/decimal v0.0.0-20211103172832-aca2edc11f73
|
||||
github.com/jmoiron/sqlx v1.3.4
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/minio/sio v0.3.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/valyala/fasthttp v1.34.0
|
||||
go.uber.org/multierr v1.8.0
|
||||
golang.org/x/text v0.3.7
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.0.4 // indirect
|
||||
github.com/klauspost/compress v1.15.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
go.uber.org/atomic v1.7.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20220214200702-86341886e292 // indirect
|
||||
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 // indirect
|
||||
)
|
|
@ -0,0 +1,121 @@
|
|||
package han
|
||||
|
||||
import (
|
||||
"github.com/valyala/fasthttp"
|
||||
"go.uber.org/multierr"
|
||||
"golang.org/x/text/encoding/korean"
|
||||
"golang.org/x/text/transform"
|
||||
"net/url"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
const (
|
||||
hexConstUpper = "0123456789ABCDEF"
|
||||
)
|
||||
|
||||
// ConvertFormEUCKRFasthttp is translate method from euc-kr encoded fasthttp.Args to utf-8 encoded fasthttp.Args.
|
||||
func ConvertFormEUCKRFasthttp(rawParsed *fasthttp.Args) (parsed *fasthttp.Args, err error) {
|
||||
var (
|
||||
//rawKey, rawValue string
|
||||
decoder = korean.EUCKR.NewDecoder()
|
||||
)
|
||||
|
||||
parsed = &fasthttp.Args{}
|
||||
rawParsed.VisitAll(func(k, v []byte) {
|
||||
if rawKey, decodeErr := decoder.Bytes(k); decodeErr != nil {
|
||||
err = multierr.Append(err, decodeErr)
|
||||
} else if rawValue, decodeErr := decoder.Bytes(v); decodeErr != nil {
|
||||
err = multierr.Append(err, decodeErr)
|
||||
} else {
|
||||
parsed.SetBytesKV(rawKey, rawValue)
|
||||
}
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ConvertFormEUCKR is translate method from euc-kr encoded url.Values to utf-8 encoded url.Values.
|
||||
func ConvertFormEUCKR(rawParsed url.Values) (parsed url.Values, err error) {
|
||||
var (
|
||||
rawKey, rawValue string
|
||||
decoder = korean.EUCKR.NewDecoder()
|
||||
)
|
||||
|
||||
parsed = make(url.Values, len(rawParsed))
|
||||
for k, v := range rawParsed {
|
||||
rawKey, err = decoder.String(k)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
valueList := make([]string, 0, len(v))
|
||||
for _, vi := range v {
|
||||
rawValue, err = decoder.String(vi)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
valueList = append(valueList, rawValue)
|
||||
}
|
||||
parsed[rawKey] = valueList
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ConvertToEUCKRUrlEncoded is translate method from utf-8 encoded string to euc-kr encoded string.
|
||||
func ConvertToEUCKRUrlEncoded(str string) (converted string, err error) {
|
||||
var (
|
||||
buf strings.Builder
|
||||
encoder = korean.EUCKR.NewEncoder()
|
||||
size int
|
||||
)
|
||||
for _, r := range str {
|
||||
switch {
|
||||
case unicode.IsNumber(r) ||
|
||||
unicode.Is(unicode.Latin, r) ||
|
||||
r == '-' ||
|
||||
r == '_' ||
|
||||
r == '.':
|
||||
buf.WriteRune(r)
|
||||
case unicode.IsSpace(r):
|
||||
buf.WriteByte('+')
|
||||
case r == '$' ||
|
||||
r == '&' ||
|
||||
r == '+' ||
|
||||
r == ',' ||
|
||||
r == '/' ||
|
||||
r == ':' ||
|
||||
r == ';' ||
|
||||
r == '=' ||
|
||||
r == '?' ||
|
||||
r == '@':
|
||||
buf.WriteByte('%')
|
||||
buf.WriteByte(hexConstUpper[byte(r)>>4])
|
||||
buf.WriteByte(hexConstUpper[byte(r)&15])
|
||||
default:
|
||||
converted, size, err = transform.String(encoder, string(r))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
buf.WriteByte('%')
|
||||
buf.WriteByte(hexConstUpper[(converted[0]>>4)&0xf])
|
||||
buf.WriteByte(hexConstUpper[converted[0]&0xf])
|
||||
if size > 1 {
|
||||
buf.WriteByte('%')
|
||||
buf.WriteByte(hexConstUpper[(converted[1]>>4)&0xf])
|
||||
buf.WriteByte(hexConstUpper[converted[1]&0xf])
|
||||
}
|
||||
}
|
||||
}
|
||||
converted = buf.String()
|
||||
return
|
||||
}
|
||||
|
||||
// ConvertFromEUCKRUrlEncoded is translate method from euc-kr encoded string to utf-8 encoded string.
|
||||
func ConvertFromEUCKRUrlEncoded(str string) (converted string, err error) {
|
||||
dst, err := url.QueryUnescape(str)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
converted, _, err = transform.String(korean.EUCKR.NewDecoder(), dst)
|
||||
return
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
package han
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConvertToEUCKRUrlEncoded(t *testing.T) {
|
||||
|
||||
// result string get from http://code.cside.com/3rdpage/us/url/converter.html
|
||||
cases := []struct {
|
||||
in string
|
||||
out string
|
||||
}{
|
||||
{
|
||||
"가나다 abc ABC 123 ~!@#$%^&*()_+`-=[]{}|;':,./<>?",
|
||||
"%B0%A1%B3%AA%B4%D9+abc+ABC+123+%7E%21%40%23%24%25%5E%26%2A%28%29_%2B%60-%3D%5B%5D%7B%7D%7C%3B%27%3A%2C.%2F%3C%3E%3F",
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
result, err := ConvertToEUCKRUrlEncoded(c.in)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
} else if result != c.out {
|
||||
t.Errorf("src=%s\nresult1=%s\nresult2=%s\n", c.in, c.out, result)
|
||||
} else {
|
||||
t.Log("OK")
|
||||
}
|
||||
result2, err := ConvertFromEUCKRUrlEncoded(result)
|
||||
if err != nil {
|
||||
t.Error("decode err", err)
|
||||
} else {
|
||||
if result2 != c.in {
|
||||
t.Error("decode err2", result2, c.in)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
package han
|
||||
|
||||
import (
|
||||
"amuz.es/src/go/misc/strutil"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
koreanPluralizeSubunit = 10
|
||||
koreanPluralizeUnit = 10000
|
||||
zeroChr = '0'
|
||||
)
|
||||
|
||||
var (
|
||||
koreanPluralizeUnitLabels = []string{"", "만", "억", "조"}
|
||||
koreanPluralizeSubunitLabels = []string{"", "십", "백", "천"}
|
||||
)
|
||||
|
||||
func transKoreanPluralizeSubunit(number int) (out string) {
|
||||
if number == 0 {
|
||||
return "0"
|
||||
}
|
||||
var (
|
||||
buf strings.Builder
|
||||
digit int
|
||||
)
|
||||
for radix := 0; number > 0; radix++ {
|
||||
digit, number = number%koreanPluralizeSubunit, number/koreanPluralizeSubunit
|
||||
if digit > 0 {
|
||||
buf.WriteString(koreanPluralizeSubunitLabels[radix%len(koreanPluralizeSubunitLabels)])
|
||||
}
|
||||
if digit > 1 || (digit == 1 && radix == 0) {
|
||||
buf.WriteByte(zeroChr + byte(digit))
|
||||
}
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func transKoreanPluralizeUnit(number int, subunitmapper func(int) string, omitLastOne bool) (out string) {
|
||||
if number == 0 {
|
||||
return "0"
|
||||
}
|
||||
var (
|
||||
buf strings.Builder
|
||||
digit, lastDigit int
|
||||
)
|
||||
for radix := 0; number > 0; radix++ {
|
||||
digit = number % koreanPluralizeUnit
|
||||
number = number / koreanPluralizeUnit
|
||||
lastDigit = digit % koreanPluralizeSubunit
|
||||
if digit > 0 {
|
||||
buf.WriteString(koreanPluralizeUnitLabels[radix%len(koreanPluralizeUnitLabels)])
|
||||
}
|
||||
// 1 뭉개기
|
||||
if omitLastOne && radix > 0 && lastDigit == 1 {
|
||||
digit--
|
||||
}
|
||||
//나머지 처리
|
||||
if digit > 1 || (digit == 1 && (radix == 0 || !omitLastOne)) {
|
||||
buf.WriteString(subunitmapper(digit))
|
||||
}
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// KoreanPluralizeSubunit is a function for translating into Korean after omitting the most significant digit when the most significant digit is 1.
|
||||
func KoreanPluralizeSubunit(number int) (out string) {
|
||||
out = strutil.Reverse(transKoreanPluralizeSubunit(number))
|
||||
return
|
||||
}
|
||||
|
||||
// KoreanPluralizeUnit is a function for translating into Korean after omitting the most significant digit when the most significant digit is 1.
|
||||
func KoreanPluralizeUnit(number int) (out string) {
|
||||
out = strutil.Reverse(transKoreanPluralizeUnit(number, transKoreanPluralizeSubunit, true))
|
||||
return
|
||||
}
|
||||
|
||||
// KoreanPluralizeUnitType2 function is a function that translates only each 10,000 units into Korean.
|
||||
func KoreanPluralizeUnitType2(number int) (out string) {
|
||||
out = strutil.Reverse(transKoreanPluralizeUnit(number, strutil.FormatIntToStringReversed, false))
|
||||
return
|
||||
}
|
||||
|
||||
//KoreanPluralizeUnitType3 is a function for translating into Korean.
|
||||
func KoreanPluralizeUnitType3(number int) (out string) {
|
||||
if number == 0 {
|
||||
return "0"
|
||||
}
|
||||
var (
|
||||
below10thousand = number % koreanPluralizeUnit
|
||||
above10thousand = (number / koreanPluralizeUnit) * koreanPluralizeUnit
|
||||
outBelow10thousand, outAbove10thousand string
|
||||
)
|
||||
|
||||
if below10thousand > 0 {
|
||||
outBelow10thousand = strutil.FormatIntToStringReversed(below10thousand)
|
||||
}
|
||||
if above10thousand > 0 {
|
||||
outAbove10thousand = transKoreanPluralizeUnit(above10thousand, transKoreanPluralizeSubunit, false)
|
||||
}
|
||||
out = strutil.Reverse(outBelow10thousand + outAbove10thousand)
|
||||
return
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,2 @@
|
|||
// Package han contains utility functions for regional issues in Korea.
|
||||
package han
|
|
@ -0,0 +1,2 @@
|
|||
// Package misc contains Miscellaneous utility collections.
|
||||
package misc // import "amuz.es/src/go/misc"
|
|
@ -0,0 +1,8 @@
|
|||
package misc
|
||||
|
||||
import json "github.com/json-iterator/go"
|
||||
|
||||
var (
|
||||
// JSONCodec is a static JSON serializer and JSON deserializer handler.
|
||||
JSONCodec = json.ConfigFastest
|
||||
)
|
|
@ -0,0 +1,2 @@
|
|||
// Package monetary contain monetary related data structures.
|
||||
package monetary
|
|
@ -0,0 +1,125 @@
|
|||
package monetary
|
||||
|
||||
import (
|
||||
"github.com/ericlagergren/decimal"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
// MonthlyInstallmentInterestAmount returns monthly interest amount from annual interest ratio.
|
||||
func MonthlyInstallmentInterestAmount(balance *decimal.Big, annualInterest *big.Rat) (fee *decimal.Big) {
|
||||
|
||||
var (
|
||||
monthly big.Rat
|
||||
feeFrac big.Rat
|
||||
)
|
||||
monthly.SetFrac64(1, 12)
|
||||
balance.Rat(&feeFrac)
|
||||
feeFrac.Mul(&feeFrac, annualInterest)
|
||||
feeFrac.Mul(&feeFrac, &monthly)
|
||||
fee = decimal.New(0, 0)
|
||||
fee.Context.RoundingMode = decimal.ToZero
|
||||
fee.SetRat(&feeFrac)
|
||||
fee.Quantize(0)
|
||||
fee.Reduce()
|
||||
return fee
|
||||
}
|
||||
|
||||
// MonthlyInstallmentInfo returns monthly repayment plan.
|
||||
func MonthlyInstallmentInfo(totalBalance *decimal.Big, modDigit, division int) (installment, installmentFractional, installmentExtra *decimal.Big) {
|
||||
var (
|
||||
monthlyFee big.Rat
|
||||
frac big.Rat
|
||||
divisionDeci decimal.Big
|
||||
)
|
||||
|
||||
divisionDeci.SetMantScale(int64(division), 0)
|
||||
totalBalance.Rat(&monthlyFee)
|
||||
frac.SetFrac64(1, int64(division))
|
||||
monthlyFee.Mul(&monthlyFee, &frac)
|
||||
|
||||
installment = decimal.New(0, 0)
|
||||
installment.Context.RoundingMode = decimal.ToZero
|
||||
installment.SetRat(&monthlyFee)
|
||||
installment.Quantize(-modDigit)
|
||||
installment.Reduce()
|
||||
|
||||
installmentFractional = decimal.New(0, 0)
|
||||
installmentFractional.Mul(installment, &divisionDeci)
|
||||
installmentFractional.Sub(totalBalance, installmentFractional)
|
||||
installmentFractional.Reduce()
|
||||
|
||||
installmentExtra = decimal.New(0, 0)
|
||||
installmentExtra.Add(installment, installmentFractional)
|
||||
installmentExtra.Reduce()
|
||||
|
||||
installmentTest := decimal.New(0, 0)
|
||||
installmentTest.Mul(installment, &divisionDeci)
|
||||
installmentTest.Add(installmentTest, installmentFractional)
|
||||
return
|
||||
}
|
||||
|
||||
// MonthlyInstallmentSchedule returns detailed monthly repayment plan
|
||||
func MonthlyInstallmentSchedule(
|
||||
totalBalance *decimal.Big,
|
||||
annualInterest *decimal.Big,
|
||||
modDigit, division int,
|
||||
payExtraAmountEarlier bool) (
|
||||
installment, installmentExtra, totalInterests *decimal.Big,
|
||||
principleBalanceBeforePayments,
|
||||
principleBalanceAfterPayments,
|
||||
installments,
|
||||
interests,
|
||||
schedules []*decimal.Big,
|
||||
) {
|
||||
if division < 1 {
|
||||
return
|
||||
} else if totalBalance == nil {
|
||||
return
|
||||
}
|
||||
var (
|
||||
leftBalance decimal.Big
|
||||
annualInterestFrac big.Rat
|
||||
)
|
||||
|
||||
if annualInterest != nil {
|
||||
annualInterest.Rat(&annualInterestFrac)
|
||||
}
|
||||
|
||||
totalInterests = decimal.New(0, 0)
|
||||
leftBalance.Copy(totalBalance)
|
||||
installmentAmount, _, installmentAmountExtra := MonthlyInstallmentInfo(totalBalance, modDigit, division)
|
||||
principleBalanceBeforePayments, principleBalanceAfterPayments, installments, interests, schedules = make([]*decimal.Big, 0, division), make([]*decimal.Big, 0, division), make([]*decimal.Big, 0, division), make([]*decimal.Big, 0, division), make([]*decimal.Big, 0, division)
|
||||
for i := 0; i < division; i++ {
|
||||
var (
|
||||
interestAmount = MonthlyInstallmentInterestAmount(&leftBalance, &annualInterestFrac)
|
||||
monthlyInstallmentWithInterest decimal.Big
|
||||
principleBalanceBeforePayment decimal.Big
|
||||
principleBalanceAfterPayment decimal.Big
|
||||
paymentAmount *decimal.Big
|
||||
)
|
||||
if payExtraAmountEarlier {
|
||||
if i == 0 {
|
||||
paymentAmount = installmentAmountExtra
|
||||
} else {
|
||||
paymentAmount = installmentAmount
|
||||
}
|
||||
} else {
|
||||
if i+1 == division {
|
||||
paymentAmount = installmentAmountExtra
|
||||
} else {
|
||||
paymentAmount = installmentAmount
|
||||
}
|
||||
}
|
||||
installments = append(installments, paymentAmount)
|
||||
interests = append(interests, interestAmount)
|
||||
totalInterests.Add(totalInterests, interestAmount)
|
||||
principleBalanceBeforePayment.Copy(&leftBalance)
|
||||
principleBalanceBeforePayments = append(principleBalanceBeforePayments, &principleBalanceBeforePayment)
|
||||
monthlyInstallmentWithInterest.Add(paymentAmount, interestAmount)
|
||||
leftBalance.Sub(&leftBalance, paymentAmount)
|
||||
principleBalanceAfterPayment.Copy(&leftBalance)
|
||||
principleBalanceAfterPayments = append(principleBalanceAfterPayments, &principleBalanceAfterPayment)
|
||||
schedules = append(schedules, &monthlyInstallmentWithInterest)
|
||||
}
|
||||
return
|
||||
}
|
|
@ -0,0 +1,196 @@
|
|||
package monetary
|
||||
|
||||
import (
|
||||
"github.com/ericlagergren/decimal"
|
||||
"math/big"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestInstallment(t *testing.T) {
|
||||
var (
|
||||
charge decimal.Big
|
||||
annualInterest decimal.Big
|
||||
annualInterestFrac big.Rat
|
||||
annualInterestFracPercentage big.Rat
|
||||
modDigit = 2
|
||||
division = 36
|
||||
)
|
||||
charge.SetUint64(10_000_000)
|
||||
annualInterest.SetMantScale(125, 3)
|
||||
annualInterest.Rat(&annualInterestFrac)
|
||||
annualInterestFracPercentage.Mul(&annualInterestFrac, big.NewRat(100, 1))
|
||||
chargeInt, _ := charge.Int64()
|
||||
|
||||
func() {
|
||||
interestFee := MonthlyInstallmentInterestAmount(&charge, &annualInterestFrac)
|
||||
interestFeeInt, _ := interestFee.Int64()
|
||||
t.Logf("할부원금 %d 년이율 %s%% 납부 이자 %d",
|
||||
chargeInt,
|
||||
annualInterestFracPercentage.FloatString(2),
|
||||
interestFeeInt,
|
||||
)
|
||||
}()
|
||||
|
||||
func() {
|
||||
installment, installmentFractional, installmentWithFractional := MonthlyInstallmentInfo(&charge, modDigit, division)
|
||||
installmentInt, _ := installment.Int64()
|
||||
installmentFractionalInt, _ := installmentFractional.Int64()
|
||||
installmentWithFractionalInt, _ := installmentWithFractional.Int64()
|
||||
t.Logf("할부총원금 %d 월 할부 절사자리수 %d, %d 개월동안, 월납입액 %d, 우수리 %d 우수리 포함 월납입액 %d",
|
||||
chargeInt,
|
||||
modDigit,
|
||||
division,
|
||||
installmentInt,
|
||||
installmentFractionalInt,
|
||||
installmentWithFractionalInt,
|
||||
)
|
||||
}()
|
||||
func() {
|
||||
payExtraAmountEarlier := true
|
||||
_, _, totalInterests,
|
||||
principleBalanceBeforePayments,
|
||||
principleBalanceAfterPayments,
|
||||
installments,
|
||||
interests,
|
||||
schedules := MonthlyInstallmentSchedule(&charge, nil, modDigit, division, payExtraAmountEarlier)
|
||||
totalInterestsInt, _ := totalInterests.Int64()
|
||||
|
||||
t.Logf("할부총원금 %d 년이율 %s%%, 월 할부 절사자리수 %d, %d 개월동안, 우수리 선납 %v 총 납부 이자 %d",
|
||||
chargeInt,
|
||||
"0",
|
||||
modDigit,
|
||||
division,
|
||||
payExtraAmountEarlier,
|
||||
totalInterestsInt,
|
||||
)
|
||||
for i := 0; i < division; i++ {
|
||||
principleBalanceBeforePayments, principleBalanceAfterPayments, installment, interest, schedule := principleBalanceBeforePayments[i], principleBalanceAfterPayments[i], installments[i], interests[i], schedules[i]
|
||||
principleBalanceBeforePaymentsInt, _ := principleBalanceBeforePayments.Int64()
|
||||
installmentInt, _ := installment.Int64()
|
||||
interestInt, _ := interest.Int64()
|
||||
scheduleInt, _ := schedule.Int64()
|
||||
principleBalanceAfterPaymentsInt, _ := principleBalanceAfterPayments.Int64()
|
||||
|
||||
t.Logf("%d번쨰 할부 납부 도래시, 납부전 할부원금 %d -> 월총금액 (월납입액 %d + 할부수수료 %d)=%d -> 납부 할부원금 %d",
|
||||
i+1,
|
||||
principleBalanceBeforePaymentsInt,
|
||||
installmentInt,
|
||||
interestInt,
|
||||
scheduleInt,
|
||||
principleBalanceAfterPaymentsInt,
|
||||
)
|
||||
}
|
||||
}()
|
||||
|
||||
func() {
|
||||
payExtraAmountEarlier := false
|
||||
_, _, totalInterests,
|
||||
principleBalanceBeforePayments,
|
||||
principleBalanceAfterPayments,
|
||||
installments,
|
||||
interests,
|
||||
schedules := MonthlyInstallmentSchedule(&charge, nil, modDigit, division, payExtraAmountEarlier)
|
||||
totalInterestsInt, _ := totalInterests.Int64()
|
||||
|
||||
t.Logf("할부총원금 %d 년이율 %s%%, 월 할부 절사자리수 %d, %d 개월동안, 우수리 선납 %v 총 납부 이자 %d",
|
||||
chargeInt,
|
||||
"0",
|
||||
modDigit,
|
||||
division,
|
||||
payExtraAmountEarlier,
|
||||
totalInterestsInt,
|
||||
)
|
||||
for i := 0; i < division; i++ {
|
||||
principleBalanceBeforePayments, principleBalanceAfterPayments, installment, interest, schedule := principleBalanceBeforePayments[i], principleBalanceAfterPayments[i], installments[i], interests[i], schedules[i]
|
||||
principleBalanceBeforePaymentsInt, _ := principleBalanceBeforePayments.Int64()
|
||||
installmentInt, _ := installment.Int64()
|
||||
interestInt, _ := interest.Int64()
|
||||
scheduleInt, _ := schedule.Int64()
|
||||
principleBalanceAfterPaymentsInt, _ := principleBalanceAfterPayments.Int64()
|
||||
|
||||
t.Logf("%d번쨰 할부 납부 도래시, 납부전 할부원금 %d -> 월총금액 (월납입액 %d + 할부수수료 %d)=%d -> 납부 할부원금 %d",
|
||||
i+1,
|
||||
principleBalanceBeforePaymentsInt,
|
||||
installmentInt,
|
||||
interestInt,
|
||||
scheduleInt,
|
||||
principleBalanceAfterPaymentsInt,
|
||||
)
|
||||
}
|
||||
}()
|
||||
|
||||
func() {
|
||||
payExtraAmountEarlier := true
|
||||
_, _, totalInterests,
|
||||
principleBalanceBeforePayments,
|
||||
principleBalanceAfterPayments,
|
||||
installments,
|
||||
interests,
|
||||
schedules := MonthlyInstallmentSchedule(&charge, &annualInterest, modDigit, division, payExtraAmountEarlier)
|
||||
totalInterestsInt, _ := totalInterests.Int64()
|
||||
|
||||
t.Logf("할부총원금 %d 년이율 %s%%, 월 할부 절사자리수 %d, %d 개월동안, 우수리 선납 %v 총 납부 이자 %d",
|
||||
chargeInt,
|
||||
annualInterestFracPercentage.FloatString(2),
|
||||
modDigit,
|
||||
division,
|
||||
payExtraAmountEarlier,
|
||||
totalInterestsInt,
|
||||
)
|
||||
for i := 0; i < division; i++ {
|
||||
principleBalanceBeforePayments, principleBalanceAfterPayments, installment, interest, schedule := principleBalanceBeforePayments[i], principleBalanceAfterPayments[i], installments[i], interests[i], schedules[i]
|
||||
principleBalanceBeforePaymentsInt, _ := principleBalanceBeforePayments.Int64()
|
||||
installmentInt, _ := installment.Int64()
|
||||
interestInt, _ := interest.Int64()
|
||||
scheduleInt, _ := schedule.Int64()
|
||||
principleBalanceAfterPaymentsInt, _ := principleBalanceAfterPayments.Int64()
|
||||
|
||||
t.Logf("%d번쨰 할부 납부 도래시, 납부전 할부원금 %d -> 월총금액 (월납입액 %d + 할부수수료 %d)=%d -> 납부 할부원금 %d",
|
||||
i+1,
|
||||
principleBalanceBeforePaymentsInt,
|
||||
installmentInt,
|
||||
interestInt,
|
||||
scheduleInt,
|
||||
principleBalanceAfterPaymentsInt,
|
||||
)
|
||||
}
|
||||
}()
|
||||
|
||||
func() {
|
||||
payExtraAmountEarlier := false
|
||||
_, _, totalInterests,
|
||||
principleBalanceBeforePayments,
|
||||
principleBalanceAfterPayments,
|
||||
installments,
|
||||
interests,
|
||||
schedules := MonthlyInstallmentSchedule(&charge, &annualInterest, modDigit, division, payExtraAmountEarlier)
|
||||
totalInterestsInt, _ := totalInterests.Int64()
|
||||
|
||||
t.Logf("할부총원금 %d 년이율 %s%%, 월 할부 절사자리수 %d, %d 개월동안, 우수리 선납 %v 총 납부 이자 %d",
|
||||
chargeInt,
|
||||
annualInterestFracPercentage.FloatString(2),
|
||||
modDigit,
|
||||
division,
|
||||
payExtraAmountEarlier,
|
||||
totalInterestsInt,
|
||||
)
|
||||
for i := 0; i < division; i++ {
|
||||
principleBalanceBeforePayments, principleBalanceAfterPayments, installment, interest, schedule := principleBalanceBeforePayments[i], principleBalanceAfterPayments[i], installments[i], interests[i], schedules[i]
|
||||
principleBalanceBeforePaymentsInt, _ := principleBalanceBeforePayments.Int64()
|
||||
installmentInt, _ := installment.Int64()
|
||||
interestInt, _ := interest.Int64()
|
||||
scheduleInt, _ := schedule.Int64()
|
||||
principleBalanceAfterPaymentsInt, _ := principleBalanceAfterPayments.Int64()
|
||||
|
||||
t.Logf("%d번쨰 할부 납부 도래시, 납부전 할부원금 %d -> 월총금액 (월납입액 %d + 할부수수료 %d)=%d -> 납부 할부원금 %d",
|
||||
i+1,
|
||||
principleBalanceBeforePaymentsInt,
|
||||
installmentInt,
|
||||
interestInt,
|
||||
scheduleInt,
|
||||
principleBalanceAfterPaymentsInt,
|
||||
)
|
||||
}
|
||||
}()
|
||||
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
package networking
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/pkg/errors"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// RoundrobinDialer is a wrapper for the DialContext function.
|
||||
type (
|
||||
RoundrobinDialer struct {
|
||||
Dialer *net.Dialer
|
||||
FallbackHost net.TCPAddr
|
||||
}
|
||||
)
|
||||
|
||||
// DialContext is a connector method with Fail-Over approach.
|
||||
func (d *RoundrobinDialer) DialContext(ctx context.Context, network string, hosts ...string) (net.Conn, error) {
|
||||
list := rand.Perm(len(hosts))
|
||||
for _, idx := range list {
|
||||
host := hosts[idx]
|
||||
for _, resolvedIP := range d.resolveHost(ctx, network, host) {
|
||||
var (
|
||||
conn net.Conn
|
||||
err error
|
||||
)
|
||||
if conn, err = d.Dialer.DialContext(ctx, resolvedIP.Network(), resolvedIP.AddrPort().String()); err != nil {
|
||||
log.Printf("failed to connected to %s => %s : %s", host, resolvedIP.String(), err)
|
||||
} else {
|
||||
return conn, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !d.FallbackHost.IP.IsUnspecified() {
|
||||
log.Printf("attempting to connect fallback address(%s)\n", d.FallbackHost.String())
|
||||
return d.Dialer.DialContext(ctx, d.FallbackHost.Network(), d.FallbackHost.AddrPort().String())
|
||||
}
|
||||
return nil, errors.New("name resolve failure")
|
||||
}
|
||||
|
||||
// resolveHost is actually resolve network addresses from a given hostname.
|
||||
func (d *RoundrobinDialer) resolveHost(ctx context.Context, network, connectAddr string) (addrs []net.TCPAddr) {
|
||||
log.Printf("attempting to connect %s", connectAddr)
|
||||
var (
|
||||
host string
|
||||
portString string
|
||||
resolvedIPs []net.IP
|
||||
tcpPort int
|
||||
err error
|
||||
)
|
||||
if host, portString, err = net.SplitHostPort(connectAddr); err != nil {
|
||||
log.Printf("invalid connection string format: %s", err)
|
||||
return
|
||||
} else if port, _ := strconv.ParseInt(portString, 10, 32); port < 0 || port > 65535 {
|
||||
log.Printf("invalid port format : %s", portString)
|
||||
return
|
||||
} else {
|
||||
tcpPort = int(port)
|
||||
}
|
||||
|
||||
if resolvedIPs, err = d.Dialer.Resolver.LookupIP(ctx, network, host); err != nil {
|
||||
log.Printf("cannot resolve host %s : %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
addrs = make([]net.TCPAddr, 0, len(resolvedIPs))
|
||||
for _, resolvedIP := range resolvedIPs {
|
||||
addrs = append(addrs, net.TCPAddr{IP: resolvedIP, Port: tcpPort})
|
||||
}
|
||||
return
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
// Package networking is simple network utility collections.
|
||||
package networking
|
|
@ -0,0 +1,57 @@
|
|||
package networking
|
||||
|
||||
import (
|
||||
"net"
|
||||
)
|
||||
|
||||
// Bigger than we need, not too big to worry about overflow
|
||||
const big = 0x100
|
||||
|
||||
// Parse IPv4 address (d.d.d.d).
|
||||
func ParseIPv4(s []byte) net.IP {
|
||||
var p [net.IPv4len]byte
|
||||
for i := 0; i < net.IPv4len; i++ {
|
||||
if len(s) == 0 {
|
||||
// Missing octets.
|
||||
return nil
|
||||
}
|
||||
|
||||
if i > 0 {
|
||||
if s[0] != '.' {
|
||||
return nil
|
||||
}
|
||||
s = s[1:]
|
||||
}
|
||||
|
||||
n, c, ok := dtoi(s)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
if c > 1 && s[0] == '0' {
|
||||
// Reject non-zero components with leading zeroes.
|
||||
return nil
|
||||
}
|
||||
s = s[c:]
|
||||
p[i] = byte(n)
|
||||
}
|
||||
if len(s) != 0 {
|
||||
return nil
|
||||
}
|
||||
return net.IPv4(p[0], p[1], p[2], p[3])
|
||||
}
|
||||
|
||||
// Decimal to integer.
|
||||
// Returns number, characters consumed, success.
|
||||
func dtoi(s []byte) (n int, i int, ok bool) {
|
||||
n = 0
|
||||
for i = 0; i < len(s) && '0' <= s[i] && s[i] <= '9'; i++ {
|
||||
n = n*10 + int(s[i]-'0')
|
||||
if n >= big {
|
||||
return big, i, false
|
||||
}
|
||||
}
|
||||
if i == 0 {
|
||||
return 0, 0, false
|
||||
}
|
||||
return n, i, true
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package networking
|
||||
|
||||
import "net"
|
||||
|
||||
// LocalIP returns the non loopback local IP of the host
|
||||
func LocalIP() string {
|
||||
addrs, err := net.InterfaceAddrs()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
for _, address := range addrs {
|
||||
// check the address type and if it is not a loopback the display it
|
||||
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
|
||||
if ipnet.IP.To4() != nil {
|
||||
return ipnet.IP.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ResolveIp is resolve a network address from a given hostname with default resolver.
|
||||
func ResolveIp(name string) (net.IP, error) {
|
||||
if addrs, err := net.ResolveIPAddr("ip4", name); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return addrs.IP, nil
|
||||
}
|
||||
}
|
||||
|
||||
func ExtractIp(remoteAddr net.Addr) string {
|
||||
if addr, ok := remoteAddr.(*net.TCPAddr); ok {
|
||||
return addr.IP.String()
|
||||
} else {
|
||||
return remoteAddr.String()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package nums
|
||||
|
||||
import (
|
||||
"github.com/ericlagergren/decimal"
|
||||
)
|
||||
|
||||
// BigSeperateFractionalPotions decompose a decimal number with a sign, integer, and fractional parts.
|
||||
func BigSeperateFractionalPotions(src *decimal.Big) (sign bool, integer, fractional uint64, fractionalLength uint8) {
|
||||
var num, integerPart decimal.Big
|
||||
num.Copy(src)
|
||||
num.Context.RoundingMode = decimal.ToZero
|
||||
sign = num.Signbit()
|
||||
integer, _ = num.Abs(&num).Uint64()
|
||||
integerPart.SetUint64(integer)
|
||||
scale := num.Sub(&num, &integerPart).Quantize(6).Reduce().Scale()
|
||||
fractional, _ = num.Mul(&num, decimal.New(1, -scale)).Reduce().Uint64()
|
||||
fractionalLength = uint8(scale)
|
||||
return
|
||||
}
|
||||
|
||||
// BigMergeFractionalPotions compose a decimal number with a sign, integer, and fractional parts.
|
||||
func BigMergeFractionalPotions(sign bool, integer, fractional uint64, fractionalLength uint8) (dst *decimal.Big) {
|
||||
var num, frac decimal.Big
|
||||
num.SetUint64(integer)
|
||||
frac.SetUint64(fractional)
|
||||
dst = &num
|
||||
|
||||
num.Mul(&num, decimal.New(1, -int(fractionalLength))).Add(&num, &frac).SetScale(int(fractionalLength))
|
||||
|
||||
if sign {
|
||||
num.Neg(&num)
|
||||
}
|
||||
num.Reduce()
|
||||
return
|
||||
}
|
|
@ -0,0 +1,262 @@
|
|||
package nums
|
||||
|
||||
import (
|
||||
"github.com/ericlagergren/decimal"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBigSeperateFractionalPotions(t *testing.T) {
|
||||
type args struct {
|
||||
src *decimal.Big
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantSign bool
|
||||
wantInteger uint64
|
||||
wantFractional uint64
|
||||
wantFractionalLength uint8
|
||||
}{
|
||||
{
|
||||
"11.111",
|
||||
args{
|
||||
decimal.New(11111, 3),
|
||||
},
|
||||
false,
|
||||
11,
|
||||
111,
|
||||
3,
|
||||
},
|
||||
{
|
||||
"0.111",
|
||||
args{
|
||||
decimal.New(111, 3),
|
||||
},
|
||||
false,
|
||||
0,
|
||||
111,
|
||||
3,
|
||||
},
|
||||
{
|
||||
"11",
|
||||
args{
|
||||
decimal.New(11000, 3),
|
||||
},
|
||||
false,
|
||||
11,
|
||||
0,
|
||||
0,
|
||||
},
|
||||
{
|
||||
"10.01",
|
||||
args{
|
||||
decimal.New(10010, 3),
|
||||
},
|
||||
false,
|
||||
10,
|
||||
1,
|
||||
2,
|
||||
},
|
||||
{
|
||||
"0.013",
|
||||
args{
|
||||
decimal.New(13, 3),
|
||||
},
|
||||
false,
|
||||
0,
|
||||
13,
|
||||
3,
|
||||
},
|
||||
{
|
||||
"-11.111",
|
||||
args{
|
||||
decimal.New(-11111, 3),
|
||||
},
|
||||
true,
|
||||
11,
|
||||
111,
|
||||
3,
|
||||
},
|
||||
{
|
||||
"-0.111",
|
||||
args{
|
||||
decimal.New(-111, 3),
|
||||
},
|
||||
true,
|
||||
0,
|
||||
111,
|
||||
3,
|
||||
},
|
||||
{
|
||||
"-11",
|
||||
args{
|
||||
decimal.New(-11000, 3),
|
||||
},
|
||||
true,
|
||||
11,
|
||||
0,
|
||||
0,
|
||||
},
|
||||
{
|
||||
"-10.01",
|
||||
args{
|
||||
decimal.New(-10010, 3),
|
||||
},
|
||||
true,
|
||||
10,
|
||||
1,
|
||||
2,
|
||||
},
|
||||
{
|
||||
"-0.013",
|
||||
args{
|
||||
decimal.New(-13, 3),
|
||||
},
|
||||
true,
|
||||
0,
|
||||
13,
|
||||
3,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotSign, gotInteger, gotFractional, gotFractionalLength := BigSeperateFractionalPotions(tt.args.src)
|
||||
t.Logf("BigSeperateFractionalPotions() sign =%v, integer = %v, fractional = %v, fractionalLength = %v", gotSign, gotInteger, gotFractional, gotFractionalLength)
|
||||
if gotSign != tt.wantSign {
|
||||
t.Errorf("BigSeperateFractionalPotions() gotSign = %v, want %v", gotSign, tt.wantSign)
|
||||
}
|
||||
if gotInteger != tt.wantInteger {
|
||||
t.Errorf("BigSeperateFractionalPotions() gotInteger = %v, want %v", gotInteger, tt.wantInteger)
|
||||
}
|
||||
if gotFractional != tt.wantFractional {
|
||||
t.Errorf("BigSeperateFractionalPotions() gotFractional = %v, want %v", gotFractional, tt.wantFractional)
|
||||
}
|
||||
if gotFractionalLength != tt.wantFractionalLength {
|
||||
t.Errorf("BigSeperateFractionalPotions() gotFractionalLength = %v, want %v", gotFractionalLength, tt.wantFractionalLength)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBigMergeFractionalPotions(t *testing.T) {
|
||||
type args struct {
|
||||
sign bool
|
||||
integer uint64
|
||||
fractional uint64
|
||||
fractionalLength uint8
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantDst *decimal.Big
|
||||
}{
|
||||
{
|
||||
"11.111",
|
||||
args{
|
||||
false,
|
||||
11,
|
||||
111,
|
||||
3,
|
||||
},
|
||||
decimal.New(11111, 3),
|
||||
},
|
||||
{
|
||||
"0.111",
|
||||
args{
|
||||
false,
|
||||
0,
|
||||
111,
|
||||
3,
|
||||
},
|
||||
decimal.New(111, 3),
|
||||
},
|
||||
{
|
||||
"11",
|
||||
args{
|
||||
false,
|
||||
11,
|
||||
0,
|
||||
0,
|
||||
},
|
||||
decimal.New(11, 0),
|
||||
},
|
||||
{
|
||||
"10.01",
|
||||
args{
|
||||
false,
|
||||
10,
|
||||
1,
|
||||
2,
|
||||
},
|
||||
decimal.New(1001, 2),
|
||||
},
|
||||
{
|
||||
"0.013",
|
||||
args{
|
||||
false,
|
||||
0,
|
||||
13,
|
||||
3,
|
||||
},
|
||||
decimal.New(13, 3),
|
||||
},
|
||||
{
|
||||
"-11.111",
|
||||
args{
|
||||
true,
|
||||
11,
|
||||
111,
|
||||
3,
|
||||
},
|
||||
decimal.New(-11111, 3),
|
||||
},
|
||||
{
|
||||
"-0.111",
|
||||
args{
|
||||
true,
|
||||
0,
|
||||
111,
|
||||
3,
|
||||
},
|
||||
decimal.New(-111, 3),
|
||||
},
|
||||
{
|
||||
"-11",
|
||||
args{
|
||||
true,
|
||||
11,
|
||||
0,
|
||||
0,
|
||||
},
|
||||
decimal.New(-11, 0),
|
||||
},
|
||||
{
|
||||
"-10.01",
|
||||
args{
|
||||
true,
|
||||
10,
|
||||
1,
|
||||
2,
|
||||
},
|
||||
decimal.New(-1001, 2),
|
||||
},
|
||||
{
|
||||
"-0.013",
|
||||
args{
|
||||
true,
|
||||
0,
|
||||
13,
|
||||
3,
|
||||
},
|
||||
decimal.New(-13, 3),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if gotDst := BigMergeFractionalPotions(tt.args.sign, tt.args.integer, tt.args.fractional, tt.args.fractionalLength); !reflect.DeepEqual(gotDst, tt.wantDst) {
|
||||
t.Errorf("BigMergeFractionalPotions() = %v, want %v", gotDst, tt.wantDst)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
package nums
|
||||
|
||||
import (
|
||||
"amuz.es/src/go/misc/strutil"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SplitSignFromInt64 decomposes a signed integer with a sign and positive integer.
|
||||
func SplitSignFromInt64(src int64) (sign bool, dst uint64) {
|
||||
signedInteger := uint64(src)
|
||||
sign = 1 == (signedInteger >> 63)
|
||||
|
||||
//Two's complement!!
|
||||
if sign {
|
||||
dst = 1 + (signedInteger ^ (1<<64 - 1))
|
||||
} else {
|
||||
dst = signedInteger & (1<<63 - 1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// MergeSignFromUInt64 composes a signed integer with a sign and positive integer.
|
||||
func MergeSignFromUInt64(sign bool, src uint64) (dst int64) {
|
||||
//Two's complement!!
|
||||
if sign {
|
||||
dst = -int64(src & (1<<63 - 1))
|
||||
} else {
|
||||
dst = int64(src & (1<<63 - 1))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// FractionalStringify is a formatting function for a fractional part number.
|
||||
func FractionalStringify(fractional, fractionalLength int, showPoint bool) (formatted string) {
|
||||
if fractionalLength == 0 {
|
||||
return
|
||||
}
|
||||
var builder strings.Builder
|
||||
|
||||
partialFormatted := strutil.FormatIntToStringReversed(fractional)
|
||||
if leftDigits := fractionalLength - len(partialFormatted); leftDigits < 0 {
|
||||
builder.WriteString(partialFormatted[-leftDigits:])
|
||||
} else {
|
||||
builder.WriteString(partialFormatted)
|
||||
for ; leftDigits > 0; leftDigits-- {
|
||||
builder.WriteByte('0')
|
||||
}
|
||||
}
|
||||
|
||||
if showPoint {
|
||||
builder.WriteByte('.')
|
||||
}
|
||||
|
||||
return strutil.Reverse(builder.String())
|
||||
}
|
|
@ -0,0 +1,194 @@
|
|||
package nums
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSplitSignFromInt64(t *testing.T) {
|
||||
type args struct {
|
||||
src int64
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantSign bool
|
||||
wantDst uint64
|
||||
}{
|
||||
{
|
||||
"0",
|
||||
args{0},
|
||||
false, 0,
|
||||
},
|
||||
{
|
||||
"-0",
|
||||
args{-0},
|
||||
false, 0,
|
||||
},
|
||||
{
|
||||
"-223",
|
||||
args{-223},
|
||||
true, 223,
|
||||
},
|
||||
{
|
||||
"112",
|
||||
args{112},
|
||||
false, 112,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotSign, gotDst := SplitSignFromInt64(tt.args.src)
|
||||
if gotSign != tt.wantSign {
|
||||
t.Errorf("SplitSignFromInt64() gotSign = %v, want %v", gotSign, tt.wantSign)
|
||||
}
|
||||
if gotDst != tt.wantDst {
|
||||
t.Errorf("SplitSignFromInt64() gotDst = %v, want %v", gotDst, tt.wantDst)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeSignFromUInt64(t *testing.T) {
|
||||
type args struct {
|
||||
sign bool
|
||||
src uint64
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantDst int64
|
||||
}{
|
||||
{
|
||||
"0",
|
||||
args{false, 0},
|
||||
0,
|
||||
},
|
||||
{
|
||||
"-0",
|
||||
args{true, 0},
|
||||
0,
|
||||
},
|
||||
{
|
||||
"-223",
|
||||
args{true, 223},
|
||||
-223,
|
||||
},
|
||||
{
|
||||
"112",
|
||||
args{false, 112},
|
||||
112,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if gotDst := MergeSignFromUInt64(tt.args.sign, tt.args.src); gotDst != tt.wantDst {
|
||||
t.Errorf("MergeSignFromUInt64() = %v, want %v", gotDst, tt.wantDst)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFractionalStringify(t *testing.T) {
|
||||
type args struct {
|
||||
fractional int
|
||||
fractionalLength int
|
||||
showPoint bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantFormatted string
|
||||
}{
|
||||
{
|
||||
".123",
|
||||
args{
|
||||
123, 3, true,
|
||||
},
|
||||
".123",
|
||||
},
|
||||
{
|
||||
"length 0",
|
||||
args{
|
||||
123, 0, true,
|
||||
},
|
||||
"",
|
||||
},
|
||||
{
|
||||
".1",
|
||||
args{
|
||||
123, 1, true,
|
||||
},
|
||||
".1",
|
||||
},
|
||||
{
|
||||
".12",
|
||||
args{
|
||||
123, 2, true,
|
||||
},
|
||||
".12",
|
||||
},
|
||||
{
|
||||
".0123",
|
||||
args{
|
||||
123, 4, true,
|
||||
},
|
||||
".0123",
|
||||
},
|
||||
{
|
||||
".01200",
|
||||
args{
|
||||
1200, 5, true,
|
||||
},
|
||||
".01200",
|
||||
},
|
||||
{
|
||||
"123",
|
||||
args{
|
||||
123, 3, false,
|
||||
},
|
||||
"123",
|
||||
},
|
||||
{
|
||||
"length 0",
|
||||
args{
|
||||
123, 0, false,
|
||||
},
|
||||
"",
|
||||
},
|
||||
{
|
||||
"1",
|
||||
args{
|
||||
123, 1, false,
|
||||
},
|
||||
"1",
|
||||
},
|
||||
{
|
||||
"12",
|
||||
args{
|
||||
123, 2, false,
|
||||
},
|
||||
"12",
|
||||
},
|
||||
{
|
||||
"0123",
|
||||
args{
|
||||
123, 4, false,
|
||||
},
|
||||
"0123",
|
||||
},
|
||||
{
|
||||
"01200",
|
||||
args{
|
||||
1200, 5, false,
|
||||
},
|
||||
"01200",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if gotFormatted := FractionalStringify(tt.args.fractional, tt.args.fractionalLength, tt.args.showPoint); gotFormatted != tt.wantFormatted {
|
||||
t.Errorf("FractionalStringify() = %v, want %v", gotFormatted, tt.wantFormatted)
|
||||
} else {
|
||||
t.Logf("FractionalStringify() = %v", gotFormatted)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package nums
|
||||
|
||||
import "image"
|
||||
|
||||
// AspectRatio returns geometry that conforms to the aspect ratio.
|
||||
func AspectRatio(srcRect image.Point, toResize uint64) image.Point {
|
||||
w, h := int(toResize), getRatioSize(int(toResize), srcRect.Y, srcRect.X)
|
||||
if srcRect.X < srcRect.Y {
|
||||
w, h = getRatioSize(int(toResize), srcRect.X, srcRect.Y), int(toResize)
|
||||
}
|
||||
return image.Point{w, h}
|
||||
}
|
||||
|
||||
func getRatioSize(a, b, c int) int {
|
||||
d := a * b / c
|
||||
return (d + 1) & -1
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
// Package nums contain number related utility functions.
|
||||
package nums
|
|
@ -0,0 +1,34 @@
|
|||
package q
|
||||
|
||||
import "container/list"
|
||||
|
||||
// NewBytesQueue creates a non blocking bytearray queue channel.
|
||||
func NewBytesQueue() (chan<- []byte, <-chan []byte) {
|
||||
send := make(chan []byte, 1)
|
||||
receive := make(chan []byte, 1)
|
||||
go manageBytesQueue(send, receive)
|
||||
return send, receive
|
||||
}
|
||||
|
||||
func manageBytesQueue(send <-chan []byte, receive chan<- []byte) {
|
||||
queue := list.New()
|
||||
defer close(receive)
|
||||
for {
|
||||
if front := queue.Front(); front == nil {
|
||||
if value, ok := <-send; ok {
|
||||
queue.PushBack(value)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
} else {
|
||||
select {
|
||||
case receive <- front.Value.([]byte):
|
||||
queue.Remove(front)
|
||||
case value, ok := <-send:
|
||||
if ok {
|
||||
queue.PushBack(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
// Package q contain non-blocking unlimited queue data structures.
|
||||
package q
|
|
@ -0,0 +1,36 @@
|
|||
package q
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
)
|
||||
|
||||
// NewStringQueue creates a non blocking string queue channel.
|
||||
func NewStringQueue() (chan<- string, <-chan string) {
|
||||
send := make(chan string, 1)
|
||||
receive := make(chan string, 1)
|
||||
go manageStringQueue(send, receive)
|
||||
return send, receive
|
||||
}
|
||||
|
||||
func manageStringQueue(send <-chan string, receive chan<- string) {
|
||||
queue := list.New()
|
||||
defer close(receive)
|
||||
for {
|
||||
if front := queue.Front(); front == nil {
|
||||
if value, ok := <-send; ok {
|
||||
queue.PushBack(value)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
} else {
|
||||
select {
|
||||
case receive <- front.Value.(string):
|
||||
queue.Remove(front)
|
||||
case value, ok := <-send:
|
||||
if ok {
|
||||
queue.PushBack(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package sqls
|
||||
|
||||
import "github.com/jmoiron/sqlx"
|
||||
|
||||
// MapRows is a convert function,that converts sqlx.Row to specified interface.
|
||||
func MapRows[T any](rows *sqlx.Rows, itemGetter func() T) (items []T, err error) {
|
||||
defer func() { _ = rows.Close() }()
|
||||
for rows.Next() {
|
||||
item := itemGetter()
|
||||
if err = rows.StructScan(item); err != nil {
|
||||
return
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
|
||||
err = rows.Err()
|
||||
return
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
// Package sqls contain database/sql related utility functions.
|
||||
package sqls
|
|
@ -0,0 +1,11 @@
|
|||
package sqls
|
||||
|
||||
import (
|
||||
_ "unsafe"
|
||||
)
|
||||
|
||||
//go:linkname convertAssign database/sql.convertAssign
|
||||
//go:nosplit
|
||||
func convertAssign(dest, src any) error
|
||||
|
||||
func ConvertAssign[T any](dest T, src any) error { return convertAssign(dest, src) }
|
|
@ -0,0 +1,4 @@
|
|||
// The runtime package uses //go:linkname to push a few functions into this
|
||||
// package but we still need a .s file so the Go tool does not pass -complete
|
||||
// to the go tool compile so the latter does not complain about Go functions
|
||||
// with no bodies.
|
|
@ -0,0 +1,60 @@
|
|||
package strutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
)
|
||||
|
||||
var (
|
||||
sizesIEC = []string{
|
||||
"B",
|
||||
"KiB",
|
||||
"MiB",
|
||||
"GiB",
|
||||
"TiB",
|
||||
"PiB",
|
||||
"EiB",
|
||||
"ZiB",
|
||||
"YiB",
|
||||
}
|
||||
sizes = []string{
|
||||
"B",
|
||||
"KB",
|
||||
"MB",
|
||||
"GB",
|
||||
"TB",
|
||||
"PB",
|
||||
"EB",
|
||||
"ZB",
|
||||
"YB",
|
||||
}
|
||||
)
|
||||
|
||||
func logn(n, b float64) float64 {
|
||||
return math.Log(n) / math.Log(b)
|
||||
}
|
||||
|
||||
func humanateBytes(s uint64, base float64, sizes []string) string {
|
||||
if s < 10 {
|
||||
return fmt.Sprintf("%dB", s)
|
||||
}
|
||||
e := math.Floor(logn(float64(s), base))
|
||||
suffix := sizes[int(e)]
|
||||
val := float64(s) / math.Pow(base, math.Floor(e))
|
||||
f := "%.0f"
|
||||
if val < 10 {
|
||||
f = "%.1f"
|
||||
}
|
||||
|
||||
return fmt.Sprintf(f+"%s", val, suffix)
|
||||
}
|
||||
|
||||
// FileSize calculates the file size and generate user-friendly string.
|
||||
func FileSizeIEC(s uint64) string {
|
||||
return humanateBytes(s, 1024, sizesIEC)
|
||||
}
|
||||
|
||||
// FileSize calculates the file size and generate user-friendly string.
|
||||
func FileSize(s uint64) string {
|
||||
return humanateBytes(s, 1000, sizes)
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
package strutil
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
const pemLineLength = 64
|
||||
|
||||
// LineBreaker is an io.Writer that advances a newline if one line exceeds 64 bytes.
|
||||
type LineBreaker struct {
|
||||
// Out os
|
||||
Out io.Writer
|
||||
line [pemLineLength]byte
|
||||
used int
|
||||
}
|
||||
|
||||
var nl = []byte{'\n'}
|
||||
|
||||
// Write writes len(p) bytes from p to the underlying data stream.
|
||||
// It returns the number of bytes written from p (0 <= n <= len(p))
|
||||
// and any error encountered that caused the write to stop early.
|
||||
// Write must return a non-nil error if it returns n < len(p).
|
||||
// Write must not modify the slice data, even temporarily.
|
||||
func (l *LineBreaker) Write(b []byte) (n int, err error) {
|
||||
if l.used+len(b) < pemLineLength {
|
||||
copy(l.line[l.used:], b)
|
||||
l.used += len(b)
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
n, err = l.Out.Write(l.line[0:l.used])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
excess := pemLineLength - l.used
|
||||
l.used = 0
|
||||
|
||||
n, err = l.Out.Write(b[0:excess])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
n, err = l.Out.Write(nl)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return l.Out.Write(b[excess:])
|
||||
}
|
||||
|
||||
// Close flushes any pending output from the writer.
|
||||
// It is an error to call Write after calling Close.
|
||||
func (l *LineBreaker) Close() (err error) {
|
||||
if l.used > 0 {
|
||||
_, err = l.Out.Write(l.line[0:l.used])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, err = l.Out.Write(nl)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
} else if closer, ok := l.Out.(io.Closer); ok {
|
||||
err = closer.Close()
|
||||
}
|
||||
return
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
package strutil
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"math"
|
||||
"math/big"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// PasswordCheck checks a password candidate with minimum length and regexp conditions.
|
||||
func PasswordCheck(source []byte, minLength int, condition ...*regexp.Regexp) (invalid bool) {
|
||||
if len(source) < minLength {
|
||||
return true
|
||||
}
|
||||
for _, cond := range condition {
|
||||
if !cond.Match(source) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// RandomDigits returns authenticate code with given length.
|
||||
func RandomDigits(digitLength, limit int) string {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
longest := func(source string) (longest int) {
|
||||
var (
|
||||
current int32
|
||||
count = 0
|
||||
)
|
||||
for _, digitRune := range source {
|
||||
if digitRune == current {
|
||||
count++
|
||||
} else {
|
||||
count = 1
|
||||
current = digitRune
|
||||
}
|
||||
if count > longest {
|
||||
longest = count
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
getNonce := func(digits int) string {
|
||||
seed, _ := rand.Int(rand.Reader, big.NewInt(math.MaxInt64))
|
||||
return strconv.FormatUint(seed.Uint64(), 10)
|
||||
}
|
||||
nonce := getNonce(digitLength)
|
||||
for longest(nonce) >= limit {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
panic(ctx.Err())
|
||||
default:
|
||||
nonce = getNonce(digitLength)
|
||||
}
|
||||
}
|
||||
return nonce
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package strutil
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
camelCase = regexp.MustCompile("(^[^A-Z0-9]*|[A-Z0-9]*)([A-Z0-9][^A-Z]+|$)")
|
||||
)
|
||||
|
||||
// CamelToUnderscore method converts a naming convention from CamelCase to under_score.
|
||||
func CamelToUnderscore(s string) string {
|
||||
var a []string
|
||||
for _, sub := range camelCase.FindAllStringSubmatch(s, -1) {
|
||||
if sub[1] != "" {
|
||||
a = append(a, sub[1])
|
||||
}
|
||||
if sub[2] != "" {
|
||||
a = append(a, sub[2])
|
||||
}
|
||||
}
|
||||
return strings.ToLower(strings.Join(a, "_"))
|
||||
}
|
|
@ -0,0 +1,187 @@
|
|||
package strutil
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// FillBytes fill the destination byte array with the given pattern.
|
||||
func FillBytes(dst []byte, pattern []byte) {
|
||||
for i := 0; i < len(dst); i++ {
|
||||
dst[i] = pattern[i%len(pattern)]
|
||||
}
|
||||
}
|
||||
|
||||
// FillByte fills the destination byte array with a single byte.
|
||||
func FillByte(dst []byte, pattern byte) {
|
||||
for i := 0; i < len(dst); i++ {
|
||||
dst[i] = pattern
|
||||
}
|
||||
}
|
||||
|
||||
// StrPad method pads the input string with the padString until the resulting string reaches the given length
|
||||
func StrPad(input string, padLength int, padString string, rightPad bool) (output string) {
|
||||
var (
|
||||
inputLength = len(input)
|
||||
padStringLength = len(padString)
|
||||
)
|
||||
if inputLength >= padLength {
|
||||
return input
|
||||
}
|
||||
|
||||
var (
|
||||
repeat = int(math.Ceil(float64(1) + (float64(padLength-padStringLength))/float64(padStringLength)))
|
||||
builder strings.Builder
|
||||
)
|
||||
builder.Grow(inputLength + padStringLength*repeat)
|
||||
if rightPad {
|
||||
builder.WriteString(input)
|
||||
for i := 0; i < repeat; i++ {
|
||||
builder.WriteString(padString)
|
||||
}
|
||||
output = builder.String()[:padLength]
|
||||
} else {
|
||||
for i := 0; i < repeat; i++ {
|
||||
builder.WriteString(padString)
|
||||
}
|
||||
builder.WriteString(input)
|
||||
output = builder.String()[builder.Len()-padLength:]
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// StrPadSingle method pads the input string with a single character until the resulting string reaches the given length
|
||||
func StrPadSingle(input string, padLength int, pad byte, rightPad bool) (output string) {
|
||||
var (
|
||||
inputLength = len(input)
|
||||
)
|
||||
if inputLength >= padLength {
|
||||
return input
|
||||
}
|
||||
|
||||
var (
|
||||
builder strings.Builder
|
||||
)
|
||||
builder.Grow(inputLength + padLength)
|
||||
if rightPad {
|
||||
builder.WriteString(input)
|
||||
for i := 0; i < padLength; i++ {
|
||||
builder.WriteByte(pad)
|
||||
}
|
||||
output = builder.String()[:padLength]
|
||||
} else {
|
||||
for i := 0; i < padLength; i++ {
|
||||
builder.WriteByte(pad)
|
||||
}
|
||||
builder.WriteString(input)
|
||||
output = builder.String()[builder.Len()-padLength:]
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// BytesPad method pads the input byte array with the padData byte array until the resulting string reaches the given length
|
||||
func BytesPad(input []byte, padLength int, padData []byte, rightPad bool) (output []byte) {
|
||||
var (
|
||||
inputLength = len(input)
|
||||
padDataLength = len(padData)
|
||||
)
|
||||
if inputLength >= padLength {
|
||||
output = make([]byte, inputLength)
|
||||
copy(output, input)
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
repeat = int(math.Ceil(float64(1) + (float64(padLength-padDataLength))/float64(padDataLength)))
|
||||
maxFillLength = repeat * padDataLength
|
||||
bufSize = inputLength + repeat*padDataLength
|
||||
padArea, fillArea []byte
|
||||
)
|
||||
|
||||
output = make([]byte, bufSize)
|
||||
|
||||
if rightPad {
|
||||
fillArea, padArea = output[0:inputLength], output[inputLength:bufSize]
|
||||
output = output[:padLength]
|
||||
} else {
|
||||
fillArea, padArea = output[maxFillLength:bufSize], output[0:maxFillLength]
|
||||
output = output[inputLength+maxFillLength-padLength:]
|
||||
}
|
||||
|
||||
// copy data
|
||||
copy(fillArea, input)
|
||||
// fill pad
|
||||
for i := 0; i < repeat; i++ {
|
||||
copy(padArea[:padDataLength], padData)
|
||||
padArea = padArea[padDataLength:]
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// BytesPad method pads the input byte array with a single byte until the resulting string reaches the given length
|
||||
func BytesPadSingle(input []byte, padLength int, pad byte, rightPad bool) (output []byte) {
|
||||
var (
|
||||
inputLength = len(input)
|
||||
)
|
||||
if inputLength >= padLength {
|
||||
output = make([]byte, inputLength)
|
||||
copy(output, input)
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
maxFillLength = padLength
|
||||
bufSize = inputLength + padLength
|
||||
padArea, fillArea []byte
|
||||
)
|
||||
|
||||
output = make([]byte, bufSize)
|
||||
|
||||
if rightPad {
|
||||
fillArea, padArea = output[0:inputLength], output[inputLength:bufSize]
|
||||
output = output[:padLength]
|
||||
} else {
|
||||
fillArea, padArea = output[maxFillLength:bufSize], output[0:maxFillLength]
|
||||
output = output[inputLength+maxFillLength-padLength:]
|
||||
}
|
||||
|
||||
// copy data
|
||||
copy(fillArea, input)
|
||||
// fill pad
|
||||
FillByte(padArea, pad)
|
||||
return
|
||||
}
|
||||
|
||||
// BytesUnPadSingle method removes a given pad character from both ends.
|
||||
func BytesUnPadSingle(input []byte, pad byte, rightPad bool, copyData bool) (output []byte) {
|
||||
var (
|
||||
limit = len(input)
|
||||
offset = 0
|
||||
)
|
||||
if rightPad {
|
||||
for ; limit > offset; limit-- {
|
||||
if input[limit-1] != pad {
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for ; offset < limit; offset++ {
|
||||
if input[offset] != pad {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if limit-offset < 1 {
|
||||
return
|
||||
}
|
||||
if copyData {
|
||||
output = make([]byte, limit-offset)
|
||||
copy(output, input[offset:limit])
|
||||
} else {
|
||||
output = input[offset:limit]
|
||||
}
|
||||
return
|
||||
}
|
|
@ -0,0 +1,217 @@
|
|||
package strutil
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStrPad(t *testing.T) {
|
||||
const input = "Codes"
|
||||
type args struct {
|
||||
input string
|
||||
padLength int
|
||||
padString string
|
||||
rightPad bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantOutput string
|
||||
}{
|
||||
{
|
||||
"rpad test",
|
||||
args{input, 10, " ", true},
|
||||
"Codes ",
|
||||
},
|
||||
{
|
||||
"lpad test",
|
||||
args{input, 10, "-=", false},
|
||||
"=-=-=Codes",
|
||||
},
|
||||
{
|
||||
"rpad filled test",
|
||||
args{input, len(input), "_*", true},
|
||||
input,
|
||||
},
|
||||
{
|
||||
"lpad filled test",
|
||||
args{input, len(input), "*", false},
|
||||
input,
|
||||
},
|
||||
{
|
||||
"rpad overflow test",
|
||||
args{input, 3, "_*", true},
|
||||
input,
|
||||
},
|
||||
{
|
||||
"lpad overflow test",
|
||||
args{input, 3, "*", false},
|
||||
input,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if gotOutput := StrPad(tt.args.input, tt.args.padLength, tt.args.padString, tt.args.rightPad); gotOutput != tt.wantOutput {
|
||||
t.Errorf("StrPad() = %v, want %v", gotOutput, tt.wantOutput)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStrPadSingle(t *testing.T) {
|
||||
const input = "Codes"
|
||||
type args struct {
|
||||
input string
|
||||
padLength int
|
||||
pad byte
|
||||
rightPad bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantOutput string
|
||||
}{
|
||||
{
|
||||
"rpad test",
|
||||
args{input, 10, ' ', true},
|
||||
"Codes ",
|
||||
},
|
||||
{
|
||||
"lpad test",
|
||||
args{input, 10, '-', false},
|
||||
"-----Codes",
|
||||
},
|
||||
{
|
||||
"rpad filled test",
|
||||
args{input, len(input), '*', true},
|
||||
input,
|
||||
},
|
||||
{
|
||||
"lpad filled test",
|
||||
args{input, len(input), '*', false},
|
||||
input,
|
||||
},
|
||||
{
|
||||
"rpad overflow test",
|
||||
args{input, 3, '*', true},
|
||||
input,
|
||||
},
|
||||
{
|
||||
"lpad overflow test",
|
||||
args{input, 3, '*', false},
|
||||
input,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if gotOutput := StrPadSingle(tt.args.input, tt.args.padLength, tt.args.pad, tt.args.rightPad); gotOutput != tt.wantOutput {
|
||||
t.Errorf("StrPad() = %v, want %v", gotOutput, tt.wantOutput)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
func TestBytesPad(t *testing.T) {
|
||||
const input = "Codes"
|
||||
type args struct {
|
||||
input []byte
|
||||
padLength int
|
||||
padData []byte
|
||||
rightPad bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantOutput []byte
|
||||
}{
|
||||
{
|
||||
"rpad test",
|
||||
args{[]byte(input), 10, []byte(" "), true},
|
||||
[]byte("Codes "),
|
||||
},
|
||||
{
|
||||
"lpad test",
|
||||
args{[]byte(input), 10, []byte("-="), false},
|
||||
[]byte("=-=-=Codes"),
|
||||
},
|
||||
{
|
||||
"rpad filled test",
|
||||
args{[]byte(input), len(input), []byte("_*"), true},
|
||||
[]byte(input),
|
||||
},
|
||||
{
|
||||
"lpad filled test",
|
||||
args{[]byte(input), len(input), []byte("*"), false},
|
||||
[]byte(input),
|
||||
},
|
||||
{
|
||||
"rpad overflow test",
|
||||
args{[]byte(input), 3, []byte("_*"), true},
|
||||
[]byte(input),
|
||||
},
|
||||
{
|
||||
"lpad overflow test",
|
||||
args{[]byte(input), 3, []byte("*"), false},
|
||||
[]byte(input),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if gotOutput := BytesPad(tt.args.input, tt.args.padLength, tt.args.padData, tt.args.rightPad); !reflect.DeepEqual(gotOutput, tt.wantOutput) {
|
||||
t.Errorf("BytesPad() = %v, want %v", gotOutput, tt.wantOutput)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBytesPadSingle(t *testing.T) {
|
||||
const input = "Codes"
|
||||
type args struct {
|
||||
input []byte
|
||||
padLength int
|
||||
padData byte
|
||||
rightPad bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantOutput []byte
|
||||
}{
|
||||
{
|
||||
"rpad test",
|
||||
args{[]byte(input), 10, ' ', true},
|
||||
[]byte("Codes "),
|
||||
},
|
||||
{
|
||||
"lpad test",
|
||||
args{[]byte(input), 10, '_', false},
|
||||
[]byte("_____Codes"),
|
||||
},
|
||||
{
|
||||
"rpad filled test",
|
||||
args{[]byte(input), len(input), '*', true},
|
||||
[]byte(input),
|
||||
},
|
||||
{
|
||||
"lpad filled test",
|
||||
args{[]byte(input), len(input), '*', false},
|
||||
[]byte(input),
|
||||
},
|
||||
{
|
||||
"rpad overflow test",
|
||||
args{[]byte(input), 3, '*', true},
|
||||
[]byte(input),
|
||||
},
|
||||
{
|
||||
"lpad overflow test",
|
||||
args{[]byte(input), 3, '*', false},
|
||||
[]byte(input),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if gotOutput := BytesPadSingle(tt.args.input, tt.args.padLength, tt.args.padData, tt.args.rightPad); !reflect.DeepEqual(gotOutput, tt.wantOutput) {
|
||||
t.Errorf("BytesPad() = %v, want %v", gotOutput, tt.wantOutput)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
// Package strutil contain string conversion utility collection.
|
||||
package strutil
|
|
@ -0,0 +1,74 @@
|
|||
package strutil
|
||||
|
||||
import (
|
||||
"math"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
var (
|
||||
// EmailRegex is the standard email address format.
|
||||
EmailRegex = regexp.MustCompile(`^([^<>()[\]\\.,;:\s@"]+(?:\.[^<>()[\]\\.,;:\s@"]+)*)@(?:(?:[a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,})$`)
|
||||
// DigitRegex represents a digit.
|
||||
DigitRegex = regexp.MustCompile(`\d`)
|
||||
)
|
||||
|
||||
// MaskText is a masking function for hide sensitive data.
|
||||
func MaskText(src string) (val string) {
|
||||
|
||||
var dst strings.Builder
|
||||
defer func(ptr *string) { *ptr = dst.String() }(&val)
|
||||
|
||||
// email
|
||||
if ret := EmailRegex.FindAllStringSubmatchIndex(src, -1); len(ret) > 0 {
|
||||
start := src[:ret[0][2]]
|
||||
username := src[ret[0][2]:ret[0][3]]
|
||||
end := src[ret[0][3]:]
|
||||
length := utf8.RuneCountInString(username)
|
||||
_, firstCharLength := utf8.DecodeRuneInString(username)
|
||||
_, lastCharLength := utf8.DecodeLastRuneInString(username)
|
||||
_, secondLastCharLength := utf8.DecodeLastRuneInString(username[:len(username)-lastCharLength])
|
||||
dst.WriteString(start)
|
||||
if length > 3 {
|
||||
dst.WriteString(username[:firstCharLength])
|
||||
for i := 0; i < length-3; i++ {
|
||||
dst.WriteByte('*')
|
||||
}
|
||||
dst.WriteString(username[len(username)-(secondLastCharLength+lastCharLength):])
|
||||
} else {
|
||||
dst.WriteByte('*')
|
||||
dst.WriteString(username[firstCharLength:])
|
||||
}
|
||||
dst.WriteString(end)
|
||||
} else if ret = DigitRegex.FindAllStringSubmatchIndex(src, -1); len(ret) > 2 {
|
||||
var (
|
||||
offset = 0
|
||||
factor = float64(len(ret)) / 3.0
|
||||
pos = int(math.Floor(factor))
|
||||
fillLength = int(math.Ceil(factor))
|
||||
)
|
||||
for i := pos; i < pos+fillLength; i++ {
|
||||
dst.WriteString(src[offset:ret[i][0]])
|
||||
dst.WriteByte('*')
|
||||
offset = ret[i][1]
|
||||
}
|
||||
dst.WriteString(src[offset:])
|
||||
} else {
|
||||
length := utf8.RuneCountInString(src)
|
||||
_, firstCharLength := utf8.DecodeRuneInString(src)
|
||||
_, lastCharLength := utf8.DecodeLastRuneInString(src)
|
||||
if length > 2 {
|
||||
dst.WriteString(src[:firstCharLength])
|
||||
for i := 0; i < length-2; i++ {
|
||||
dst.WriteByte('*')
|
||||
}
|
||||
dst.WriteString(src[len(src)-lastCharLength:])
|
||||
} else if length > 0 {
|
||||
dst.WriteByte('*')
|
||||
dst.WriteString(src[firstCharLength:])
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
package strutil
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMaskText(t *testing.T) {
|
||||
type args struct {
|
||||
input string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantVal string
|
||||
}{
|
||||
{
|
||||
"address",
|
||||
args{"test@example.com"},
|
||||
"t*st@example.com",
|
||||
},
|
||||
{
|
||||
"address_long",
|
||||
args{"testtesttesttest@example.com"},
|
||||
"t*************st@example.com",
|
||||
},
|
||||
{
|
||||
"address_short",
|
||||
args{"tst@example.com"},
|
||||
"*st@example.com",
|
||||
},
|
||||
{
|
||||
"address_short_2",
|
||||
args{"tt@example.com"},
|
||||
"*t@example.com",
|
||||
},
|
||||
{
|
||||
"address_very_short",
|
||||
args{"t@example.com"},
|
||||
"*@example.com",
|
||||
},
|
||||
{
|
||||
"non-formal",
|
||||
args{"texample.com"},
|
||||
"t**********m",
|
||||
},
|
||||
{
|
||||
"non-formal2",
|
||||
args{"te🧭ample.co🧭"},
|
||||
"t**********🧭",
|
||||
},
|
||||
{
|
||||
"non-formal short",
|
||||
args{"com"},
|
||||
"c*m",
|
||||
},
|
||||
{
|
||||
"non-formal short 2",
|
||||
args{"⏰om"},
|
||||
"⏰*m",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if gotVal := MaskText(tt.args.input); gotVal != tt.wantVal {
|
||||
t.Errorf("MaskText() = %v, want %v", gotVal, tt.wantVal)
|
||||
} else {
|
||||
t.Log("==> ", gotVal)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,295 @@
|
|||
package strutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"math"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
fingerNum = 10
|
||||
zeroChr = '0'
|
||||
)
|
||||
|
||||
// Reverse is a function that returns a string in reverse order.
|
||||
func Reverse(s string) string {
|
||||
runes := []rune(s)
|
||||
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
|
||||
runes[i], runes[j] = runes[j], runes[i]
|
||||
}
|
||||
return string(runes)
|
||||
}
|
||||
|
||||
// FormatIntToStringReversed is a function that converts number to decimal string reversed order(LSB).
|
||||
func FormatIntToStringReversed(number int) (out string) {
|
||||
if number == 0 {
|
||||
return "0"
|
||||
}
|
||||
var (
|
||||
buf strings.Builder
|
||||
digit int
|
||||
sign = number < 0
|
||||
)
|
||||
if sign {
|
||||
number = -number
|
||||
}
|
||||
for radix := 0; number > 0; radix++ {
|
||||
digit, number = number%fingerNum, number/fingerNum
|
||||
buf.WriteByte(zeroChr + byte(digit))
|
||||
}
|
||||
if sign {
|
||||
buf.WriteByte('-')
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// FormatIntToString is a function that converts number to decimal string(MSB).
|
||||
func FormatIntToString(number int) (out string) {
|
||||
if number == 0 {
|
||||
return "0"
|
||||
}
|
||||
var (
|
||||
buf strings.Builder
|
||||
digit int
|
||||
sign = number < 0
|
||||
)
|
||||
|
||||
if sign {
|
||||
number = -number
|
||||
buf.WriteByte('-')
|
||||
}
|
||||
|
||||
var (
|
||||
maxDigit = int(math.Log10(float64(number))) + 1
|
||||
numBuf = make([]byte, maxDigit)
|
||||
)
|
||||
|
||||
for radix := 0; number > 0; radix++ {
|
||||
digit, number = number%fingerNum, number/fingerNum
|
||||
numBuf[maxDigit-radix-1] = zeroChr + byte(digit)
|
||||
}
|
||||
buf.Write(numBuf)
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// FormatIntToBytesReversed is a function that converts number to decimal byte array(ASCII code based) reversed order(LSB).
|
||||
func FormatIntToBytesReversed(number int) (out []byte) {
|
||||
if number == 0 {
|
||||
return []byte("0")
|
||||
}
|
||||
var (
|
||||
buf bytes.Buffer
|
||||
digit int
|
||||
sign = number < 0
|
||||
)
|
||||
|
||||
if sign {
|
||||
number = -number
|
||||
}
|
||||
for radix := 0; number > 0; radix++ {
|
||||
digit, number = number%fingerNum, number/fingerNum
|
||||
buf.WriteByte(zeroChr + byte(digit))
|
||||
}
|
||||
|
||||
if sign {
|
||||
buf.WriteByte('-')
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// FormatIntToBytes is a function that converts number to decimal byte array(ASCII code based,MSB).
|
||||
func FormatIntToBytes(number int) (out []byte) {
|
||||
if number == 0 {
|
||||
return []byte("0")
|
||||
}
|
||||
|
||||
var (
|
||||
digit int
|
||||
sign = number < 0
|
||||
maxDigit int
|
||||
)
|
||||
|
||||
if sign {
|
||||
number = -number
|
||||
maxDigit = int(math.Log10(float64(number))) + 2
|
||||
} else {
|
||||
maxDigit = int(math.Log10(float64(number))) + 1
|
||||
}
|
||||
|
||||
out = make([]byte, maxDigit)
|
||||
if sign {
|
||||
out[0] = '-'
|
||||
}
|
||||
|
||||
for radix := 0; number > 0; radix++ {
|
||||
digit, number = number%fingerNum, number/fingerNum
|
||||
out[maxDigit-radix-1] = zeroChr + byte(digit)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ParseStringToInt is a function that converts a decimal string to integer.
|
||||
func ParseStringToInt(in string) (number int) {
|
||||
if len(in) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
out []uint8
|
||||
sign = in[0] == '-'
|
||||
)
|
||||
if sign {
|
||||
out = make([]uint8, 0, len(in)-1)
|
||||
in = in[1:]
|
||||
} else {
|
||||
out = make([]uint8, 0, len(in))
|
||||
}
|
||||
for _, digit := range in {
|
||||
if digit < '0' || digit > '9' {
|
||||
continue
|
||||
}
|
||||
out = append(out, byte(digit-'0'))
|
||||
}
|
||||
scanned := len(out)
|
||||
pow := 1
|
||||
|
||||
for i := scanned; i > 0; i-- {
|
||||
number += pow * int(out[i-1])
|
||||
pow *= 10
|
||||
}
|
||||
|
||||
if sign {
|
||||
number = -number
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ParseBytesToInt is a function that converts a decimal byte array(ASCII based) to integer.
|
||||
func ParseBytesToInt(in []byte) (number int) {
|
||||
if len(in) == 0 {
|
||||
return -1
|
||||
}
|
||||
|
||||
var (
|
||||
out []uint8
|
||||
sign = in[0] == '-'
|
||||
)
|
||||
|
||||
if sign {
|
||||
out = make([]uint8, 0, len(in)-1)
|
||||
in = in[1:]
|
||||
} else {
|
||||
out = make([]uint8, 0, len(in))
|
||||
}
|
||||
|
||||
for _, digit := range in {
|
||||
if digit < '0' || digit > '9' {
|
||||
continue
|
||||
}
|
||||
out = append(out, digit-'0')
|
||||
}
|
||||
scanned := len(out)
|
||||
if scanned == 0 {
|
||||
return -1
|
||||
}
|
||||
pow := 1
|
||||
|
||||
for i := scanned; i > 0; i-- {
|
||||
number += pow * int(out[i-1])
|
||||
pow *= 10
|
||||
}
|
||||
|
||||
if sign {
|
||||
number = -number
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// FormatUnsignedIntToStringReversed is a function that converts number to decimal string reversed order(LSB).
|
||||
func FormatUnsignedIntToStringReversed(sign bool, number uint) (out string) {
|
||||
if number == 0 {
|
||||
return "0"
|
||||
}
|
||||
var (
|
||||
buf strings.Builder
|
||||
digit uint
|
||||
)
|
||||
|
||||
for radix := 0; number > 0; radix++ {
|
||||
digit, number = number%fingerNum, number/fingerNum
|
||||
buf.WriteByte(zeroChr + byte(digit))
|
||||
}
|
||||
if sign {
|
||||
buf.WriteByte('-')
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// FormatUnsignedIntToString is a function that converts number to decimal string(MSB).
|
||||
func FormatUnsignedIntToString(sign bool, number uint) (out string) {
|
||||
if number == 0 {
|
||||
return "0"
|
||||
}
|
||||
var (
|
||||
buf strings.Builder
|
||||
digit uint
|
||||
maxDigit = int(math.Log10(float64(number))) + 1
|
||||
numBuf = make([]byte, maxDigit)
|
||||
)
|
||||
if sign {
|
||||
buf.WriteByte('-')
|
||||
}
|
||||
for radix := 0; number > 0; radix++ {
|
||||
digit, number = number%fingerNum, number/fingerNum
|
||||
numBuf[maxDigit-radix-1] = zeroChr + byte(digit)
|
||||
}
|
||||
|
||||
buf.Write(numBuf)
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// FormatUnsignedIntToBytesReversed is a function that converts number to decimal byte array(ASCII code based) reversed order(LSB).
|
||||
func FormatUnsignedIntToBytesReversed(sign bool, number uint) (out []byte) {
|
||||
if number == 0 {
|
||||
return []byte("0")
|
||||
}
|
||||
var (
|
||||
buf bytes.Buffer
|
||||
digit uint
|
||||
)
|
||||
for radix := 0; number > 0; radix++ {
|
||||
digit, number = number%fingerNum, number/fingerNum
|
||||
buf.WriteByte(zeroChr + byte(digit))
|
||||
}
|
||||
if sign {
|
||||
buf.WriteByte('-')
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// FormatUnsignedIntToBytes is a function that converts number to decimal byte array(ASCII code based, MSB).
|
||||
func FormatUnsignedIntToBytes(sign bool, number uint) (out []byte) {
|
||||
if number == 0 {
|
||||
return []byte("0")
|
||||
}
|
||||
var (
|
||||
digit uint
|
||||
maxDigit = int(math.Log10(float64(number))) + 1
|
||||
)
|
||||
|
||||
if sign {
|
||||
maxDigit++
|
||||
out = make([]byte, maxDigit)
|
||||
out[0] = '-'
|
||||
} else {
|
||||
out = make([]byte, maxDigit)
|
||||
}
|
||||
|
||||
for radix := 0; number > 0; radix++ {
|
||||
digit, number = number%fingerNum, number/fingerNum
|
||||
out[maxDigit-radix-1] = zeroChr + byte(digit)
|
||||
}
|
||||
return
|
||||
}
|
|
@ -0,0 +1,368 @@
|
|||
package strutil
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFormatIntToBytesReversed(t *testing.T) {
|
||||
type args struct {
|
||||
number int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantOut []byte
|
||||
}{
|
||||
{
|
||||
"simple",
|
||||
args{1234567890},
|
||||
[]byte("0987654321"),
|
||||
},
|
||||
{
|
||||
"-simple",
|
||||
args{-1234567890},
|
||||
[]byte("0987654321-"),
|
||||
},
|
||||
{
|
||||
"0",
|
||||
args{0},
|
||||
[]byte("0"),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if gotOut := FormatIntToBytesReversed(tt.args.number); !reflect.DeepEqual(gotOut, tt.wantOut) {
|
||||
t.Errorf("FormatIntToBytesReversed() = %v, want %v", string(gotOut), string(tt.wantOut))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
func TestFormatIntToBytes(t *testing.T) {
|
||||
type args struct {
|
||||
number int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantOut []byte
|
||||
}{
|
||||
{
|
||||
"simple",
|
||||
args{1234567890},
|
||||
[]byte("1234567890"),
|
||||
},
|
||||
{
|
||||
"-simple",
|
||||
args{-1234567890},
|
||||
[]byte("-1234567890"),
|
||||
},
|
||||
{
|
||||
"0",
|
||||
args{0},
|
||||
[]byte("0"),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if gotOut := FormatIntToBytes(tt.args.number); !reflect.DeepEqual(gotOut, tt.wantOut) {
|
||||
t.Errorf("FormatIntToBytes() = %v, want %v", string(gotOut), string(tt.wantOut))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatIntToStringReversed(t *testing.T) {
|
||||
type args struct {
|
||||
number int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantOut string
|
||||
}{
|
||||
{
|
||||
"simple",
|
||||
args{1234567890},
|
||||
"0987654321",
|
||||
},
|
||||
{
|
||||
"-simple",
|
||||
args{-1234567890},
|
||||
"0987654321-",
|
||||
},
|
||||
{
|
||||
"0",
|
||||
args{0},
|
||||
"0",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if gotOut := FormatIntToStringReversed(tt.args.number); gotOut != tt.wantOut {
|
||||
t.Errorf("FormatIntToStringReversed() = %v, want %v", gotOut, tt.wantOut)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatIntToString(t *testing.T) {
|
||||
type args struct {
|
||||
number int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantOut string
|
||||
}{
|
||||
{
|
||||
"simple",
|
||||
args{1234567890},
|
||||
"1234567890",
|
||||
},
|
||||
{
|
||||
"simple",
|
||||
args{-1234567890},
|
||||
"-1234567890",
|
||||
},
|
||||
{
|
||||
"0",
|
||||
args{0},
|
||||
"0",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if gotOut := FormatIntToString(tt.args.number); gotOut != tt.wantOut {
|
||||
t.Errorf("FormatIntToString() = %v, want %v", gotOut, tt.wantOut)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseBytesToInt(t *testing.T) {
|
||||
type args struct {
|
||||
in []byte
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantNumber int
|
||||
}{
|
||||
{
|
||||
"simple",
|
||||
args{[]byte("1234567890")},
|
||||
1234567890,
|
||||
},
|
||||
{
|
||||
"simple",
|
||||
args{[]byte("-1234567890")},
|
||||
-1234567890,
|
||||
},
|
||||
{
|
||||
"0",
|
||||
args{[]byte("0")},
|
||||
0,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if gotNumber := ParseBytesToInt(tt.args.in); gotNumber != tt.wantNumber {
|
||||
t.Errorf("ParseBytesToInt() = %v, want %v", gotNumber, tt.wantNumber)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseStringToInt(t *testing.T) {
|
||||
type args struct {
|
||||
in string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantNumber int
|
||||
}{
|
||||
{
|
||||
"simple",
|
||||
args{"1234567890"},
|
||||
1234567890,
|
||||
},
|
||||
{
|
||||
"-simple",
|
||||
args{"-1234567890"},
|
||||
-1234567890,
|
||||
},
|
||||
{
|
||||
"0",
|
||||
args{"0"},
|
||||
0,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if gotNumber := ParseStringToInt(tt.args.in); gotNumber != tt.wantNumber {
|
||||
t.Errorf("ParseStringToInt() = %v, want %v", gotNumber, tt.wantNumber)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatUnsignedIntToStringReversed(t *testing.T) {
|
||||
type args struct {
|
||||
sign bool
|
||||
number uint
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantOut string
|
||||
}{
|
||||
{
|
||||
"simple",
|
||||
args{false, 1234567890},
|
||||
"0987654321",
|
||||
},
|
||||
{
|
||||
"-simple",
|
||||
args{true, 1234567890},
|
||||
"0987654321-",
|
||||
},
|
||||
{
|
||||
"0",
|
||||
args{false, 0},
|
||||
"0",
|
||||
},
|
||||
{
|
||||
"-0",
|
||||
args{true, 0},
|
||||
"0",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if gotOut := FormatUnsignedIntToStringReversed(tt.args.sign, tt.args.number); gotOut != tt.wantOut {
|
||||
t.Errorf("FormatUnsignedIntToStringReversed() = %v, want %v", gotOut, tt.wantOut)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatUnsignedIntToString(t *testing.T) {
|
||||
type args struct {
|
||||
sign bool
|
||||
number uint
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantOut string
|
||||
}{
|
||||
{
|
||||
"simple",
|
||||
args{false, 1234567890},
|
||||
"1234567890",
|
||||
},
|
||||
{
|
||||
"-simple",
|
||||
args{true, 1234567890},
|
||||
"-1234567890",
|
||||
},
|
||||
{
|
||||
"0",
|
||||
args{false, 0},
|
||||
"0",
|
||||
},
|
||||
{
|
||||
"-0",
|
||||
args{true, 0},
|
||||
"0",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if gotOut := FormatUnsignedIntToString(tt.args.sign, tt.args.number); gotOut != tt.wantOut {
|
||||
t.Errorf("FormatUnsignedIntToString() = %v, want %v", gotOut, tt.wantOut)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatUnsignedIntToBytesReversed(t *testing.T) {
|
||||
type args struct {
|
||||
sign bool
|
||||
number uint
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantOut []byte
|
||||
}{
|
||||
{
|
||||
"simple",
|
||||
args{false, 1234567890},
|
||||
[]byte("0987654321"),
|
||||
},
|
||||
{
|
||||
"-simple",
|
||||
args{true, 1234567890},
|
||||
[]byte("0987654321-"),
|
||||
},
|
||||
{
|
||||
"0",
|
||||
args{false, 0},
|
||||
[]byte("0"),
|
||||
},
|
||||
{
|
||||
"0",
|
||||
args{true, 0},
|
||||
[]byte("0"),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if gotOut := FormatUnsignedIntToBytesReversed(tt.args.sign, tt.args.number); !reflect.DeepEqual(gotOut, tt.wantOut) {
|
||||
t.Errorf("FormatUnsignedIntToBytesReversed() = %v, want %v", gotOut, tt.wantOut)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatUnsignedIntToBytes(t *testing.T) {
|
||||
type args struct {
|
||||
sign bool
|
||||
number uint
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantOut []byte
|
||||
}{{
|
||||
"simple",
|
||||
args{false, 1234567890},
|
||||
[]byte("1234567890"),
|
||||
},
|
||||
{
|
||||
"-simple",
|
||||
args{true, 1234567890},
|
||||
[]byte("-1234567890"),
|
||||
},
|
||||
{
|
||||
"0",
|
||||
args{false, 0},
|
||||
[]byte("0"),
|
||||
},
|
||||
{
|
||||
"-0",
|
||||
args{true, 0},
|
||||
[]byte("0"),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if gotOut := FormatUnsignedIntToBytes(tt.args.sign, tt.args.number); !reflect.DeepEqual(gotOut, tt.wantOut) {
|
||||
t.Errorf("FormatUnsignedIntToBytes() = %v, want %v", gotOut, tt.wantOut)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package strutil
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// B2S converts byte slice to a string without memory allocation.
|
||||
// See https://groups.google.com/forum/#!msg/Golang-Nuts/ENgbUzYvCuU/90yGx7GUAgAJ .
|
||||
//
|
||||
// Note it may break if string and/or slice header will change
|
||||
// in the future go versions.
|
||||
func B2S(b []byte) string {
|
||||
return *(*string)(unsafe.Pointer(&b))
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
package types
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"github.com/ericlagergren/decimal"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Decimal is a DECIMAL in sql. Its zero value is valid for use with both
|
||||
// Value and Scan.
|
||||
//
|
||||
// Although decimal can represent NaN and Infinity it will return an error
|
||||
// if an attempt to store these values in the database is made.
|
||||
//
|
||||
// Because it cannot be nil, when Big is nil Value() will return "0"
|
||||
// It will error if an attempt to Scan() a "null" value into it.
|
||||
type Decimal struct {
|
||||
decimal.Big
|
||||
}
|
||||
|
||||
// NullDecimal is the same as Decimal, but allows the Big pointer to be nil.
|
||||
// See docmentation for Decimal for more details.
|
||||
//
|
||||
// When going into a database, if Big is nil it's value will be "null".
|
||||
type NullDecimal struct {
|
||||
*decimal.Big
|
||||
}
|
||||
|
||||
// NewDecimal creates a new decimal from a decimal
|
||||
func NewDecimal(d *decimal.Big) (num Decimal) {
|
||||
if d != nil {
|
||||
num.Big.Copy(d)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// NewNullDecimal creates a new null decimal from a decimal
|
||||
func NewNullDecimal(d *decimal.Big) NullDecimal {
|
||||
return NullDecimal{Big: d}
|
||||
}
|
||||
|
||||
// Value implements driver.Valuer.
|
||||
func (d Decimal) Value() (driver.Value, error) {
|
||||
return decimalValue(&d.Big, false)
|
||||
}
|
||||
|
||||
// Scan implements sql.Scanner.
|
||||
func (d *Decimal) Scan(val any) (err error) {
|
||||
_, err = decimalScan(&d.Big, val, false)
|
||||
return
|
||||
}
|
||||
|
||||
// Value implements driver.Valuer.
|
||||
func (n NullDecimal) Value() (driver.Value, error) {
|
||||
return decimalValue(n.Big, true)
|
||||
}
|
||||
|
||||
// Scan implements sql.Scanner.
|
||||
func (n *NullDecimal) Scan(val any) error {
|
||||
newD, err := decimalScan(n.Big, val, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n.Big = newD
|
||||
return nil
|
||||
}
|
||||
|
||||
func decimalValue(d *decimal.Big, canNull bool) (driver.Value, error) {
|
||||
if canNull && d == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if d.IsNaN(0) {
|
||||
return nil, errors.New("refusing to allow NaN into database")
|
||||
}
|
||||
if d.IsInf(0) {
|
||||
return nil, errors.New("refusing to allow infinity into database")
|
||||
}
|
||||
|
||||
return d.String(), nil
|
||||
}
|
||||
|
||||
func decimalScan(d *decimal.Big, val any, canNull bool) (*decimal.Big, error) {
|
||||
if val == nil {
|
||||
if !canNull {
|
||||
return nil, errors.New("null cannot be scanned into decimal")
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if d == nil {
|
||||
d = new(decimal.Big)
|
||||
}
|
||||
|
||||
switch t := val.(type) {
|
||||
case float64:
|
||||
d.SetFloat64(t)
|
||||
return d, nil
|
||||
case string:
|
||||
if _, ok := d.SetString(t); !ok {
|
||||
if err := d.Context.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, errors.Errorf("invalid decimal syntax: %q", t)
|
||||
}
|
||||
return d, nil
|
||||
case []byte:
|
||||
if err := d.UnmarshalText(t); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d, nil
|
||||
default:
|
||||
return nil, errors.Errorf("cannot scan decimal value: %#v", val)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,139 @@
|
|||
package types
|
||||
|
||||
import (
|
||||
"github.com/ericlagergren/decimal"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDecimal_Value(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []string{
|
||||
"3.14",
|
||||
"0",
|
||||
"43.4292",
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
var d Decimal
|
||||
d.SetString(test)
|
||||
|
||||
val, err := d.Value()
|
||||
if err != nil {
|
||||
t.Errorf("%d) %+v", i, err)
|
||||
}
|
||||
|
||||
s, ok := val.(string)
|
||||
if !ok {
|
||||
t.Errorf("%d) wrong type returned", i)
|
||||
}
|
||||
|
||||
if s != test {
|
||||
t.Errorf("%d) want: %s, got: %s", i, test, s)
|
||||
}
|
||||
}
|
||||
|
||||
var infinity Decimal
|
||||
infinity.SetInf(true)
|
||||
if _, err := infinity.Value(); err == nil {
|
||||
t.Error("infinity should not be passed into the database")
|
||||
}
|
||||
var nan Decimal
|
||||
nan.SetNaN(true)
|
||||
if _, err := nan.Value(); err == nil {
|
||||
t.Error("nan should not be passed into the database")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecimal_Scan(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []string{
|
||||
"3.14",
|
||||
"0",
|
||||
"43.4292",
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
var d Decimal
|
||||
if err := d.Scan(test); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if got := d.String(); got != test {
|
||||
t.Errorf("%d) want: %s, got: %s", i, test, got)
|
||||
}
|
||||
}
|
||||
|
||||
var d Decimal
|
||||
if err := d.Scan(nil); err == nil {
|
||||
t.Error("it should disallow scanning from a null value")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNullDecimal_Value(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []string{
|
||||
"3.14",
|
||||
"0",
|
||||
"43.4292",
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
d := NullDecimal{Big: new(decimal.Big)}
|
||||
d.Big, _ = d.SetString(test)
|
||||
|
||||
val, err := d.Value()
|
||||
if err != nil {
|
||||
t.Errorf("%d) %+v", i, err)
|
||||
}
|
||||
|
||||
s, ok := val.(string)
|
||||
if !ok {
|
||||
t.Errorf("%d) wrong type returned", i)
|
||||
}
|
||||
|
||||
if s != test {
|
||||
t.Errorf("%d) want: %s, got: %s", i, test, s)
|
||||
}
|
||||
}
|
||||
|
||||
infinity := NullDecimal{Big: new(decimal.Big).SetInf(true)}
|
||||
if _, err := infinity.Value(); err == nil {
|
||||
t.Error("infinity should not be passed into the database")
|
||||
}
|
||||
nan := NullDecimal{Big: new(decimal.Big).SetNaN(true)}
|
||||
if _, err := nan.Value(); err == nil {
|
||||
t.Error("nan should not be passed into the database")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNullDecimal_Scan(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []string{
|
||||
"3.14",
|
||||
"0",
|
||||
"43.4292",
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
var d NullDecimal
|
||||
if err := d.Scan(test); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if got := d.String(); got != test {
|
||||
t.Errorf("%d) want: %s, got: %s", i, test, got)
|
||||
}
|
||||
}
|
||||
|
||||
var d NullDecimal
|
||||
if err := d.Scan(nil); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if d.Big != nil {
|
||||
t.Error("it should have been nil")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,114 @@
|
|||
package types
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/subtle"
|
||||
"database/sql/driver"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Bytes16 is an alias for [16]byte,
|
||||
// Bytes16 implements Marshal and Unmarshal.
|
||||
type Bytes16 [16]byte
|
||||
|
||||
// Marshal obj into your JSON variable.
|
||||
func (b Bytes16) Marshal() ([]byte, error) {
|
||||
return b[:], nil
|
||||
}
|
||||
|
||||
func (b Bytes16) MarshalTo(buf []byte) (n int, err error) {
|
||||
copy(buf, b[:])
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
// Unmarshal your JSON variable into dest.
|
||||
func (b *Bytes16) Unmarshal(buf []byte) error {
|
||||
if len(buf) != 16 {
|
||||
return fmt.Errorf("invalid bytes16 (got %d bytes)", len(buf))
|
||||
}
|
||||
copy(b[:], buf)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b Bytes16) Compare(other Bytes16) int {
|
||||
return bytes.Compare(b[:], other[:])
|
||||
}
|
||||
|
||||
func (b Bytes16) Equal(other Bytes16) bool {
|
||||
return subtle.ConstantTimeCompare(b[:], other[:]) == 1
|
||||
}
|
||||
|
||||
// ParseBytes is like Parse, except it parses a byte slice instead of a string.
|
||||
func ParseBytes16(buf []byte) (b Bytes16, err error) {
|
||||
if len(buf) != hex.EncodedLen(len(b)) {
|
||||
err = fmt.Errorf("invalid bytes16 length: %d", len(buf))
|
||||
}
|
||||
_, err = hex.Decode(b[:], buf)
|
||||
return
|
||||
}
|
||||
|
||||
func (b *Bytes16) UnmarshalJSON(from []byte) error {
|
||||
quote := []byte("\"")
|
||||
quoteSize := len(quote)
|
||||
|
||||
if len(from) < quoteSize*2 {
|
||||
return errors.New("invalid quote notation")
|
||||
}
|
||||
|
||||
if !bytes.HasPrefix(from, quote) || !bytes.HasSuffix(from, quote) {
|
||||
return errors.New("invalid quote notation")
|
||||
} else if parsed, err := ParseBytes16(from[quoteSize : len(from)-quoteSize]); err == nil {
|
||||
*b = parsed
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b Bytes16) MarshalJSON() ([]byte, error) {
|
||||
var buffer bytes.Buffer
|
||||
buffer.WriteRune('"')
|
||||
buffer.WriteString(hex.EncodeToString(b[:]))
|
||||
buffer.WriteRune('"')
|
||||
return buffer.Bytes(), nil
|
||||
}
|
||||
|
||||
func (b *Bytes16) Size() int {
|
||||
if b == nil {
|
||||
return 0
|
||||
}
|
||||
return 16
|
||||
}
|
||||
|
||||
func (b *Bytes16) FromHexString(buf []byte) error {
|
||||
hexBuf := make([]byte, hex.DecodedLen(len(buf)))
|
||||
if n, err := hex.Decode(hexBuf, buf); err != nil {
|
||||
return err
|
||||
} else {
|
||||
hexBuf = hexBuf[:n]
|
||||
}
|
||||
if err := b.Unmarshal(hexBuf); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b Bytes16) ToHexString() string {
|
||||
return hex.EncodeToString(b[:])
|
||||
}
|
||||
|
||||
// Scan implements the Scanner interface.
|
||||
func (b *Bytes16) Scan(src any) error {
|
||||
switch src.(type) {
|
||||
case string:
|
||||
b.FromHexString([]byte(src.(string)))
|
||||
default:
|
||||
return errors.New("Incompatible type")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Value implements the driver Valuer interface.
|
||||
func (b Bytes16) Value() (driver.Value, error) {
|
||||
return b.ToHexString(), nil
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
// Package types contain misc data structures.
|
||||
package types
|
|
@ -0,0 +1,87 @@
|
|||
package types
|
||||
|
||||
import (
|
||||
"amuz.es/src/go/misc"
|
||||
"amuz.es/src/go/misc/strutil"
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// JSON is an alias for json.RawMessage, which is
|
||||
// a []byte underneath.
|
||||
// JSON implements Marshal and Unmarshal.
|
||||
type JSON json.RawMessage
|
||||
|
||||
// String output your JSON.
|
||||
func (j JSON) String() string {
|
||||
return strutil.B2S(j)
|
||||
}
|
||||
|
||||
// Unmarshal your JSON variable into dest.
|
||||
func (j JSON) Unmarshal(dest any) error {
|
||||
iter := misc.JSONCodec.BorrowIterator(nil)
|
||||
defer misc.JSONCodec.ReturnIterator(iter)
|
||||
iter.ReadVal(dest)
|
||||
return iter.Error
|
||||
}
|
||||
|
||||
// Marshal obj into your JSON variable.
|
||||
func (j *JSON) Marshal(obj any) (err error) {
|
||||
stream := misc.JSONCodec.BorrowStream(nil)
|
||||
defer misc.JSONCodec.ReturnStream(stream)
|
||||
|
||||
stream.WriteVal(obj)
|
||||
defer stream.Flush()
|
||||
if err = stream.Error; err != nil {
|
||||
return
|
||||
}
|
||||
buf := make(JSON, stream.Buffered())
|
||||
copy(buf, stream.Buffer())
|
||||
*j = buf
|
||||
return
|
||||
}
|
||||
|
||||
// UnmarshalJSON sets *j to a copy of data.
|
||||
func (j *JSON) UnmarshalJSON(data []byte) error {
|
||||
if j == nil {
|
||||
return errors.New("json: unmarshal json on nil pointer to json")
|
||||
}
|
||||
|
||||
*j = append((*j)[0:0], data...)
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON returns j as the JSON encoding of j.
|
||||
func (j JSON) MarshalJSON() ([]byte, error) {
|
||||
return j, nil
|
||||
}
|
||||
|
||||
// Value returns j as a value.
|
||||
// Unmarshal into RawMessage for validation.
|
||||
func (j JSON) Value() (driver.Value, error) {
|
||||
var r json.RawMessage
|
||||
if err := j.Unmarshal(&r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return []byte(r), nil
|
||||
}
|
||||
|
||||
// Scan stores the src in *j.
|
||||
func (j *JSON) Scan(src any) error {
|
||||
var source []byte
|
||||
|
||||
switch src.(type) {
|
||||
case string:
|
||||
source = []byte(src.(string))
|
||||
case []byte:
|
||||
source = src.([]byte)
|
||||
default:
|
||||
return errors.New("incompatible type for json")
|
||||
}
|
||||
|
||||
*j = JSON(append((*j)[0:0], source...))
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,119 @@
|
|||
package types
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestJSONString(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
j := JSON("hello")
|
||||
if j.String() != "hello" {
|
||||
t.Errorf("Expected %q, got %s", "hello", j.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONUnmarshal(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type JSONTest struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
var jt JSONTest
|
||||
|
||||
j := JSON(`{"Name":"hi","Age":15}`)
|
||||
err := j.Unmarshal(&jt)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if jt.Name != "hi" {
|
||||
t.Errorf("Expected %q, got %s", "hi", jt.Name)
|
||||
}
|
||||
if jt.Age != 15 {
|
||||
t.Errorf("Expected %v, got %v", 15, jt.Age)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONMarshal(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type JSONTest struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
jt := JSONTest{
|
||||
Name: "hi",
|
||||
Age: 15,
|
||||
}
|
||||
|
||||
var j JSON
|
||||
err := j.Marshal(jt)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if j.String() != `{"Name":"hi","Age":15}` {
|
||||
t.Errorf("expected %s, got %s", `{"Name":"hi","Age":15}`, j.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONUnmarshalJSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
j := JSON(nil)
|
||||
|
||||
err := j.UnmarshalJSON(JSON(`"hi"`))
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if j.String() != `"hi"` {
|
||||
t.Errorf("Expected %q, got %s", "hi", j.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONMarshalJSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
j := JSON(`"hi"`)
|
||||
res, err := j.MarshalJSON()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(res, []byte(`"hi"`)) {
|
||||
t.Errorf("Expected %q, got %v", `"hi"`, res)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONValue(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
j := JSON(`{"Name":"hi","Age":15}`)
|
||||
v, err := j.Value()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(j, v.([]byte)) {
|
||||
t.Errorf("byte mismatch, %v %v", j, v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONScan(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
j := JSON{}
|
||||
|
||||
err := j.Scan(`"hello"`)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(j, []byte(`"hello"`)) {
|
||||
t.Errorf("bad []byte: %#v ≠ %#v\n", j, string([]byte(`"hello"`)))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package types
|
||||
|
||||
import (
|
||||
"amuz.es/src/go/misc"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type JSONTime time.Time
|
||||
|
||||
func (dttm JSONTime) MarshalJSON() ([]byte, error) {
|
||||
stream := misc.JSONCodec.BorrowStream(nil)
|
||||
defer misc.JSONCodec.ReturnStream(stream)
|
||||
stream.WriteInt64(time.Time(dttm).UTC().Unix())
|
||||
return append([]byte(nil), stream.Buffer()...), stream.Error
|
||||
}
|
||||
|
||||
func (dttm *JSONTime) UnmarshalJSON(b []byte) error {
|
||||
iterator := misc.JSONCodec.BorrowIterator(b)
|
||||
defer misc.JSONCodec.ReturnIterator(iterator)
|
||||
valueType := iterator.WhatIsNext()
|
||||
switch valueType {
|
||||
case jsoniter.NumberValue:
|
||||
*dttm = JSONTime(time.Unix(iterator.ReadInt64(), 0))
|
||||
return nil
|
||||
default:
|
||||
return errors.Errorf("%d is wrong type", valueType)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,136 @@
|
|||
package types
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/subtle"
|
||||
"database/sql/driver"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type UUID [16]byte
|
||||
|
||||
var (
|
||||
zeroUUID UUID
|
||||
)
|
||||
|
||||
func (u UUID) Marshal() ([]byte, error) {
|
||||
return u[:], nil
|
||||
}
|
||||
|
||||
func (u UUID) MarshalTo(buf []byte) (n int, err error) {
|
||||
if len(u) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
copy(buf, u[:])
|
||||
return len(u), nil
|
||||
}
|
||||
func (u *UUID) Unmarshal(buf []byte) error {
|
||||
if len(buf) != 16 {
|
||||
return fmt.Errorf("invalid UUID (got %d bytes)", len(buf))
|
||||
}
|
||||
copy(u[:], buf)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u UUID) Compare(other UUID) int {
|
||||
return bytes.Compare(u[:], other[:])
|
||||
}
|
||||
|
||||
func (u UUID) Equal(other UUID) bool {
|
||||
return subtle.ConstantTimeCompare(u[:], other[:]) == 1
|
||||
}
|
||||
func (u *UUID) UnmarshalJSON(from []byte) error {
|
||||
quote := []byte("\"")
|
||||
quoteSize := len(quote)
|
||||
|
||||
if len(from) < quoteSize*2 {
|
||||
return errors.New("invalid quote notation")
|
||||
}
|
||||
|
||||
if !bytes.HasPrefix(from, quote) || !bytes.HasSuffix(from, quote) {
|
||||
return errors.New("invalid quote notation")
|
||||
} else if _, err := hex.Decode(u[:], from[quoteSize:len(from)-quoteSize]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u UUID) MarshalJSON() ([]byte, error) {
|
||||
var buffer bytes.Buffer
|
||||
buffer.WriteRune('"')
|
||||
buffer.WriteString(hex.EncodeToString(u[:]))
|
||||
buffer.WriteRune('"')
|
||||
return buffer.Bytes(), nil
|
||||
}
|
||||
|
||||
func (u *UUID) Size() int {
|
||||
if u == nil {
|
||||
return 0
|
||||
}
|
||||
if len(*u) == 0 {
|
||||
return 0
|
||||
}
|
||||
return 16
|
||||
}
|
||||
|
||||
func NewUUID() (u UUID) {
|
||||
newObj := UUID{}
|
||||
newObj.Random()
|
||||
return newObj
|
||||
}
|
||||
|
||||
func (u *UUID) UUIDFromHexString(buf []byte) error {
|
||||
hexBuf := make([]byte, hex.DecodedLen(len(buf)))
|
||||
if n, err := hex.Decode(hexBuf, buf); err != nil {
|
||||
return err
|
||||
} else {
|
||||
hexBuf = hexBuf[:n]
|
||||
}
|
||||
if err := u.Unmarshal(hexBuf); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u UUID) ToHexString() string {
|
||||
return hex.EncodeToString(u[:])
|
||||
}
|
||||
|
||||
// Scan implements the Scanner interface.
|
||||
func (u *UUID) Scan(src any) error {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
b, ok := src.([]byte)
|
||||
if !ok {
|
||||
return errors.New("Scan source was not []bytes")
|
||||
}
|
||||
|
||||
return u.UUIDFromHexString(b)
|
||||
}
|
||||
|
||||
// Value implements the driver Valuer interface.
|
||||
func (u UUID) Value() (driver.Value, error) {
|
||||
return u.ToHexString(), nil
|
||||
}
|
||||
|
||||
func (u *UUID) Random() *UUID {
|
||||
_, _ = rand.Read(u[:])
|
||||
u[6] = (u[6] & 0x0f) | 0x40 // Version 4
|
||||
u[8] = (u[8] & 0x3f) | 0x80 // Variant is 10
|
||||
return u
|
||||
}
|
||||
|
||||
func (u *UUID) Clear() *UUID {
|
||||
copy(u[:], zeroUUID[:])
|
||||
return u
|
||||
}
|
||||
|
||||
func (u UUID) IsZero() bool {
|
||||
return subtle.ConstantTimeCompare(zeroUUID[:], u[:]) == 1
|
||||
}
|
Loading…
Reference in New Issue