How to use NSFileHandle to read integer from file - objective-c

I want to read a list of integers from a text file, I just want to write code like
int temp;
fin>>temp;
But when I read the Cocoa documentation, I found NSFileHandle is suggested, and there is no method like I assumed, only one related method:
- (NSData *)readDataOfLength:(NSUInteger)length
Is there any class/method can help me do this in Objective C? Thanks.

I want to read a list of integers from a text file, I just want to
write code like
int temp; fin>>temp;
You have a lot of choices for file access. If you want to use C++-style access, you can, but you naturally need to open the file using the appropriate C++ file or stream methods. If you use Objective-C++, though, you can easily mix C++ code into your Objective-C.
You can also use the C standard library file routines like fopen(), fread(), etc.
Using C or C++ to read files is often a good choice if the files are coming from a source other than your program, something beyond your control.
But when I read Cocoa document, I found NSFileHandle is suggested, and
there is no method like I assumed, only one related method:
Again, lots of choices. Yes, you can use NSFileHandle to read bytes from the file into a NSData object, and then you can get ranges of bytes out of the data object. A much more common way to write and read data, though, is to use NSKeyedArchiver and NSKeyedUnarchiver:
NSData *data = [NSData dataWithContentsOfFile:pathToFile];
NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
int age = [unarchiver decodeIntForKey:#"Age"];
int weight = [unarchiver decodeIntForKey:#"Weight"];
NSString *name = [unarchiver decodeObjectForKey:#"Name"];
That's just the tip of the iceberg, though. It seems like a lot of code compared to what you were looking for, but it can also be a lot less work. Because objects and their relationships can be stored and read, you can read in a complex graph of objects with very little code:
OrgChart *chart = [NSUnarchiver unarchiveObjectWithFile:pathToFile];
Another option is to use property lists, which are very easy to use, but limited in the data types that can be used.
If you want to learn more about these topics, read Archives and Serializations Programming Guide, Property List Programming Guide, and File System Programming Guide.

You could use Objective-C++ and iostreams as you're used to.
You could use the C I/O functions like fopen and fscanf. (Probably what I'd do.)
Apple provides NSScanner for parsing, but it only reads from a string, not from an input stream. If you really want to use it, first you'll have to read your whole file (or a large chunk) into an NSString (or an NSData and then convert that to NSString).
If you can require the text file to be in JSON format, you can read it in one gulp and use NSJSONSerialization to parse it.

Related

Does NSMutableDictionary now truncate data as a string or add ellipses? It seems to, here

Does NSMutableDictionary now truncate data as a string, or return ellipses for long data? I use this feature to save a plist with different colors in it. This has worked fine (with minor modifications) since around 2005.
But last month, I think after an OS update, I noticed all of my data was starting to get corrupted. I've narrowed it down to this. When I run this code...
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
NSError *error = nil;
[dict setObject:[NSKeyedArchiver archivedDataWithRootObject:[NSColor redColor] requiringSecureCoding:NO error:&error] forKey:#"backdropColor"];
NSString *test = [dict description];
Note that before MacOS 10.13, you can use this code, which has the same bug.
[dict setObject:[NSArchiver archivedDataWithRootObject: [NSColor redColor]] forKey:#"backdropColor"];
When I run either, I get the following result:
backdropColor = {length = 3576, bytes = 0x62706c69 73743030 d4010203 04050607 ... 00000000 00000d88 };
See the ... ? That's not supposed to be there. It used to fill in that ... with all of the data.
I can't find any documentation that explains a change, and while this code has remained unchanged for years, it's now corrupted months of work for one of my users already.
Turning some of our comments into an answer:
-[NSObject description] is not meant to be a general-purpose parsing/serialization format, and over time, the descriptions of objects may change. In macOS Catalina, the description for NSData changed to truncate contents in the middle to avoid full display of enormous data blobs.
Currently, I throw a ton of objects into NSData and then export that using the description to a plist, which can then be easily parsed back into an NSData object later. That's all it needs to do, correct dump an NSData object out and read it back in.
Based on your minimal requirements, the simplest resolution for your problem is simply storing NSData objects directly in your plist, instead of their -descriptions. The plist format natively supports binary data, and all Foundation tools (like NSPropertyListSerialization) will accept NSData instances directly for writing out to disk.
If you would like to explicitly convert your binary data into a safely round-trippable string, consider converting it to a base64-encoded string using -[NSData base64EncodedStringWithOptions:], storing the string in the plist, and retrieving later with -[NSData initWithBase64EncodedString:options:].
However, if you require backwards compatibility with the old format (e.g. versions of your app running on macOS Catalina and newer must be able to save files readable on older versions of macOS and your app), you will need to write your own method for replicating the format.

Parsing Excel Data in Apple Swift

My current workflow involves using Applescript to essentially delimit Excel data and format it into plain text files. We're pushing towards an all Swift environment, but I haven't yet found any sort of kits for parsing my Excel data into Swift.
The only thing I can think of is to use C or something and wrap it, but that's not ideal. Any better suggestions for parsing this data for use in Swift?
The goal is to eliminate Applescript, but I'm not sure if that will be possible while still interacting with Excel files. Scripting Excel via Applescript seems to be the only method.
EDIT: I don't have the option of eliminating Excel from this workflow. This is how the data will be coming to the application, thus I have to include it.
Being able to streamline the process of parsing this data then processing it will be paramount. I know Applescript has been good in the past with helping me to process it; however, it's getting a little too closed-off for me.
I've been looking at writing something in Swift/Cocoa, but that still might require the data to be extracted with an Applescript, right?
A big plus for pushing Swift is the readability. I don't know Objective-C all that well, and swift would be an easier transition, I feel.
My workflow on PC has been using the COM object, which as has been said, isn't available in the Mac Excel app. I'm only looking for data extraction at this point. Some previous apps did processing within the app, but I'm looking to make this very self-contained, thus all processing within the app I'm developing. Once the data is extracted from the .XLS or .XLSX files, I'll be doing some text editing via RegEx and perhaps a little number crunching. Nothing too crazy. As of now, it will run on the client side, but I'm looking to extend this to a server process.
In Mac OS X 10.6 Snow Leopard Apple introduced the AppleScriptObjC framework which makes it very easy to interact between Cocoa and AppleScript. AppleScript code and a Objective-C like syntax can be used in the same source file. It's much more convenient than Scripting Bridge and NSAppleScript.
AppleScriptObjC cannot be used directly in Swift because the command loadAppleScriptObjectiveCScripts of NSBundle is not bridged to Swift.
However you can use a Objective-C bridge class for example
ASObjC.h
#import Foundation;
#import AppleScriptObjC;
#interface NSObject (Excel)
- (void)openExcelDocument:(NSString *)filePath;
- (NSArray *)valueOfUsedRange;
#end
#interface ASObjC : NSObject
+ (ASObjC *)sharedASObjC;
#property id Excel;
#end
ASObjC.m
#import "ASObjC.h"
#implementation ASObjC
+ (void)initialize
{
if (self == [ASObjC class]) {
[[NSBundle mainBundle] loadAppleScriptObjectiveCScripts];
}
}
+ (ASObjC *)sharedASObjC
{
static id sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[ASObjC alloc] init];
});
return sharedInstance;
}
- (instancetype)init
{
self = [super init];
if (self) {
_Excel = NSClassFromString(#"ASExcel");
}
return self;
}
#end
Create a AppleScript source file form the AppleScriptObjC template
ASExcel.applescript
script ASExcel
property parent: class "NSObject"
on openExcelDocument:filePath
set asFilePath to filePath as text
tell application "Microsoft Excel"
set sourceBook to open workbook workbook file name asFilePath
repeat
try
get workbooks
return
end try
delay 0.5
end repeat
end tell
end openDocument
on valueOfUsedRange()
tell application "Microsoft Excel"
tell active sheet
set activeRange to used range
return value of activeRange
end tell
end tell
end valueOfUsedRange
end script
Link to the AppleScriptObjC framework if necessary.
Create the Bridging Header and import ASObjC.h
Then you can call AppleScriptObjC from Swift with
ASObjC.sharedASObjC().Excel.openExcelDocument("Macintosh HD:Users:MyUser:Path:To:ExcelFile.xlsx")
or
let excelData = ASObjC.sharedASObjC().Excel.valueOfUsedRange() as! Array<[String]>
It's somewhat unclear if you're trying to eliminate Excel as a dependency (which is not unreasonable: it costs money and not everyone has it) or AppleScript as a language (totally understandable, but a bad practical move as Apple's alternatives for application automation all suck).
There are third-party Excel-parsing libraries available for other languages, e.g. I've used Python's openpyxl (for .xlsx files) and xlrd (for .xsl) libraries successfully in my own projects. And I see through the magicks of Googles that someone's written an ObjC framework, DHlibxls, which [assuming no dynamic trickery] should be usable directly from Swift, but I've not used it myself so can't tell you anything more.
You can use ScriptingBridge or NSAppleScript to interact with Apple Scriptable stuff
ScriptingBridge can generate a header file from the Apple Script dictionary.
NSAppleScript can execute any AppleScript for you by passing a String
1. Export to plaintext CSV
If all you're trying to do is extract data from Excel to use elsewhere, as opposed to capturing Excel formulas and formatting, then you probably should not try to read the .xls file. XLS is a complex format. It's good for Excel, not for general data interchange.
Similarly, you probably don't need to use AppleScript or anything else to integrate with Excel, if all you want to do is save the data as plaintext. Excel already knows how to save data as plaintext. Just use Excel's "Save As" command. (That's what it's called on the Mac. I don't know about PCs.)
The question is then what plaintext format to use. One obvious choice for this is a plaintext comma-separated value file (CSV) because it's a simple de facto standard (as opposed to a complex official standard like XML). This will make it easy to consume in Swift, or in any other language.
2. Export in UTF-8 encoding if possible, otherwise as UTF-16
So how do you do that exactly? Plaintext is wonderfully simple, but one subtlety that you need to keep track of is the text encoding. A text encoding is a way of representing characters in a plaintext file. Unfortunately, you cannot reliably tell the encoding of a file just by inspecting the file, so you need to choose an encoding when you save it and remember to use that encoding when you read it. If you mess this up, accented characters, typographer's quotation marks, dashes, and other non-ASCII characters will get mangled. So what text encoding should you use? The short answer is, you should always use UTF-8 if possible.
But if you're working with an older version of Excel, then you may not be able to use UTF-8. In that case, you should use UTF-16. In particular, UTF-16 is, I believe, the only export option in Excel 2011 for Mac which produces a predictable result which will not depend in surprising ways on obscure locale settings or Microsoft-specific encodings.
So if you're on Excel 2011 for Mac, for instance, choose "UTF-16 Unicode Text" from Excel's Save As command.
This will cause Excel to save the file so that every row is a line of text, and every column is separated by a tab character. (So technically, this is a tab-separated value files, rather than a comma-separated value file.)
3. Import with Swift
Now you have a plaintext file, which you know was saved in a UTF-8 (or UTF-16) encoding. So now you can read it and parse it in Swift.
If your Excel data is complicated, you may need a full-featured CSV parser. The best choice is probably CHCSVParser.
Using CHCSV, you can parse the file with the following code:
NSURL * const inputFileURL = [NSURL fileURLWithPath:#"/path/to/exported/file.txt"];
unichar tabCharacter = '\t';
NSArray *rows = [NSArray arrayWithContentsOfCSVFile:inputFilePath options:CHCSVParserOptionsSanitizesFields
delimiter:tabCharacter];
(You could also call it from Swift, of course.)
On the other hand, if you're data is relatively simple (for instance, it has no escaped characters), then you might not need to use an external library at all. You can write some Swift code that parses tab-separated values just by reading in the file as a string, splitting on newlines, and then splitting on tabs.
This function will take a String representing TSV data and return an array of dictionaries:
/**
Reads a multiline, tab-separated String and returns an Array<NSictionary>, taking column names from the first line or an explicit parameter
*/
func JSONObjectFromTSV(tsvInputString:String, columnNames optionalColumnNames:[String]? = nil) -> Array<NSDictionary>
{
let lines = tsvInputString.componentsSeparatedByString("\n")
guard lines.isEmpty == false else { return [] }
let columnNames = optionalColumnNames ?? lines[0].componentsSeparatedByString("\t")
var lineIndex = (optionalColumnNames != nil) ? 0 : 1
let columnCount = columnNames.count
var result = Array<NSDictionary>()
for line in lines[lineIndex ..< lines.count] {
let fieldValues = line.componentsSeparatedByString("\t")
if fieldValues.count != columnCount {
// NSLog("WARNING: header has %u columns but line %u has %u columns. Ignoring this line", columnCount, lineIndex,fieldValues.count)
}
else
{
result.append(NSDictionary(objects: fieldValues, forKeys: columnNames))
}
lineIndex = lineIndex + 1
}
return result
}
So you only need to read the file into a string and pass it to this function. That snippet comes from this gist for a tsv-to-json converter. And if you need to know more about which text encodings Microsoft products produce, and which ones Cocoa can auto-detect, then this repo on text encoding contains the research on export specimens which led to the conclusion that UTF-16 is the way to go for old Microsoft products on the Mac.
(I realize I'm linking to my own repos here. Apologies?)
There is no need to export Excel files to CSV for Swift as you can use an existing open-source library for parsing XLSX files. If you use CocoaPods or Swift
Package Manager for integrating 3rd-party libraries, CoreXLSX supports those. After the library is integrated, you can use it like this:
import CoreXLSX
guard let file = XLSXFile(filepath: "./file.xlsx") else {
fatalError("XLSX file corrupted or does not exist")
}
for path in try file.parseWorksheetPaths() {
let ws = try file.parseWorksheet(at: path)
for row in ws.sheetData.rows {
for c in row.cells {
print(c)
}
}
}
This will open file.xlsx and print all cells within that file. You can also filter cells by references and access only cell data that you need for your automation.

Building a custom NSArchiver serialize to string

How does NSArchiver serialize to file? I assume it's serialized in binary format, is that correct? What if I want to store it in string so I can store into SQLite database? Do I need to write my own custom NSArchiver? If so, how do I go about doing that? Are there any tutorials out there?
p.s. I do realize Core Data can do this but let me cross that option out for now.
You can archive to an NSData object instead of to a file, if you want, with +archivedDataWithRootObject:. It won't be a "string," but that's fine, because an NSString in Cocoa represents a sequence of Unicode characters, while an NSData represents a sequence of bytes (which you could easily store wherever you want, including in a database).
Note that you really should be using NSKeyedArchiver instead:
+ (NSData *)archivedDataWithRootObject:(id)rootObject
+ (id)unarchiveObjectWithData:(NSData *)data

NSLocalizedString using external sources in Objective C?

Is there a way to make the localization or Localizable.strings read from directories outside NSBundle?
I am trying to make my app read localizations from a file that is downloaded via a server, is there a way good way to do this?
Thanks in advance.
You will have to write your own MyLocalizedString function which reads the file manually. A .strings file is actually an old type of property list, so it can be read using the NSPropertyListSerialization class like this:
id plist = [NSPropertyListSerialization
propertyListWithData:[NSData dataWithContentsOfFile:stringsFilePath]
options:0
format:NULL
error:&error];
plist is just an NSDictionary, so you can read a string value from the result like this:
[plist objectForKey:#"my_string"];
You should probably implement some sort of cache so that you aren't parsing the whole file for each string lookup.
Note that if you are using genstrings and the other related command line tools, you can use the following option to specify the name of your custom lookup function:
Usage: genstrings [OPTION] file1.[mc] ... filen.[mc]
...[snip]...
-s substring substitute 'substring' for NSLocalizedString.
If these strings files are being uploaded to a server, you can improve performance in the app a bit by first converting them to the binary plist format (this is what Xcode would normally do during a build):
plutil -convert binary1 myStrings.strings
You will have to put together the pieces yourself. There isn't any built-in facility to do that. Your localizations can just be serialized dictionaries with the development language string as the key and the localized string as the value. You can serialize as a plist, which may be most convenient to edit, or using a keyed archive, which may be more compact.

Generating large NSString xml

I have a plist and i want to convert it to xml. The xml itself is going to be around 1.2mb in size. What the best way to generate this xml? Simply with a NSMutableString? I am just worried about the performance issues and wether there is a better way to generate xml.
Thanks
For those wondering, what I have right now is something like this:
NSString *xml = [NSString stringWithFormat:#"<Sheet>%#</Sheet>", [self getSheetXMLString]];
and then, in getSheetXMLString method, i have more methods like above which drill down deep until the plist is fully transversed.
Thanks again.
What do you plan to do with the XML, if it is to output over a network or write to a file then instead of creating a NSString you could just write straight out to the network/file. If you plan to do manipulation if the XML you may want to consider libxml2, which is a C library included in iOS.